AcWing算法提高课【第一章动态规划3】背包模型
423. 采药
题目
辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。
为此,他想拜附近最有威望的医师为师。
医师为了判断他的资质,给他出了一个难题。
医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是辰辰,你能完成这个任务吗?
输入格式
输入文件的第一行有两个整数 T 和 MM,用一个空格隔开,T 代表总共能够用来采药的时间,M 代表山洞里的草药的数目。
接下来的 M 行每行包括两个在 1 到 100 之间(包括 1 和 100)的整数,分别表示采摘某株草药的时间和这株草药的价值。
输出格式
输出文件包括一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。
数据范围
1≤T≤1000,
1≤M≤100输入样例:
70 3 71 100 69 1 1 2
输出样例:
3
分析:
一看就是0/1背包模型,时间对应着体积,价值对应着价值。
每次面临两种选择,选和不选、
代码:
二维代码:
1 #include <iostream> 2 3 using namespace std; 4 5 const int N = 1010; 6 7 int n, m; 8 int v[N], w[N]; 9 int f[N][N];//含义:选到第i个物品,当前体积为j的情况下,价值最大 10 11 int main() 12 { 13 cin >> m >> n; 14 for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i]; 15 16 for (int i = 1; i <= n; i ++ ) 17 { 18 for (int j = 0; j <= m; j ++ ) 19 { 20 f[i][j] = f[i - 1][j]; 21 if (j - v[i] >= 0) 22 f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]); 23 } 24 } 25 26 cout << f[n][m] << endl; 27 28 return 0; 29 }
二维代码:
1 #include <iostream> 2 3 using namespace std; 4 5 const int N = 1010; 6 7 int n, m; 8 int v[N], w[N]; 9 int f[N][N];//含义:选到第i个物品,当前体积为j的情况下,价值最大 10 11 int main() 12 { 13 cin >> m >> n; 14 for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i]; 15 16 for (int i = 1; i <= n; i ++ ) 17 { 18 for (int j = 0; j <= m; j ++ ) 19 f[i][j] = f[i - 1][j];//不选当前第i个物品,那么f维护的最大值这个属性,就是前面的值 20 for (int j = 0; j <= m; j ++ ) 21 if (j - v[i] >= 0) 22 f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]); 23 24 } 25 26 cout << f[n][m] << "\n"; 27 return 0; 28 }
滚动数组代码:
1 #include <iostream> 2 3 using namespace std; 4 5 const int N = 1010; 6 7 int n, m; 8 int v[N], w[N]; 9 int f[2][N];//含义:选到第i个物品,当前体积为j的情况下,价值最大 10 11 int main() 12 { 13 cin >> m >> n; 14 for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i]; 15 16 for (int i = 1; i <= n; i ++ ) 17 { 18 for (int j = 0; j <= m; j ++ ) 19 f[i & 1][j] = f[(i - 1) & 1][j];//不选当前第i个物品,那么f维护的最大值这个属性,就是前面的值 20 for (int j = 0; j <= m; j ++ ) 21 if (j - v[i] >= 0) 22 f[i & 1][j] = max(f[i & 1][j], f[(i - 1) & 1][j - v[i]] + w[i]); 23 24 } 25 26 cout << f[n & 1][m] << "\n"; 27 return 0; 28 }
一维代码:
1 #include <iostream> 2 3 using namespace std; 4 5 const int N = 1010; 6 7 int n, m; 8 int v[N], w[N]; 9 int f[N];//含义:选到第i个物品,当前体积为j的情况下,价值最大 10 11 int main() 12 { 13 cin >> m >> n; 14 for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i]; 15 16 for (int i = 1; i <= n; i ++ ) 17 for (int j = m; j >= v[i]; j -- ) 18 f[j] = max(f[j], f[j - v[i]] + w[i]); 19 //j~m为当前的第i个物品,也就是第i层,而第i层,则是由1~j-1,由i-1层决定的。 20 cout << f[m] << endl; 21 return 0; 22 }
1024. 装箱问题
题目:
有一个箱子容量为 V,同时有 n 个物品,每个物品有一个体积(正整数)。
要求 n 个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。
输入格式
第一行是一个整数 V,表示箱子容量。
第二行是一个整数 n,表示物品数。
接下来 n 行,每行一个正整数(不超过10000),分别表示这 n 个物品的各自体积。
输出格式
一个整数,表示箱子剩余空间。
数据范围
0<V≤20000,
0<n≤30输入样例:
24 6 8 3 12 7 9 7
输出样例:
0
分析:
还是一个0/1背包问题,这里的物品体积和价值都是我们的物品体积,然后每个物品只能选择一个,要在m体积内拼出一个最大体积,这个体积一定小于m
代码:
二维写法:
1 #include <iostream> 2 3 using namespace std; 4 5 const int N = 40, M = 20010; 6 7 int n,m; 8 int a[N]; 9 int f[N][M]; 10 11 int main() 12 { 13 cin >> m >> n; 14 for (int i = 1; i <= n; i ++ ) cin >> a[i]; 15 16 for (int i = 1; i <= n; i ++ ) 17 { 18 for (int j = 0; j <= m; j ++ ) 19 f[i][j] = f[i - 1][j]; 20 for (int j = 0; j <= m; j ++ ) 21 if (j - a[i] >= 0) 22 f[i][j] = max(f[i][j], f[i - 1][j - a[i]] + a[i]); 23 24 } 25 26 cout << m - f[n][m] << endl; 27 28 return 0; 29 }
一维写法:
1 #include <iostream> 2 3 using namespace std; 4 5 const int N = 40, M = 20010; 6 7 int n,m; 8 int a[N]; 9 int f[M]; 10 11 int main() 12 { 13 cin >> m >> n; 14 15 for (int i = 1; i <= n; i ++ ) 16 { 17 int x; cin >> x; 18 for (int j = m; j >= x; j -- ) 19 f[j] = max(f[j], f[j - x] + x); 20 } 21 22 cout << m - f[m] << endl; 23 24 return 0; 25 }
1022. 宠物小精灵之收服
题目:
宠物小精灵是一部讲述小智和他的搭档皮卡丘一起冒险的故事。
一天,小智和皮卡丘来到了小精灵狩猎场,里面有很多珍贵的野生宠物小精灵。
小智也想收服其中的一些小精灵。
然而,野生的小精灵并不那么容易被收服。
对于每一个野生小精灵而言,小智可能需要使用很多个精灵球才能收服它,而在收服过程中,野生小精灵也会对皮卡丘造成一定的伤害(从而减少皮卡丘的体力)。
当皮卡丘的体力小于等于0时,小智就必须结束狩猎(因为他需要给皮卡丘疗伤),而使得皮卡丘体力小于等于0的野生小精灵也不会被小智收服。
当小智的精灵球用完时,狩猎也宣告结束。
我们假设小智遇到野生小精灵时有两个选择:收服它,或者离开它。
如果小智选择了收服,那么一定会扔出能够收服该小精灵的精灵球,而皮卡丘也一定会受到相应的伤害;如果选择离开它,那么小智不会损失精灵球,皮卡丘也不会损失体力。
小智的目标有两个:主要目标是收服尽可能多的野生小精灵;如果可以收服的小精灵数量一样,小智希望皮卡丘受到的伤害越小(剩余体力越大),因为他们还要继续冒险。
现在已知小智的精灵球数量和皮卡丘的初始体力,已知每一个小精灵需要的用于收服的精灵球数目和它在被收服过程中会对皮卡丘造成的伤害数目。
请问,小智该如何选择收服哪些小精灵以达到他的目标呢?
输入格式
输入数据的第一行包含三个整数:N,M,K,分别代表小智的精灵球数量、皮卡丘初始的体力值、野生小精灵的数量。
之后的K行,每一行代表一个野生小精灵,包括两个整数:收服该小精灵需要的精灵球的数量,以及收服过程中对皮卡丘造成的伤害。
输出格式
输出为一行,包含两个整数:C,R,分别表示最多收服C个小精灵,以及收服C个小精灵时皮卡丘的剩余体力值最多为R。
数据范围
0<N≤1000,
0<M≤500,
0<K≤100输入样例1:
10 100 5 7 10 2 40 2 50 1 20 4 20
输出样例1:
3 30
输入样例2:
10 100 5 8 110 12 10 20 10 5 200 1 110
输出样例2:
0 100
分析:
这一题同样还是0/1背包的模型,变化的唯一地方就在于原来的背包容量由模型中的一个,到现在变成了两个,限制多加了一维,但是还是可以按照0/1被包的方式直接套路出来的。
三位滚动数组(第二问一直不对,没搞好呢):
1 #include <iostream> 2 3 using namespace std; 4 5 const int N = 1010, M = 510, K = 110; 6 7 int n, m, k;//精灵球的数量,皮卡丘的体积,精灵的数量 8 int f[2][N][M]; 9 10 int main() 11 { 12 cin >> n >> m >> k; 13 14 for (int i = 0; i < k; i ++ ) 15 { 16 int v1, v2; cin >> v1 >> v2; 17 for (int j = 0; j <= n; j ++ ) 18 for (int t = 0; t <= m - 1; t ++ ) 19 f[i & 1][j][t] = f[(i - 1) & 1][j][t]; 20 //如果是等于m的话,说明体力恰好消耗完,这就很亏哦,因为当体力为0的时候,认为捕获失败 21 for (int j = 0; j <= n; j ++ ) 22 for (int t = 0; t <= m - 1; t ++ ) 23 if (j - v1 >= 0 && t - v2 >= 0) 24 f[i & 1][j][t] = max(f[i & 1][j][t], f[(i - 1) & 1][j - v1][t - v2] + 1); 25 } 26 27 cout << f[k & 1][n][m - 1] << " "; 28 29 int t = m - 1; 30 while (t > 0 && f[1][n][t - 1] == f[1][n][m - 1]) t --; 31 int s = m - 1; 32 while (s > 0 && f[0][n][s - 1] == f[0][n][m - 1]) s --; 33 cout << m - min(t, s) << "\n"; 34 35 return 0; 36 }
消一维代码
1 #include <iostream> 2 3 using namespace std; 4 5 const int N = 1010, M = 510, K = 110; 6 7 int n, m, k;//精灵球的数量,皮卡丘的体积,精灵的数量 8 int v1[K], v2[K]; 9 int f[N][M]; 10 11 int main() 12 { 13 cin >> n >> m >> k; 14 for (int i = 1; i <= k; i ++ ) cin >> v1[i] >> v2[i]; 15 16 for (int i = 1; i <= k; i ++ ) 17 { 18 for (int j = n; j >= v1[i]; j -- ) 19 for (int t = m - 1; t >= v2[i]; t -- ) 20 f[j][t] = max(f[j][t], f[j - v1[i]][t - v2[i]] + 1); 21 } 22 23 cout << f[n][m - 1] << " "; 24 int t = m - 1; 25 while (t > 0 && f[n][t - 1] == f[n][m - 1]) t --; 26 cout << m - t << "\n"; 27 }
278. 数字组合
题目:
给定 N个正整数 A1,A2,…,AN,从中选出若干个数,使它们的和为 M,求有多少种选择方案。
输入格式
第一行包含两个整数 N 和 M。
第二行包含 N 个整数,表示 A1,A2,…,AN.
输出格式
包含一个整数,表示可选方案数。
数据范围
1≤N≤100
1≤M≤10000
1≤Ai≤1000输入样例:
4 4 1 1 2 2
输出样例:
3
分析:
还是0/1背包的模板,这次我们不是求得最值,而是求的方案数,这是需要累加的。
那个例子来说明,如果我们要组成一个整数x,他的来源有三个,如x-1,x-2,x-3,我们该怎么求得x的方案数呢,肯行是将他来源的方案数加起来才能得到我们的方案数嘛,这应该就是同时发生相加这回事吧?
代码:
1 //这种方案数的题目,在我印象中似乎是需要讲边界设置一下的,保证答案只能从一个起点开始 2 #include <iostream> 3 4 using namespace std; 5 6 const int N = 110, M = 10010; 7 8 int n, m; 9 int f[M]; 10 11 int main() 12 { 13 cin >> n >> m; 14 15 f[0] = 1;//边界,只有0可以给我们贡献 16 for (int i = 1; i <= n; i ++ ) 17 { 18 int x; cin >> x; 19 for (int j = m; j >= x; j -- ) 20 f[j] += f[j - x]; 21 } 22 23 cout << f[m] << endl; 24 25 return 0; 26 }
1023. 买书
题目:
小明手里有n元钱全部用来买书,书的价格为10元,20元,50元,100元。
问小明有多少种买书方案?(每种书可购买多本)
输入格式
一个整数 n,代表总共钱数。
输出格式
一个整数,代表选择方案种数。
数据范围
0≤n≤1000
输入样例1:
20
输出样例1:
2
输入样例2:
15
输出样例2:
0
输入样例3:
0
输出样例3:
1
分析:
注意这题和上一题的区别,上一题是从n个物品中选择若干个的方案数,而这一题是给定几个物品,每个物品都有无限个可以选择。
这显然是一个完全背包的问题。
代码:
1 //还是一个求方案数的题目,不要忘记初始化f[0] = 1; 2 //这是一个完全背包问题,每个物品都可以选择无数次 3 #include <iostream> 4 5 using namespace std; 6 7 const int N = 1010; 8 9 int n; 10 int a[4] = {10, 20, 50, 100}; 11 int f[N]; 12 13 int main() 14 { 15 cin >> n; 16 17 f[0] = 1; 18 for (int i = 0; i < 4; i ++ ) 19 for (int j = a[i]; j <= n; j ++ ) 20 f[j] += f[j - a[i]]; 21 22 cout << f[n] << "\n"; 23 24 return 0; 25 }
1021. 货币系统
题目:
给你一个n种面值的货币系统,求组成面值为m的货币有多少种方案。
输入格式
第一行,包含两个整数n和m。
接下来n行,每行包含一个整数,表示一种货币的面值。
输出格式
共一行,包含一个整数,表示方案数。
数据范围
n≤15,m≤3000
输入样例:
3 10 1 2 5
输出样例:
10
分析:
显然这是一个完全背包求方案数的题目,注意开long long了
代码:
1 //n种货币,可选无数次,显然是一个完全背包问题 2 #include <iostream> 3 4 using namespace std; 5 6 typedef long long ll; 7 const int N = 3010; 8 9 int n, m; 10 ll f[N]; 11 12 int main() 13 { 14 cin >> n >> m; 15 16 f[0] = 1; 17 while (n -- ) 18 { 19 int x; cin >> x; 20 for (int i = x; i <= m; i ++ ) 21 f[i] += f[i - x]; 22 } 23 24 cout << f[m] << "\n"; 25 26 return 0; 27 }
532. 货币系统
题目:
在网友的国度中共有 n 种不同面额的货币,第 i 种货币的面额为 a[i],你可以假设每一种货币都有无穷多张。
为了方便,我们把货币种数为 n、面额数组为 a[1..n] 的货币系统记作 (n,a)。
在一个完善的货币系统中,每一个非负整数的金额 xx 都应该可以被表示出,即对每一个非负整数 x,都存在 n 个非负整数 t[i]t[i] 满足 a[i]×t[i]a[i]×t[i] 的和为 xx。
然而,在网友的国度中,货币系统可能是不完善的,即可能存在金额 xx 不能被该货币系统表示出。
例如在货币系统 n=3, a=[2,5,9]中,金额 1,3就无法被表示出来。
两个货币系统 (n,a)和 (m,b是等价的,当且仅当对于任意非负整数 x,它要么均可以被两个货币系统表出,要么不能被其中任何一个表出。
现在网友们打算简化一下货币系统。
他们希望找到一个货币系统 (m,b)满足 (m,b)与原来的货币系统 (n,a)等价,且 m 尽可能的小。
他们希望你来协助完成这个艰巨的任务:找到最小的 m。
输入格式
输入文件的第一行包含一个整数 T,表示数据的组数。
接下来按照如下格式分别给出 T 组数据。
每组数据的第一行包含一个正整数 n。
接下来一行包含 n 个由空格隔开的正整数 a[i]。
输出格式
输出文件共有 T 行,对于每组数据,输出一行一个正整数,表示所有与 (n,a) 等价的货币系统 (m,b) 中,最小的 m。
数据范围
1≤n≤100,
1≤a[i]≤25000,
1≤T≤20输入样例:
2 4 3 19 10 6 5 11 29 13 19 17
输出样例:
2 5
分析:
很显然还是一个完全背包,这次我们可以给货币价值按升序排序,然后判断前面的是否可以表示后面的就OK了。
代码:
1 //完全背包,思路是,给他们排个序,如果前面能表示后边的,那就pass掉 2 #include <iostream> 3 #include <cstring> 4 #include <cstdio> 5 #include <algorithm> 6 7 using namespace std; 8 9 const int N = 30010; 10 11 int n; 12 int a[N]; 13 bool f[N]; 14 int cnt; 15 void work() 16 { 17 cnt = 0; 18 memset(f, 0, sizeof f); 19 cin >> n; 20 21 for (int i = 0; i < n; i ++ ) cin >> a[i]; 22 23 sort(a, a + n); 24 25 f[0] = 1; 26 for (int i = 0; i < n; i ++ ) 27 { 28 if (f[a[i]]) cnt ++; 29 for (int j = a[i]; j <= N; j ++ ) 30 f[j] |= f[j - a[i]]; 31 } 32 33 cout << n - cnt << endl; 34 } 35 36 int main() 37 { 38 int T; cin >> T; 39 while (T -- ) 40 { 41 work(); 42 } 43 44 return 0; 45 }