线性DP问题

        我们介绍过经典动态规划后,再来介绍下线性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还可以解决许多问题,我们下节继续介绍。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值