动态规划初步
文章目录
动态规划认知
在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到。
01 背包
问题
有n件物品和一个容量为m的背包,放入第i件物品所需要的空间为wi,第i件物品的价值为vi,问背包可以放入物品的最大价值为多少?
- 此问题不可用贪心算法,贪心法得到的往往不是最优解。
- 直接暴力穷举n件物品,时间复杂度是O(2^n),效率过低,且存在大量重复运算。
算法
综上我们采用两重循环的方式来解决此问题,避免了复杂的递归,并且采用数组存储数据,使得计算次数大大减少,复杂度由O(2^n)优化到了O(V*W)。
for(int i=1; i<=n; i++){ //物品
for(int j=V; j>=0; j--){ //容量
if(j >= w[i]) //放得下物品,使用状态转移方程
dp[i][j] = max(dp[i-1][j-w[i]]+v[i], dp[i-1][j]);
else //放不下物品,直接跳过,沿承 i-1 个物品的价值
dp[i][j] = dp[i-1][j];
}
}
优化
空间优化
实际上,二维数组的空间复杂度过高,有时候也不现实,所以有时采用状态压缩,将二维数组压缩至一维。
压缩原理:实质上,i本身无实质性意义,在第 i+1 次循环尚未开始之前,第 i 行已经保存了当前的数据,且 0 到 i-1 行就没有使用价值了,故可以将二维数组压缩为一维数组,从后向前遍历,状态转移方程中的 [i-1] 就无存在价值了。
对于外层循环中的每一个 i 值,其实都是不需要记录的,在第 i-1 次循环已结束且第 i 次循环未开始,dp数组还未更新时,**dp[j]**还记录着前 i-1 个物品在容量为 j 时的最大价值,这样就相当于还记录着 dp[i-1][j]和dp[i-1][j-vol[i]]+val[i]。
状态转移方程修改为 dp[j] = max(dp[j], dp[j - w[i]] + v[i])
恰好装满
如果要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1…V]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。
如果没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0…V]全部设为0。 ——《背包九讲》
可以将其理解为不能把背包填满的解都是非法(价值小于零)的,从而在计算中排除这些解。或者说当前的合法解一定是从之前的合法状态推得的。
这个技巧可以推广到其他背包问题,不仅仅限于01背包。
常数优化
经过图表可知,并不需要将表中的数据全部算出,只需要算一部分即可。因为最后所求结果为dp[n][v],其在 n-1 行所需的最小的 j 为 v-w[n-1],同理,对于第 i 行, j 最小循环至 v- sum{w[n…i-1]} ,综上所述我们可以优化第二层的循环次数,代码如下:
for(int i=1;i<=n;i++)
bound=max{V-sum{w[i..n]},w[i]};
for(int j=V;j>=bound;j--)
因为若是 V-sum{w[i…n]} < w[i] 的话,则无法取到第 **i **个物品了,所以就把下限定为 w[i],进一步减少第二层循环次数,这种方法在V很大的情况下会节省时间。
完全背包
问题
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
思考方向还是先从二维数组下手,得到转移方程,再向一维数组优化
算法
此处采用和01背包相似的思考方式,01背包每个物品都拿一次,所以对于同一个 i 下的 j 都应该是独立的,即每个 j 中至多包含一个当前物品,所以由后向前遍历,但是01背包中,每个 i 不止用一次,可能用多次,所以偏大的 j 中的情况就不应从i-1 那一行转移,而应该是从同一行中转移,这样才能保证可以出现多个 i
for(int i = 1; i <= n; i++) //正序
for(int j = 0; j <= V; j++){ //正序
if(j >= w[i]){ //放得下
dp[i][j] = max{dp[i-1][j] , dp[i][j-w[i]] + v[i];
} else{ //放不下
dp[i][j] = dp[i-1][j];
}
}
思维陷阱
背包问题其实有一个隐藏的条件,就是包的体积和物品的质量其实都是整数,虽说是无限多的物品,但是实质上还是枚举出来了,在 i 行的遍历一定能枚举所有含 i 情况。
完全背包每次做出选择是取决于当前这一步的,而01背包每次做出选择是取决于上一步的。主要就是因为当前这一步一个物品放过以后,表格逐渐向右填写,随着可放空间的增加,可以判断这一步是否还可以再放一个当前的物品。
多重背包
题目
有N种物品和一个容量为V的背包。第i种物品最多有num[i]件可用,每件费用是w[i],价值是c[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
思路
此题显然有朴素算法,即将所有的物品一个一个地进行验证,也就是转化成01背包,但是这样的复杂度达到了O(V*Σnum[i])
,复杂度很高.
我们考虑一下,发现对于任意一个num[i],我们都可以按照二进制拆成ceil(log(num[i]))
个物品,每个新物品分别代表着 1,2,4...2^k,num[i]-2^k
个物品,同理价值和重量也是按比例增加的,然后采用01背包处理,这样就把复杂度降到了 O(V*Σ(log num[i]))
,有了很大的改善.(看博客说可以用单调队列优化到O(V*N))
,此点后续补充
然后我们还发现了一点优化空间,如果背包无法装下某物品的总量,那么此物品相对于背包就是无限的,此处可以用完全背包来解决.
代码
综上所述,我们有如下的算法
int n, V; // n为物品个数,V为背包总体积
void ZeroOnePack(int weight, int value) { // 01背包
for (int i = V; i >= weight; i--) {
dp[i] = max(dp[i], dp[i - weight] + value);
}
}
void CompletePack(int weight, int value) { // 完全背包
for (int i = weight; i <= V; ++i) {
dp[i] = max(dp[i], dp[i - weight] + value);
}
}
void MultiplePack(int weight, int value, int number) { // 多重背包
if (number * weight >= V) { // 对于总容量来说此物品是无限的,所以采用完全背包处理
CompletePack(weight, value);
} else { //否则需要特殊处理
// 分解成01背包问题(采用二进制优化背包个数到o(求和(lgC[i]))
for (int k = 1; k <= number; k <<= 1) { // 因为是二进制,所以起始位置一定是1,终止位置一定是number
ZeroOnePack(k * weight, k * value);
number -= k;
}
ZeroOnePack(number * weight, number * value); // 将为二进制分完之后的部分01背包处理
}
}
混合三种背包
题目
给出n个物品,有的是只有一个,有的是无限的,有的是有限个,求最大价值.
思路
根据从多重背包最后的代码中得到的启示,我们完全可以将三个背包函数全部封装好,然后直接调用即可.这里也体现了封装的重要性.
代码
for i=1..N
if 第i件物品属于01背包
ZeroOnePack(c[i],w[i])
else if 第i件物品属于完全背包
CompletePack(c[i],w[i])
else if 第i件物品属于多重背包
MultiplePack(c[i],w[i],n[i])
二维费用背包问题
题目
对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[i]和b[i]。两种代价可付出的最大值(两种背包容量)分别为V和U。物品的价值为w[i]。
思路
费用只加了一维,自然dp也要增加一维
然后在转移时注意维护两个费用即可,至于循环方向和之前的相同
dp[i][v][u]=max{dp[i-1][v][u],dp[i-1][v-a[i]][u-b[i]]+w[i]}
当然,此处表示个数的第一维也是可以省略的.
总个数的限制
有的背包问题除了对体积有限制之外还对总共的个数有限制,我们发现这就是加了一个限制条件,也就是加了一维费用,每个物品都要占据’一个位子’,也就是上文中的b[i]恒等于1.
分组背包
问题
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
思路
背包九讲中给出了这样的算法
这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设
f[k][v]
表示前k组物品花费费用v能取得的最大权值,则有:f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于组k}
也就是对于同一个v,我们必须完成当前组里面所有的物品的加入
for 所有的组k
for v=V..0
for 所有的i属于组k
f[v]=max{f[v],f[v-c[i]]+w[i]}
切记二三层循环顺序不能反过来,如果反过来就等价于我们对所有的物品(无论他们是不是在一组)进行了01背包.
尾声
后续的 "依赖背包"和"泛化背包"目前还没有好的理解方式,日后再做研究
to be continue