我们介绍过经典动态规划后,再来介绍下线性DP。
1.分组背包
概念:有一些物品,把物品分为n组,其中第i组第k个书品的体积为c[i][k],价值为w[i][k];每组内的物品冲突,每组内最多只能选出一个物品装入被背包;给定体积为C的背包,问如何选择物品,使撞击背包的物品的总价值最大。
例题(hdu1712)
题目叙述:小逸这学期选N门课程,但他只想学M天。每门课的学分不同,问这M天如何安排N门课,才能拿到最多学分。
输入:有多个测试,每个测试的第一行输入N和M,后面有N行,每行输入M个数字,表示一个矩阵A[i][j],1<=i<=N<=100,1<=j<=M<=100,表示第i门课学j天能得到A[i][j]学分,若N=M=0,表示测试结束。
输出:对每个测试,输出最多学分。
思路:
这个问题可以类似于我们的背包问题。我们可以将这里的天数M类似于我们的背包体积C;将这N门课程类似于我们的n类物品,只不过现在我们的每组中又有了m个物品,需要我们进行选择,装还是不装,每组中最多选择一个,其实思路和之前介绍的背包问题相似度极高,这里可以先自己思考以下。
这里我们就用“自我滚动”来解决这个问题。
#include <bits/stdc++.h>
using namespace std;
const int N = 105;
int w[N][N], c[N][N];
int dp[N];
int n, m;
int main() {
while (scanf("%d %d", &n, &m) && n && m) {
for (int i = 1; i <= n; i++) {
for (int k = 1; k <= m; k++) { //m也是第i组的物品个数
cin >> w[i][k]; //输入第i组第k个物品的价值
c[i][k] = k; //第i组第k个物品的体积,学k天才能得分,那么它的体积也就是k
}
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) { //遍历n个组
for (int j = m; j >= 0; j--) { //容量为m
for (int k = 1; k <= m; k++) { //遍历k个物品第i组的所有物品
if (j >= c[i][k]) { //状态转移方程
dp[j] = max(dp[j], dp[j - c[i][k]] + w[i][k]);
}
}
}
}
cout << dp[m] << endl;
}
}
return 0;
}
2.多重背包
概念
给定n种物品和一个背包,第 i 种物品的体积为ci,价值为wi,并且有m个,背包的总容量为C。如何选择装入背包中的物品,使装入背包中的物品的总价值最大。
例题
P1776 宝物筛选 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
解法一:简单方法
将相同的mi个第i种物品单独看成mi个物品,然后按我们最先学习的经典背包问题解决。
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int n,C,dp[N];
int w[N],c[N],m[N];
int main(){
cin>>n>>C;//物品数量,背包体积
for(int i=1;i<=n;i++) cin>>w[i]>>c[i]>>m[i];
for(int i=1;i<=n;i++)//枚举物品
for(int j=C;j>=c[i];j--)//枚举背包体积
for(int k=1;k<=m[i]&&k*c[i]<=j;j++)//看看这种物品装几个合适
dp[j]=max(dp[j],dp[j-k*c[i]]+k*w[i]);
cout<<dp[C]<<enl;
return 0;
}
但是我们使用这种方法,提交后会超时。下面我们来介绍一种比较精妙的优化方法,这种方法可以运用到很多类似的情况中,帮助我们降低时间复杂度,它就是——二进制拆分优化。
解法二:二进制拆分优化
思路:
这是种简单而有效的技巧。在上述简单方法的基础上加人这个优化,能显著改善复杂度。理解起来很简单,例如。第 i 种物品有25个,这25不物品放进背包的组合含有26种情况,即0~25个物品放入背包。不过,要组合成26种情况,其实井不需要25个物品。根据二进制的计算原理,任何一个十进制整数X都可以用1.2.4.8等2的倍数相加得到,如25= 16+8+1。这些2的倍数只有log2X个。题目中第i种物品有m个,用log2m的个数就能组合出0~m种情况。从而将时间复杂度降到对数级别。
注意拆分的具体实现,不能全部拆成2的倍数,而是先按2的倍数从小到大拆,最后是一个小于或等于最大倍数的余数。对于mi这样拆分非常有必要,能够保证拆出的数相加在[1,m;]范围内,不会大于mi。例如,mi=25,把它拆成1+2+4+8+10,最后是余数10,10<16=24,读者可以验证用这5个数能组合成1~25的所有数字,不会超过25。
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int n,C,dp[N];
int w[N],c[N],m[N];
int new_n;//二进制拆分后的新物品总数量
int new_w[N],new_c[N],new_m[N];//二进制拆分后的新物品数据
int main(){
cin>>n>>C;
for(int i=1;i<=n;i++) cin>>w[i]>>c[i]>>m[i];
//以下是二进制拆分
int new_n=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=m[i];j<<=1){//二进制枚举:1 2 4 8
m[i]-=j;//减去已经拆分的
new_c[++new_n]=j*c[i];
new_w[new_n]=j*w[i];
}
if(m[i]){//看看有没有余数
new_c[++new_n]=m[i]*c[i];
new_w[new_n]=m[i]*w[i];
}
}
//以下就是我们熟悉的DP了
for(int i=1;i<=new_n;i++)//枚举物品
for(int j=C;j>=new_c[i];j--)//枚举背包容量
dp[j]=max(dp[j],dp[j-new_c[i]]+new_w[i]);
cout<<dp[C]<<endl;
return 0;
}
线性DP还可以解决许多问题,我们下节继续介绍。