背包 DP 详解

背包DP

背包 DP,说白了就是往一个背包里扔东西,求最后的最大价值是多少,一般分为了三种:01 背包、完全背包和多重背包。而 01 背包则是一切的基础。

01 背包

特点:每个物品只有一个,只能选择装这个物品或者不装。

这时我们一般设一个 dp[i][j] 表示前 i i i 个物品再总体积为 j j j 的情况下装的最大价值是多少。所以 01 背包的时间复杂度为 O ( n 2 ) O(n^2) O(n2),这时我们可以得到这样一个状态转移方程:

d p i , j = max ⁡ { d p i − 1 , j , d p i − 1 , j − v i + w i } dp_{i,j}=\max\{dp_{i-1,j},dp_{i-1,j-v_i}+w_i\} dpi,j=max{dpi1,j,dpi1,jvi+wi}

这里的 v i v_i vi 指当前物品所占的体积, w i w_i wi 指当前物品的价值。

那这是怎么一回事呢?我们可以这样想:我要么不选这件物品,那么总价值就和 d p i − 1 , j dp_{i-1,j} dpi1,j 一样(即同样的体积在前 i − 1 i-1 i1 件物品中的最大价值),如果选了,那就和前 i − 1 i-1 i1 件物品在体积为 j − v i j-v_i jvi 的情况下的最大价值再加上当前物品的价值 w i w_i wi 一样,那再取其中的最大值就行了。

但是,我们会发现一个问题:如果你的总体积为 V V V,总物品数为 n n n,那你的空间复杂度就是 O ( V n ) O(Vn) O(Vn),这可是一个极其庞大的数字,这是我们就要请上我们的滚动背包!

滚动背包旨在通过观察状态转移方程看看哪些空间没用从而可以省掉。比如说上面的状态转移方程,我们会发现 DP 的第一维只与当前状态与上一状态有关,而与其他的无关,所以其它空间就是被浪费了的,所以我们只需要把第一维开个 2 2 2,而不用开 n n n,这样,空间复杂度就被压到了 O ( 2 V ) O(2V) O(2V),状态转移方程也就变成了这样:

d p i   m o d   2 , j = max ⁡ { d p ( i − 1 )   m o d   2 , j , d p ( i − 1 )   m o d   2 , j − v i + w i } dp_{i\bmod2,j}=\max\{dp_{(i-1)\bmod2,j},dp_{(i-1)\bmod2,j-v_i}+w_i\} dpimod2,j=max{dp(i1)mod2,j,dp(i1)mod2,jvi+wi}

上面这种方法适用于初学者,因为它清楚直观、浅显易懂。

让我们先来看一道例题

N N N 件物品和一个容量是 V V V 的背包。每件物品只能使用一次。

i i i 件物品的体积是 v i v_i vi,价值是 w i w_i wi

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出最大价值。

很经典的一道 01 背包问题,直接用上面的公式就对了。

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,vv,v[1006],w[1006],dp[2][1006]; 
signed main()
{
	cin>>n>>vv;
	for(int i=1;i<=n;i++)
	{
		cin>>v[i]>>w[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<=vv;j++)
		{
			dp[i%2][j]=dp[(i-1)%2][j];
			if(j>=v[i])
			{
				dp[i%2][j]=max(dp[(i-1)%2][j],dp[(i-1)%2][j-v[i]]+w[i]);
			}
		}
	}
	cout<<dp[n%2][vv];
	return 0;
}

当然,我们也可以在这个的基础上优化成一维滚动背包,但是中间的循环需要颠倒过来一下,具体我们可以这么想:我们上面的状态转移方程再更新时原本的和现在的状态是不会受影响的,而如果我们正着循环,可能就会出现这种情况:

按照我们之前的状态转移方程,我们应该拿原本的旧的状态来更新新的状态,但现在我们却拿我们计算好了的新的状态来更新更新的状态,这是完全不符合的,而倒过来循环正好就能避免这种事情的发生。(想通了的同学继续看,没想通的同学抠破脑袋想。)

所以上面的代码还可以写成这样:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,vv,v[1006],w[1006],dp[1006]; 
int main()
{
	cin>>n>>vv;
	for(int i=1;i<=n;i++)
	{
		cin>>v[i]>>w[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=vv;j>=v[i];j--)
		{
			dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
		}
	}
	cout<<dp[vv];
	return 0;
}

完全背包

特点:每个物体有无穷多个,可以无限取同一个物品。

这时的改动很小,只需要把上面 01 背包优化成一维时的循环改成正着循环就行了。这又是为啥呢?

我们前面不是说了吗:正着循环会把更新好的状态拿去更新当前状态。如果是 01 背包每种只能用一次,那肯定不行。但是完全背包可以多次放一个物品啊,当前的最新状态对于另一个状态来讲可能只是一个旧状态罢了。

