第一次使用
01 背包 朴素 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <iostream> #include <algorithm> #define MAXN 1010 using namespace std;int n, m;int v[MAXN], w[MAXN];int f[MAXN][MAXN];int main () { ios::sync_with_stdio (false ); cin.tie (0 ); cout.tie (0 ); cin >> n >> m; for (int i = 1 ; i <= n; i ++) { cin >> v[i] >> w[i]; } for (int i = 1 ; i <= n; i ++) { for (int j = 1 ; j <= m; j ++) { f[i][j] = f[i - 1 ][j]; if (j >= v[i]) f[i][j] = max (f[i][j],f[i-1 ][j-v[i]] + w[i]); } } cout << f[n][m]; return 0 ; } 复制
一维优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <iostream> #define MAXN 1010 using namespace std;int n,m;int v[MAXN],w[MAXN];int f[MAXN];int main () { cin >> n >> m; for (int i = 1 ; i <= n; i ++) { cin >> v[i] >> w[i]; } for (int i = 1 ; i <= n; i ++) { for (int j = m; j >= v[i]; j --) { f[j] = max ( f[j], f[j - v[i]] + w[i] ); } } cout << f[m] << endl; return 0 ; } 复制
完全背包 朴素做法 即 3 维做法
先考虑状态,容易想到选到某个位置后,其特征有空间,物品种类和价值。价值是要求的,所以我们可以开二维状态,分别表示空间和物品种类。即 表示从前 种物品里选,占了 单位空间的最大收益。 参考 lzp 大佬博客中的要求,这个状态是否能保证唯一且可递推呢?
唯一:选物品的范围确定,所占用的空间确定,在所有方案中存储最大收益的方案。由于空间确定,因此不会影响后面决策,此时一定能保证最优且唯一。 可递推:前面最优,容易想到通过枚举新拿的物品数量求得当前最优方案(后面讲) 接下来讲递推过程: 前面 0/1 背包我们只需要考虑一个物品是否选 ,但是现在还需要考虑选多少 。 考虑是否选择时,我们直接枚举当前物品的状态,即 0/1,选或不选。 放到现在这个题目中,把刚才 0/1 背包枚举选或不选 的过程看作枚举物品数量 ,即选 个或选 个,这启发我们可以通过枚举物品数量来解决选多少 的问题。 状态转移方程就自然而然地出来了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int backpack () { #define MAXN 1010 using namespace std; int n,m; int v[MAXN],w[MAXN]; int f[MAXN][MAXN]; cin >> n >> m; for (int i = 1 ; i <= n; i ++) { cin >> v[i] >> w[i]; } for (int i = 1 ; i <= n; i ++) for (int j = 0 ; j <= m; j ++) { for (int k = 0 ; k * v[i] <= j; k ++) f[i][j] = max (f[i][j], f[i - 1 ][j - v[i] * k] + w[i] * k); } cout << f[n][m] << endl; } 复制
2 维优化 上面的转移有三维!太慢了!想办法如何减去一个维度。
状态方面,没有办法再降了,因为去掉任意一维都会导致无法表示唯一的状态。 那就考虑优化递推的过程。想一想是否有哪些状态能够转移到当前状态但是没考虑到?答案是有的。 考虑递推过程的第二层、第三层循环,对于每一个体积 , 都需要从 枚举到 ,形象点来说大概长这样
1 2 3 4 xxx xxx xxx xxx xxx xxx xxx xxx xxx xxx
显然, 时都重复地枚举了待转移状态,想办法把这些冗余的东西去掉——直接从算过 的状态转移过来。 这意味着我们不能再单纯的用物品数量进行递推。观察第一行到第二行,容易发现在满体积的时候,第二行只是比第一行多了一个物品 而已。 讨论单个物品是否选择,于是新的递推方式就显而易见了,设当前状态为 ,我既可以选一个物品 ,也可以不选一个物品 ,从 转移过来。 状态转移方程:
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #define MAXN 1010 using namespace std;int n,m;int v[MAXN],w[MAXN];int f[MAXN][MAXN];cin >> n >> m; for (int i = 1 ; i <= n; i ++) { cin >> v[i] >> w[i]; } for (int i = 1 ; i <= n; i ++) for (int j = 0 ; j <= m; j ++) { f[i][j] = f[i - 1 ][j]; if (j >= v[i]) f[i][j] = max (f[i][j], f[i][j-v[i]] + w[i]); } cout << f[n][m] << endl; 复制
滚动数组优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #define MAXN 1010 using namespace std;int n,m;int v[MAXN],w[MAXN];int f[MAXN][MAXN];int backpack () { cin >> n >> m; for (int i = 1 ; i <= n; i ++) { cin >> v[i] >> w[i]; } for (int i = 1 ; i <= n; i ++) for (int j = v[i]; j <= m; j ++) { f[j] = max (f[j], f[j-v[i]] + w[i]); } cout << f[m] << endl; } 复制
多重背包 朴素 种物品,背包容量为 ,第 件物品最多能选 个,每件物品占空间 ,价值为
第一种容易想到的解法是把完全背包的方法拿来用,因为多重背包只是加了 的限制。 状态转移方程:
考虑多重背包和完全背包的区别可以发现,多重背包虽然每种物品不只一种,但是有确定的数量,而完全背包没有数量限制,则可以轻松地在忽略时间复杂度的情况下把多重背包拆分 成 01 背包,即把 件第 件物品挨个算单个物品再按照 01 背包的方式处理。
1 2 3 4 5 6 7 for (int i = 1 ; i <= n; ++ i) { for (int j = 0 ; j <= m; ++ j) { for (int k = 0 ; k <= min (c[i],j/w[i]); ++ k) { f[i][j] = max (f[i][j], f[i-1 ][j-k*v[i]] + k*w[i]); } } } 复制
二进制优化 这种暴力方式求解比较慢,容易发现,在求解过程中,每个同种 物品是按照不同种 物品进行处理的,换句话说,我选第一个 a 物品与选第二个 a 物品,实际上效果都是选 1 个 a 物品,但是这样的情况在上文解法中做了两次决策,显然有一种是多余的。 也就是说,接下来的优化方向是减少 每次决策所遇到的同数量同种 物品,那会联想到什么呢? 想办法把物品分为多个组,使这些组的不同组合能表示出所有数量,于是想到进制,这里采用二进制拆分。
易证,使用 能够表示出 到 之间的所有的数,即二进制计数。也就是说,如果把物品的数量 拆分成 ,由 的范围可得,其中 能够表示出 的所有整数,然后也能轻松得出 选出若干与 相加能表示出 之间所有整数,两个合并即是
因此可以将 拆分为 种新的物品,每种新物品的体积和价值分别为:
拆分完再进行 01 背包即可。
代码来自 GrainRain 's Blog
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 const int N = 25000 ;const int M = 2010 ;int n, m;int v[N], w[N];int f[N];cin >> n >> m; int cnt = 0 ;for (int i = 1 ; i <= n; i ++) { int a, b, s; cin >> a >> b >> s; for (int k = 1 ; k <= s; k *= 2 ) { s -= k; v[++ cnt] = a * k; w[cnt] = b * k; } if (s > 0 ) { v[++ cnt] = a * s; w[cnt] = b * s; } } n = cnt; for (int i = 1 ; i <= n; i ++) for (int j = m; j >= v[i]; j --) f[j] = max (f[j], f[j - v[i]] + w[i]); cout << f[m] << endl; 复制
分组背包 组物品,第 组有 个物品,第 组的第 个物品的价值是 ,体积为 ,背包体积为 ,每组物品只能选择 个
直接枚举组时挨个枚举物品就行了。 按照组划分阶段,第 组不选则 ,第 组选第 个则
这里直接给出数组删维优化过的代码
1 2 3 4 5 6 7 for (int i = 1 ; i <= n; ++ i) { for (int j = m; j >= m; -- j) { for (int k = 1 ; k <= c[i]; ++ k) { f[i][j] = max (f[j], f[j-v[i][k]] + w[i][k]); } } } 复制
依赖背包 我的 Windows 盘还剩 5 个 G,新买的东方冰之勇者记占 2G,LLVM 依赖于 MSVC 生成工具,MSVC 占用 2G 磁盘,LLVM 占用 2G 磁盘,问我应该怎么安装收益最大reboot to archlinux!
A 物品依赖于 B,选 B 物品之前需要先选 A 物品,问最大价值
容易发现对于任意有依赖关系的物品 A B 来说,一共有以下三种可能的决策 (A 依赖 B)
A B 都不选 选 B 不选 A A B 都选 因为这三种情况只会出现一个,联想分组背包的特征,将这三种情况每个看成一个组里的物品,即转化为了分组背包。
当成树形 DP 好做一点,加到 TODO 里面。