题意:
金明有n块钱,他有m样东西可买,这m样东西每份都有自己的价格v与重要度(lv:1~5),求在他不超过n元的前提下,使每件物品的价格与重要度的乘积的总和最大。
分析:
Dp的话要开一个二维数组,n < 30000, m < 25, 这个二维数组还是可以开出来的。
接着就是简单的dp模板题了。
1 int rec(int i, int j) 2 { 3 if(dp[i][j] >= 0) return dp[i][j]; // ① 4 5 int res; 6 if(i == m) res = 0; // ② 7 else if(j < buy[i].v) res = rec(i + 1, j); //③ 8 else res = max(rec(i + 1, j), rec(i + 1, j - buy[i].v) + buy[i].v * buy[i].l); // ④ 9 10 return dp[i][j] = res; 11 }
这里使用的记忆化搜索以减少搜索时间。
① :判断是否已经搜过这点,是则直接返回;
② :已经搜索完物品
③ :背包装不下该物品的重量(这里是价值量),则跳过该物品
④ :可以选,比较选与不选哪两者的价值更大,谁大就先记录哪个
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<algorithm> 5 using namespace std; 6 const int maxn = 26; 7 int n, m; 8 struct node{ 9 int v; // 价值 10 int l; // 等级 11 }buy[maxn]; 12 int dp[maxn+1][30005]; 13 int ans, res; 14 15 int rec(int i, int j) 16 { 17 if(dp[i][j] >= 0) return dp[i][j]; 18 // printf("[%d][%d] ", i, j); 19 int res; 20 if(i == m) res = 0; 21 else if(j < buy[i].v) res = rec(i + 1, j); 22 else res = max(rec(i + 1, j), rec(i + 1, j - buy[i].v) + buy[i].v * buy[i].l); 23 24 return dp[i][j] = res; 25 } 26 27 int main() 28 { 29 while(cin >> n >> m){ 30 for(int i = 0; i < m; i++) cin >> buy[i].v >> buy[i].l; 31 memset(dp, -1, sizeof(dp)); 32 33 cout << rec(0, n) << endl; 34 } 35 return 0; 36 }
下一题:
题意:
Uim与小A去吃饭,这家店的菜式有n种但每种只能选一次,uim有m块钱,保证能把m块钱刚好花完,问有多少种点菜方式。如,n = 4,m = 4,n道菜分别1,1,2,2(元),即有3种选择[1,1,2(3)][1,1,2(4)][2,2]。
分析:
本题跪了,因为我还领略到dp的精髓,于是看了题解。
现在我可以粗略地认为,dp在一些问题上,就是“选与不选”的关系,然后再判断这关系前后的结果问题。
比如这题:
点菜只有点与不点的关系,并且数据了保证m块一定会被花完,因此就有以下关系:
If(j – 第i道菜的价格) dp[i][j] = dp[i-1][j] + 1; // 钱刚好充足,则点菜的方案+1
这个是钱充足的特例,当钱不刚好充足的时候,即钱比菜钱多的时候有以下关系:
If(i-第i道菜的价格)dp[i][j] = dp[i-1][j] + f[I-1][j-v[i]]; // 吃这道菜之前的方案数加上吃这道菜的方案数
如果钱不充足的话,就不吃(白给),与吃第i-1道菜的方案数相同:
If(i-第i道菜的价格)dp[i][j] = dp[i-1][j];
最后输出dp[n][m]即可得到AC,因为以上遍历的意思就是:有m钱,对于吃这n道菜的方案数,所以,dp[n][m]就是最终答案。
感谢洛谷作者 sslzgrh 的题解分析。
1 #include<cstdio> 2 #include<iostream> 3 #include<cstring> 4 using namespace std; 5 6 int n, m; 7 int dp[102][10003]; 8 int v[102]; 9 10 int main() 11 { 12 while(cin >> n >> m){ 13 for(int i = 1; i <= n; i++) cin >> v[i]; 14 15 for(int i = 1; i <= n; i++){ 16 for(int j = 1; j <= m; j++){ 17 if(j == v[i]) dp[i][j] = dp[i-1][j] + 1; 18 if(j > v[i]) dp[i][j] = dp[i-1][j] + dp[i-1][j-v[i]]; 19 if(j < v[i]) dp[i][j] = dp[i-1][j]; 20 } 21 } 22 cout << dp[n][m] << endl; 23 } 24 return 0; 25 }
先暂停下,这时我开始对dp有点头绪了:dp也是搜索的一种,只不过这种搜索未必能把所有情况搜索完毕。解题的关键是找出“是”与“非”二者之间的关系所带来的关系,以及前一种关系与后一种关系之间的关系(这么说是不是已经晕了)。我现在困惑的是,如何才能构造出与表达这种关系相适应的算法?也许这只能多做题了吧。
下一题:
题意:
辰辰到山里采药,他有T个小时,山里有M株草药,每株草药都有自己所需要花费的时间t与价值v,问在T小时辰辰采到的药草的最大价值总和。
分析:
?????这不就是完全的01背包问题了吗?比开心的金明还要01背包问题,即关系只有采与不采的关系,完全的水题。
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 using namespace std; 5 6 int T, M; 7 int t[102]; 8 int v[102]; 9 int dp[102][1002]; // 默认每株草药花费的时间在1~1000 10 11 int main() 12 { 13 while(cin >> T >> M){ 14 for(int i = 0; i < M; i++) cin >> t[i] >> v[i]; 15 16 for(int i = 0; i < M; i++){ 17 for(int j = 0; j <= T; j++){ 18 if(j < t[i]) 19 dp[i+1][j] = dp[i][j]; 20 else 21 dp[i+1][j] = max(dp[i][j], dp[i][j-t[i]] + v[i]); 22 } 23 } 24 cout << dp[M][T] << endl; 25 } 26 return 0; 27 }
下一题:
题意:
有个容量为V的箱子,并且有n个物品,每个物品有属于自己的体积v,问最后装得的箱子的容量最小。
分析:
同样也是01背包问题,只不过最后要还要用总量减去最大值。
V – dp[n][V]
1 #include<cstdio> 2 #include<iostream> 3 #include<cstring> 4 using namespace std; 5 6 int V, n; 7 int bag[32]; 8 int dp[32][20006]; 9 10 int main() 11 { 12 while(cin >> V >> n){ 13 for(int i = 0; i < n; i++) cin >> bag[i]; 14 15 for(int i = 0; i < n; i++){ 16 for(int j = 0; j <= V; j++){ 17 if(j < bag[i]) 18 dp[i+1][j] = dp[i][j]; 19 else 20 dp[i+1][j] = max(dp[i][j], dp[i][j-bag[i]] + bag[i]); 21 } 22 } 23 cout << V - dp[n][V] << endl; 24 } 25 return 0; 26 }
下一题:
题意:
与采药一题题意几乎一样,只不过草药可以无限采。
分析:
可以无限采的话就变成了完全背包问题,如果数据小的话,这样写就可以AC了
1 #include<iostream> 2 #include<cstdio> 3 using namespace std; 4 5 int T, M; 6 int t[10005], v[10005]; 7 int dp[100005] 8 int main() 9 { 10 while(cin >> T >> M){ 11 for(int i = 0; i < M; i++) cin >> t[i] >> v[i]; 12 13 for(int i = 0; i < M; i++){ 14 for(int j = 0; j <= T; j++){ 15 if(j < t[i]) 16 dp[i+1][j] = dp[i][j]; 17 else 18 dp[i+1][j] = max(dp[i][j], dp[i+1][j-t[i]] + v[i]); 19 } 20 } 21 cout << dp[M][T] << endl; 22 } 23 return 0; 24 }
但该题数据大的让人恶心,开二维数组是一定会爆的,所以得转换一下(分析贴在代码里):
感谢洛谷作者神云_cloud的题解:
1 #include<iostream> 2 #include<cstdio> 3 using namespace std; 4 5 int T, M; 6 int t[10005], v[10005]; 7 int dp[100005]; 8 int main() 9 { 10 while(cin >> T >> M){ 11 for(int i = 0; i < M; i++) cin >> t[i] >> v[i]; 12 13 for(int i = 0; i < M; i++){ 14 for(int j = t[i]; j <= T; j++){ // 把原来的 j = 0 改成 j = t[i] 15 // if(j < t[i]) 16 // dp[i+1][j] = dp[i][j]; 17 // else // 这样一来就默认 j <= T 了 18 dp[j] = max(dp[j], dp[j-t[i]] + v[i]); 19 } 20 } 21 cout << dp[T] << endl; 22 } 23 return 0; 24 }
最后小结:
以上题目都是被洛谷标记为普及难度的dp题,包含了01背包问题与完全背包问题,通过这几题的练习可以熟悉dp的模板与特性。不过dp相比贪心算法的思路还是更具有图表型,但有时太固执于图表又会陷入数据过大而不知所措的困境(如疯狂的采药这题)。
还是那种感觉,dp算法是更多解决“拿“与”不拿“的问题。但数据如果变大了要怎么办?这得要对dp的思维特别熟悉才能够把dp优化。所以,除了会”默写“,更重要的还是要掌握思想呀。
或许你会觉得我这有些分析写跟没写一样,因为我觉得真的都是模板题或者加一点转变,相信聪明的你熟悉模板的话一看就想到该怎么写了。我是拿着《(第2版)挑战程序设计竞赛》一书边看别学边敲的,所以感觉知道有这样的模板算法,一看题目就知道是水题了quq,着实抱歉。
谢谢你能看到最后。