动态规划-背包问题

简单背包(01背包)

例:https://oj.czos.cn/p/1282

有n件物品,已知每件物品的价值v[i]和重量w[i],背包容量为c,要求从这n件物品中任取若干件装入背包内,使背包的物品价值最大。

未优化写法

设dp[i][j]表示有i件物品,在背包容量为j的情况下的最大价值度,则最终的答案为dp[n][c]

分析dp数组的状态转移方程:
对于第i件物品,背包容量为j

  1. 如果w[i]>j,则放不下,那么\(dp[i][j]=dp[i-1][j]\)
  2. 如果w[i]<=j, 放得下, 那么可以选,也可以不选,即\(dp[i][j]=max(dp[i-1][j],v[i]+dp[i-1][j-w[i]])\)
#include <bits/stdc++.h>
using namespace std;
int n,c,w[20010],v[110],dp[110][20010];
int main()
{
	scanf("%d %d",&c,&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d %d",&w[i],&v[i]);
	}
	//动态规划
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=c;j++)
		{
			if(w[i]>j) dp[i][j]=dp[i-1][j];
			else dp[i][j]=max(dp[i-1][j],v[i]+dp[i-1][j-w[i]]);
		}
	}
	//输出答案
	printf("%d",dp[n][c]);
	return 0;
}

优化写法-滚动数组

dp二维数组当中有很多空间是浪费的,因为第i行dp值的计算只和第i-1行有关,所以可以只用一个一维数组,实现滚动数组的方式来维护dp数组
方法:用一维dp数组,从后往前更新,这样dp[j]相当于原先的dp[i-1][j],而dp[j-w[i]]相当于原先的dp[i-1][j-w[i]]
注意,dp数组要从后往前更新,否则会覆盖原先数组的值,导致dp[i-1][j-w[i]]取到错误的值
此时状态转移方程变为:

\[dp[j]=max(dp[j],dp[j-w[i]]) \]

这一步调整之后,时间复杂度和空间复杂度都会有很大的提升

#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5+10;
int n,wi,vi,c;
int dp[maxn];
int main()
{
	scanf("%d %d",&c,&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d %d",&wi,&vi);
		//wi之前的相当于放不下的情况 可以不用算 直接继承自上一次的值
		for(int j=c;j>=wi;j--)
		{
			//进来之后相当于之前的放得下的情况 需要在选和不选当中取出最大值
			dp[j]=max(dp[j],vi+dp[j-wi]);
		}
	}
	//输出答案
	printf("%d",dp[c]);
	return 0;
}

完全背包

例:https://oj.czos.cn/p/1780
和01背包不同的是,每件物品有无限多个可以选择。
设dp[i][j]表示有i件物品,在背包容量为j的情况下的最大价值,那么很容易联想到完全背包的状态转移方程就是

\[dp[i][j]=max(dp[i-1][j],dp[i-1][j-k*w[i]]+k*v[i])(1<=k<=w/w[i]) \]

下面对这个方程进行进一步的推导 尝试去掉k:

经过推广可以得到等价的方程:

\[dp[i][j]=max(dp[i-1][j],max(dp[i-1][j-k*w[i]]+k*v[i]))(1<=k<=w/w[i]) ① \]

其中的k先拿一个出来,得到

\[dp[i][j]=max(dp[i-1][j],max(dp[i-1][(j-w[i])-k*w[i]]+k*v[i])+v[i])(1<k<=w/w[i]) ② \]

将①中k的范围扩大,可以得到③

\[dp[i][j]=max(dp[i-1][j-k*w[i]]+k*v[i])(0<=k<=w/w[i]) ③ \]

将③中的j替换为j-w[i]之后得到

\[dp[i][j-w[i]]=max(dp[i-1][(j-w[i])-k*w[i]]+k*v[i])(0<=k<=w/w[i]) ④ \]

将④带入②,可以得出完全背包问题的最终方程:

\[dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]) \]

与01背包的区别仅仅只是\(dp[i][j-w[i]]+v[i]\)\(dp[i-1][j-w[i]]+v[i]\)的区别

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5+10;
int c,n,vi,wi;
int dp[maxn];
int main()
{
	//dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i])
	scanf("%d %d",&c,&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d %d",&wi,&vi);
		//wi之前的不用计算 相当于之前的放不下的情况
		for(int j=wi;j<=c;j++)
		{
			//wi开始 相当于之前的放得下的情况 正序递推dp数组
			dp[j]=max(dp[j],dp[j-wi]+vi);
		}
	}
	printf("%d",dp[c]);
	return 0;
}