所以,我们把上面的代码稍作改动:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,vv,v[1006],w[1006],dp[1006]; 
int main()
{
	cin>>n>>vv;
	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<=vv;j++)
		{
			dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
		}
	}
	cout<<dp[vv];
	return 0;
}

就水灵灵的 A C \color{green}{AC} AC 了!

(讲的稍微有点快,没看懂的同学可以回去再看。)

多重背包

特点:每件物品有一定数量,所装的物品数量不能超过这个值。

还是拿一道例题讲一下:

N N N 种物品和一个容量是 V V V 的背包。第 i i i 种物品最多有 s i s_i si 件,每件体积是 v i v_i vi,价值是 w i w_i wi
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

外面两层循环跟 01 背包一模一样,但是因为有数量限制,里面需要多加一层循环:

for(int k=0;k<=s[i]&&v[i]*k<=j;k++)

这层循环就是循环你要拿或者是不拿 k k k 个物品。

然后我们写状态转移方程:

d p i , j = max ⁡ { d p i − 1 , j , d p i − 1 , j − k × v i + k × w i } dp_{i,j}=\max\{dp_{i-1,j},dp_{i-1,j-k\times v_i}+k\times w_i\} dpi,j=max{dpi1,j,dpi1,jk×vi+k×wi}

再根据与 01 背包一样的套路优化一下即可。

代码如下:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,vv,v[106],w[106],s[106],dp[106]; 
signed main()
{
	cin>>n>>vv;
	for(int i=1;i<=n;i++)
	{
		cin>>v[i]>>w[i]>>s[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=vv;j>=v[i];j--)
		{
			for(int k=0;k<=s[i]&&v[i]*k<=j;k++)
			{
				dp[j]=max(dp[j],dp[j-v[i]*k]+w[i]*k);
			}
		}
	}
	cout<<dp[vv];
	return 0;
}

二进制优化

但是(一般一切顺利的时候搜会有个“但是”),我们稍加计算发现它的时间复杂度达到了恐怖的 O ( V × ∑ s i ) O(V\times\sum s_i) O(V×si),这个时间复杂度是极高的,为了降低时间复杂度,我们采取了一种优化:转为 01 背包问题。

因为我们有至多 s i s_i si 个,那么我们可以选择的个数有哪些呢?这时,聪明的计算机学家们就想到了一种办法:转二进制

这个转二进制有什么好处呢?好处可大了去了。因为我们知道任何一个数倍拆成二进制后是可以拼凑出再 s i s_i si 以内的所有数的,而对于当前这个数加不加上又是一个 01 背包问题。

也就是这样:假设 s i = 7 s_i=7 si=7,那么 7 7 7 写成二进制就是 111 111 111,拆成二的幂次相加就是 2 2 + 2 1 + 2 0 2^2+2^1+2^0 22+21+20,而对于每一个数选不选进来凑成一个小于 s i s_i si 的数是个 01 背包问题。于是我们就将多重背包转化成了 01 背包。而对于每一个数再乘 v i v_i vi 表示新的体积,乘 w i w_i wi 表示新的质量,然后做 01 背包就行。

这样我们就将时间复杂度降到了 O ( V × ∑ log ⁡ 2 ( s i ) ) O(V\times\sum\log_2(s_i)) O(V×log2(si)),大大优化了啊!

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
struct binary{
	int v,w;
};
int n,vv,v[1006],w[1006],s[1006],dp[2006]; 
vector<binary>v1;
signed main()
{
	cin>>n>>vv;
	for(int i=1;i<=n;i++)
	{
		cin>>v[i]>>w[i]>>s[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=s[i];j*=2)
		{
			s[i]-=j;
			v1.push_back({v[i]*j,w[i]*j});
		}
		if(s[i]>=0)
		{
			v1.push_back({v[i]*s[i],w[i]*s[i]});
		}
	}
	for(auto i:v1)
	{
		for(int j=vv;j>=i.v;j--)
		{
			dp[j]=max(dp[j],dp[j-i.v]+i.w);
		}
	}
	cout<<dp[vv];
	return 0;
}

单调队列优化

注:一般情况下只会卡到二进制优化,所以单调队列优化看不懂的同学可以直接跳过。

二进制优化已经很好了,但还不是最好,真正好的直接优化到了 O ( N × V ) O(N\times V) O(N×V),让我们有请单调队列优化

~~由于本作者实在是太菜了,竟然没看懂单调队列优化,~~感兴趣的同学可以参考这篇文章

小结

这篇文章带你梳理了三种基本背包:01 背包、完全背包与多重背包,至于更多的背包模型请大家搜索背包九讲,那里有更多的大佬在等着你们。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值