多重背包 https://oj.czos.cn/p/1888

和01背包不同的是,每件物品有Si件

#include <bits/stdc++.h>
using namespace std;
const int maxn = 110;
int n,c;
int w[maxn],v[maxn],s[maxn],dp[maxn];
int main()
{
	scanf("%d %d",&n,&c);
	for(int i=1;i<=n;i++)
	{
		scanf("%d %d %d",&w[i],&v[i],&s[i]);
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=s[i];j++)
		{
			for(int k=c;k>=w[i];k--)
			{
				dp[k]=max(dp[k],dp[k-w[i]]+v[i]);
			}
		}
	}
	printf("%d\n",dp[c]);
	return 0;
}

多重背包的二进制优化 https://oj.czos.cn/p/1889

二进制优化是一种压缩的思想
(1)有n个不同的物品,要讨论\(2^n\)种选择的可能(每个物品选或者不选):
(2)相同的物品有n件,虽然要讨论\(2^n\)种选择的可能,但由于n个物品是一样的,那么就减少了讨论数量,比如:有4个物品,如果是不同物品的选2个,选12、23是不同的选择,但如果是相同的物品,选哪两个就都是一样的了。因此,n个物品,要讨论的可能就分别是:选0个、选1个、选2个、选3个…选n个。
(3)要将0-n个不同的选择表达出来,比较简单的方法是将n二进制化。比如:整数7,只需要用124三个数任意组合,就能组合出0-7这8种可能。再比如:整数10,只需要用1243(注意最后一个数),就能组合出0-10这11种可能,这样n这个值就被二进制化了。因此如果要讨论10个一样的物品,就转化为讨论4个不同的物品了;而n个一样的物品,就转化为\(log_2n\)个不同的物品进行讨论。

#include <bits/stdc++.h>
using namespace std;
const int maxn = 11100;
int n,c,k,wi,vi,si;
int w[maxn],v[maxn],s[maxn],dp[maxn];
int main()
{
	scanf("%d %d",&n,&c);
	//读入数据 同时进行二进制压缩
	for(int i=1;i<=n;i++)
	{
		scanf("%d %d %d",&wi,&vi,&si);
		int t = 1;
		while(t<=si)
		{
			k++;
			w[k] = t*wi;
			v[k] = t*vi;
			si -= t;
			t *= 2; //注意这里要放在后面
		}
		if(si>0)
		{
			k++;
			w[k] = si*wi;
			v[k] = si*vi;
		}
	}
	//01背包
	for(int i=1;i<=k;i++)
	{
		for(int j=c;j>=w[i];j--)
		{
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
		}
	}
	printf("%d\n",dp[c]);
	return 0;
}

混合背包 https://oj.czos.cn/p/1905

二进制压缩后就只剩下01背包和完全背包了

#include <bits/stdc++.h>
using namespace std;
const int maxn = 10500;
int vi,wi,si,n,c,k;
int v[maxn],w[maxn],s[maxn],dp[maxn];
int main()
{
	scanf("%d %d",&n,&c);
	for(int i=1;i<=n;i++)
	{
		scanf("%d %d %d",&wi,&vi,&si);
		//多重背包转换为01背包
		if(si>0){
			int t = 1;//权重
			while(t<=si)
			{
				k++;
				v[k] = t*vi;
				w[k] = t*wi;
				s[k] = -1;
				si -= t;
				t *= 2;
			}
			if(si>0)
			{
				k++;
				v[k] = si*vi;
				w[k] = si*wi;
				s[k] = -1;
			}
		}else{
			//01背包和完全背包都是正常存储即可
			k++;
			w[k] = wi;
			v[k] = vi;
			s[k] = si;
		}
	}
	//剩下01背包和完全背包的dp
	for(int i=1;i<=k;i++)
	{
		//01背包
		if(s[i]==-1)
		{
			for(int j=c;j>=w[i];j--)
			{
				dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
			}
		}else{
			//完全背包
			for(int j=w[i];j<=c;j++)
			{
				dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
			}
		}
	}
	//输出答案
	printf("%d",dp[c]);
	return 0;
}
posted @ 2022-08-12 20:56  Zhe8468  阅读(149)  评论(0)    收藏  举报