背包DP 呀

背包DP 呀

本笔记按照蓝书顺序进行,方便以后直接查找题目

背包总结

花了两天的时间复习完了蓝书上背包,简略的说一下学到懂东西。

  • 背包变形式:填数的方案总和,二者是一样的,因此在遇到填数的问题 时,可以往背包上去想
  • 一些看起来很像背包的题目,要注意数据范围,动态的改变DP的维度。
  • DP的路径输出,逆推法。
  • 复杂问题转换成判定性问题对于统计个数,找到最值等等的DP,我们可以在了解值域的情况下,直接把答案放入DP维度,通过判断是否存在来找到所需答案。

虽然不能说对背包十分熟练,起码学到了不少其他的东西,还复习了单调队列。

总的来说,想必未来背包将是我解决填数的问题的主要思路。

01背包

滚动数组优化,倒序循环,都是基本知识,不说了,看题。

  1. 数字组合
  • 给出 N N N 个数,选出一些数,使得和为 M M M ,求方案数
  • N ≤ 100 , M ≤ 10000 , A i ≤ 100 N\le 100,M\le 10000,A_i\le 100 N100,M10000,Ai100

思路

特点:背包是允许不满的,但是此题要求必须装满,求解方案数,不是求解最值。

实际上,DP方程基本和背包一样:
f [ i ] [ j ] = f [ i − 1 ] [ j ] + f [ i − 1 ] [ j − a [ i ] ] f[i][j]=f[i-1][j]+f[i-1][j-a[i]] f[i][j]=f[i1][j]+f[i1][ja[i]]
一样滚动数组,倒序循环即可。

我们来探究一下,为什么方程一样但是一个是装满一个是不装满。

因为就在于:装满的求的是方案数,当一个物品装入后不能使当前背包变满,那么就不会得到转移,而对于求最值,即使当前背包不是变满,可是当前物品可以对答案做出贡献,重点在于方案数和最值

这也是一个选数问题:标记一下,以后总结一下做到的选数问题,选数问题真是令人头疼,真正遇到的时候,就连暴力都不知道怎么写。

非满背包的最值求=包满背包方案数求解

int T;
int n,m;
int a[B];
int f[B];
void work()
{
	cin>>n>>m;
	for (int i=1;i<=n;i++) cin>>a[i];
	f[0]=1;
	for (int i=1;i<=n;i++)
	{
		for (int j=m;j>=a[i];j--)
		{
			f[j]=f[j]+f[j-a[i]];//和背包不一样, 背包求解最值,此时即使背包不满,但是同样会有贡献,满足最优子结构
			//但是方案数就不一样,方案数只有完全契合才能地推 
		}
	} 
	cout<<f[m]; 
}

完全背包

把二维写出来,就明白滚动数组优化后的为什么这么写了。

  1. 自然数拆分
  • 给定一个自然数 N N N ,要求把 N N N 拆分成若干正整数相加的形式,参与加法运算的数可以重复。
  • 注意,拆成方案不考虑顺序,至少拆成两个数的和
  • m o d    2147483648 \mod 2147483648 mod2147483648
  • N ≤ 4 × 1 0 3 N\le 4\times 10^3 N4×103

思路

一开始读题目的时候感觉没有思路,要不是看到重复,在自然状态下,没有完全背包的加持,我是绝对不会忘完全背包上去想的。变换题目,看到 N N N 的数据,稍微更改一下题目就很显然的做了。

  • 1 − N 1-N 1N 中选数,一个数可以选多次,使得相加之和为 N N N

突然发现这和第一题的问法好像——给出 N N N 个数,求选择相加之和为 M M M 的方案数

所以这是一种更恶心的问法——将 N N N 通过一些正整数进行拆分,求方案数。

听到拆分好像更难些,反过来其实就是选择一些数字合成

我草,做题时要尝尝转换题目,比如反过来题目是不是更好理解

将题目转换之后,其实就可以看到,这是完全背包的满背包方案数,很容易想出来
f [ j ] = f [ j ] + f [ j − i ] f[j]=f[j]+f[j-i] f[j]=f[j]+f[ji]
但是不是倒序循环,而是正序循环。

所以基本上,背包的方案数求解基本运用到选数,更恶心点是数字拆分…

int read(){int x;scanf("%lld",&x);return x;}
const int B=1e6+10;
const int inf=0x3f3f3f3f;
const int mod=2147483648; 
int T;
int n;
int f[4009];
void work()
{
	cin>>n;
	f[0]=1; 
	for (int i=1;i<=n;i++)
	{
		for (int j=i;j<=n;j++)
		{
			f[j]=(f[j]+f[j-i])%mod;
		}
	}
	cout<<(f[n]-1)%mod;
}

我有一个思考:

  • 如果把背包体积改成乘法,哪咋做,很显然的,除法转移貌似不行,好像曾经做过,不过忘了。
  1. 陪审团
  • ACWing280 题目还很长,不写了

做题时思路:

列一下自己做题时出现的难点:

  • 路径计算,DP路径统计一直都是我很头疼的一件事,我的路径计算还停留在用一个新数组去记录,并且不知道是否正确。

  • 绝对值,一开始我把公式化简成了
    ∣ ∑ D i − P i ∣ |\sum{D_i-P_i}| DiPi
    然后就按照每次 ∣ D i − P i ∣ |D_i-P_i| DiPi 去统计了,结果发现很明显是错误的,当时就炸了,这可没办法跑DP,因此状态与状态之间不可以转移,每个状态的答案取决于当前差的和的绝对值大小,因为绝对值,最小值,最大值维护都是错误的,当时就没有办法解决绝对值,因此也就不做了

  • 还有这个题有个特点,就是先求 min ⁡ { ∣ ∑ D i − P i ∣ } \min\{|\sum{D_i-P_i}|\} min{DiPi} ,再求 max ⁡ { D i + P i } \max\{D_i+P_i\} max{Di+Pi} ,DP中的DP,大概率不是的。

题解

  • 路径计算学到了倒推的新方法,确实厉害
  • 题解直接把所求的差值直接放到维度上,这样就不需要求最小的了,把所有的都求出来,然后从0开始,只要有值,就说明是最小的,把复杂问题变成判定性问题,发现 D i , P i D_i,P_i Di,Pi 的值都特别小,然后 N N N 也特别小,所以直接舍弃绝对值,直接在维度上记录 [ − 400 , 400 ] [-400,400] [400,400] 的最大 D i + P i D_i+P_i Di+Pi 确实秒
  • 之所以放在背包后面,原因是因为题目中也包含了选和不选的DP思考方式,和01背包相似,以此看做是01背包的变形,可以以此来当出题人恶心其他人…

看了 y总的视频,写代码真快,确实帅,思路清晰,每行代码都是一句话。

总结——学到了什么

  • 分析题目不能直接就上算法,应该先想清楚这个题目的做题顺序然后在考虑,我出现的错误就是上来就DP,导致看成了DP中DP
  • 在数据很小的情况,我们可以尝试将所求值放在DP维度上,通过判断是否存在DP值来判断是否存在当前最大值!!!好重要,将复杂问题转换成判定性问题。
  • DP路径问题,y总有句话说的好:DP路径问题反过来就是从终点倒推到起点。我们只需要找到是从哪里转移的就可以复刻路径,这样就解决了所有DP的转移问题,妙啊~
  • 多组数据读入方法 while (scanf("%d%d",&n,&m), n||m) 读到 0 0 0 结束。
#include<bits/stdc++.h>
using namespace std;
int read(){int x;scanf("%d",&x);return x;}
const int B=1e6+10;
const int inf=0x3f3f3f3f;
int T;
int f[211][21][810];
int base=400;
int n,m;
int p[B];
int d[B]; 
int ans[B];
int num;
void work()
{	
	while (scanf("%d%d",&n,&m), n||m)
	{
		for (int i=1;i<=n;i++) p[i]=read(),d[i]=read();
		memset(f,-0x3f,sizeof(f));
		f[0][0][base]=0;
		for (int i=1;i<=n;i++)
			for (int j=0;j<=m;j++)
			{
				for (int k=0;k<=800;k++)
				{
					f[i][j][k]=f[i-1][j][k];
					if (j-1<0) continue;
					int x=k-(d[i]-p[i]);
					if (x>800 || x<0) continue;
					f[i][j][k]=max(f[i][j][k],f[i-1][j-1][x]+d[i]+p[i]);
				}
			}
		int v=0;
		while (f[n][m][base+v]<0 && f[n][m][base-v]<0) v++;
		if (f[n][m][base+v]>f[n][m][base-v]) v=base+v;
		else v=base-v;
		int cnt=0;
		int i=n,j=m,k=v;
		int D=0,P=0;
		while (j)
		{
			if (f[i][j][k]==f[i-1][j][k]) i--;
			else
			{
				ans[++cnt]=i;
				k-=(d[i]-p[i]);
				D+=d[i];
				P+=p[i];
				i--;j--;
			}
		}
		printf("Jury #%d\n",++num);
		printf("Best jury has value %d for prosecution and value %d for defence:\n",P,D);
		for (int i=m;i>=1;i--) cout<<" "<<ans[i];
		puts("\n");
	}
}
int main()
{
	T=1;
	while (T--) work();
	return 0;
}

多重背包

弱化版退化成01背包做就可以,时间复杂度为 O ( ∑ 1 n c [ i ] × M ) O(\sum_1^n{c[i]}\times M) O(1nc[i]×M)

多重背包二进制优化:

原题链接

时间复杂度为 O ( ∑ 1 n log ⁡ c [ i ] × M ) O(\sum_{1}^n \log c[i]\times M) O(1nlogc[i]×M) ,基本上可以认为就是 N ≤ 1000 , M ≤ 2000 N\le 1000 ,M \le 2000 N1000,M2000 外加一个   l o g \ log  log

原理:通过二进制拆分,拆成 p + 2 p+2 p+2 个物品,使得,可以组成 0 0 0 c [ i ] c[i] c[i] 中所有数。

推导: 因为我们知道 2 0 , 2 1 . . . . 2 k 2^0,2^1....2^k 20,21....2k 从中选择数字,可以组合成 0 0 0 2 k + 1 − 1 2^{k+1}-1 2k+11 所有的数字。

我们找到一个 p p p 满足 2 0 + 2 1 . . . + 2 p ≤ C 2^0+2^1...+2^p\le C 20+21...+2pC ,其中 C C C 表示物品个数,那么我们设 R = C − ( 2 0 + 2 1 . . . + 2 p ) R=C-(2^0+2^1...+2^p) R=C(20+21...+2p)

由于 2 p + 1 > C 2^{p+1}>C 2p+1>C 2 0 + 2 1 . . . + 2 p + 2 p + 1 > C 2^0+2^1...+2^p+2^{p+1}>C 20+21...+2p+2p+1>C

移项得 2 p + 1 > C − ( 2 0 + 2 1 . . . + 2 p ) 2^{p+1}>C-(2^0+2^1...+2^p) 2p+1>C(20+21...+2p) 因为 R = C − ( 2 0 + 2 1 . . . + 2 p ) R=C-(2^0+2^1...+2^p) R=C(20+21...+2p)

所以得 2 p + 1 > R 2^{p+1}>R 2p+1>R

因为有推导给出的结论,所以 2 0 , 2 1 . . . 2 p 2^0,2^1...2^p 2021...2p 可以组成 0 0 0 R R R 的任意数。

我们再来证明 R R R C C C 中的数如何全部组合成功。

显然的 R + 2 0 + 2 1 . . . + 2 p = C R+2^0+2^1...+2^p=C R+20+21...+2p=C 2 p + 1 > C − R 2^{p+1}>C-R 2p+1>CR ,即 R R R C C C 部分。

所以可知 2 0 , 2 1 . . . 2 p 2^0,2^1...2^p 2021...2p 可以组成 C − R C-R CR 任意部分。

进推得的 R + 2 0 , 2 1 . . . 2 p R+2^0,2^1...2^p R+2021...2p 可以组成 R R R C C C 部分

所以得出 R , 2 0 , 2 1 . . . 2 p R,2^0,2^1...2^p R,2021...2p ,一共 p + 2 p+2 p+2 元素可以组成 C C C 的任意部分。

总的来说,其实就是先证明 R R R 部分可以组成,然后在证明 R R R C C C 的部分也可以组成。

模板如下:

注意点

  • 更新之后的体积和价值中不在包含以前的物品
#include<bits/stdc++.h>
#define int long long 
using namespace std;
int read(){int x;scanf("%lld",&x);return x;}
const int B=1e6+10;
const int inf=0x3f3f3f3f;
int T;
int f[B];
int n,m;
int v[B],w[B],c[B];
int a[B],b[B],cnt;
void work()
{
	n=read(),m=read();
	for (int i=1;i<=n;i++) 
	{
		w[i]=read();
		v[i]=read();
		c[i]=read(); 
	}
	for (int i=1;i<=n;i++)
	{
		for (int j=1;j<=c[i];j<<=1)
		{
			a[++cnt]=w[i]*j;
			b[cnt]=v[i]*j;
			c[i]-=j;
		}
		if (c[i]) {a[++cnt]=c[i]*w[i];b[cnt]=c[i]*v[i];}
	}
	for (int i=1;i<=cnt;i++)
		for (int j=m;j>=a[i];j--)
			f[j]=max(f[j],f[j-a[i]]+b[i]);
	cout<<f[m]; 
}
signed main()
{
	T=1;
	while (T--) work();
	return 0;
}

多重背包单调队列优化

时间复杂度将至到线型 O ( N M ) O(NM) O(NM) ,做多可以实现 1000 × 10000 1000\times 10000 1000×10000

原理:通过减少DP方程转移方向的数量来简化时间复杂度

实现:通过多重背包一般转移式可以发现:
f [ i ] [ j ] = max ⁡ { f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − k × w [ i ] ] + k × v [ i ] } f[i][j]=\max\{f[i-1][j],f[i-1][j-k\times w[i]]+k\times v[i]\} f[i][j]=max{f[i1][j],f[i1][jk×w[i]]+k×v[i]}
每次转移,体积差距都在 w [ i ] w[i] w[i] 的倍数,说明转移和被转移的体积对 w [ i ] w[i] w[i] 取模余数相同的。我们发现,余数不同的之间不会相互影响,因此我们可以考虑分组进行转移。

在这里插入图片描述

我再来对单调队列进行一下补充:为什么用单调队列,想必大家都已经明白了,这个算法的难点在于如何维护单调队列。单单这么讲,我们直观的看,就是滑动窗口模板,但是我们发现,从 j → j + w j\to j+w jj+w 的时候。
f [ i ] [ j + w ] = max ⁡ { f [ i − 1 ] [ j ] + v , f [ i − 1 ] [ j − w ] + 2 × w . . . . . . } f[i][j+w]=\max\{f[i-1][j]+v,f[i-1][j-w]+2\times w......\} f[i][j+w]=max{f[i1][j]+v,f[i1][jw]+2×w......}
而在转移 f [ i ] [ j ] f[i][j] f[i][j] 的时候是如下:
f [ i ] [ j ] = max ⁡ { f [ i − 1 ] [ j − w ] + v , f [ i − 1 ] [ j − 2 × w ] + 2 × w . . . . . . } f[i][j]=\max\{f[i-1][j-w]+v,f[i-1][j-2\times w]+2\times w......\} f[i][j]=max{f[i1][jw]+v,f[i1][j2×w]+2×w......}
我们发现在求解 f [ i ] [ j ] f[i][j] f[i][j] 的时候,我们的需要集合转移和 f [ i ] [ j + w ] f[i][j+w] f[i][j+w] 的集合转移是不一样的。我们所说的连续是指对于按照余数分组之后,体积是连续,但是我们发现,对于相同位置,需要的状态转移表达式是不一样的。

比如

  • f [ i ] [ j + w ] f[i][j+w] f[i][j+w] 中有 f [ i − 1 ] [ j − w ] + 2 × v f[i-1][j-w]+2\times v f[i1][jw]+2×v
  • f [ i ] [ j ] f[i][j] f[i][j] 中有 f [ i − 1 ] [ j − w ] + v f[i-1][j-w]+v f[i1][jw]+v

可是我们的单调队列维护的就是这个最大值。

不难发现,当体积发生变换的时候 j → j + w j\to j+w jj+w,相同位置上的值发生改变了,这该如何办。

所以我们发现,这题目的真正的难点在这是一个数字发生变化的移动窗口

那么我们如何解决这个问题?

我们发现当 j → j + w j\to j+w jj+w 的时候,队列中的每个状态都会 + v +v +v ,因为我们队列中存放的是每个转移的体积,维护的是 f [ i − 1 ] [ k ] + V f[i-1][k]+V f[i1][k]+V(随机数) 同时加是不会影响单调性的,相当于没加,我们可以当成不操作,但是,有一个新数会加入到队列 f [ i − 1 ] [ j ] + v f[i-1][j]+v f[i1][j]+v 此时,我们为保证大小,就必须更改队列在维护时的元素的 f + V f+V f+V 的值,使得他们都加 v v v

我们可以反过来看,我们需要比较的只是大小关系,我把 f [ i − 1 ] [ j ] f[i-1][j] f[i1][j] 加入,省去 v v v,这样就不需要队列中的其他元素加了,只需要加上自己原有的 n u m × v num\times v num×v 就可以了。

所以得出了出队代码

int V=(j-q[tail])*v;
while (head<=tail && f[i-1][q[tail]]+V<=f[i-1][j]) tail--;//因为我们一直维护的是上一维度的f值,所以和 f[i-1][j] 比较,而不是 f[i][j]
q[++tail]=j;

模板

int f[3][B];
int q[B];
int n,m;
void work()
{
	cin>>n>>m;
	int x=0; 
	for (int i=1;i<=n;i++)
	{
		x^=1;
		int w=read(),v=read(),c=read();
		for (int j=0;j<w;j++)
		{
			int head=1,tail=0;
			for (int k=j;k<=m;k+=w)
			{
				f[x][k]=f[x^1][k];
				while (head<=tail && k-c*w>q[head]) head++;
				if (head<=tail) f[x][k]=max(f[x][k],f[x^1][q[head]]+(k-q[head])/w*v);
				while (head<=tail && f[x^1][q[tail]]+(k-q[tail])/w*v<=f[x^1][k]) tail--;//维护的是上一维度的f值 
				q[++tail]=k;
			}
		}
	}
	cout<<f[x][m];
}
  1. 硬币
  • N N N 种硬币,每种面值为 A i A_i Ai ,数量为 C i C_i Ci
  • 问可以组成面值的个数
  • N ≤ 100 , M ≤ 100000 N\le 100,M\le 100000 N100M100000

思路:

很明显的背包求解方案数模型,看数据范围只能线型时间复杂度,直接套用单调队列模板解决,事实上发现,并不需要队列维护,而是套用了分组转移的原理。

学到的小技巧:

  • 当遇到的题目发现诚心卡log,要想办法降级常数,否则写的繁琐导致常数和log差不多就过不去了…
//写丑了,需要吸氧才能过
#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;
int read(){int x;scanf("%d",&x);return x;}
const int B=1e5+10;
const int inf=0x3f3f3f3f;
int T;
int n,m;
int f[3][B]; 
int w[B],c[B]; 
int q[B];
void work()
{
	while (scanf("%d%d",&n,&m),n||m)
	{
		for (int i=1;i<=n;i++) w[i]=read();
		for (int i=1;i<=n;i++) c[i]=read();
		int x=0;
		memset(f,0,sizeof(f));
		f[0][0]=1;
		for (int i=1;i<=n;i++)
		{
			x^=1;
			for (int j=0;j<w[i];j++)
			{
				int head=1,tail=0;
				int sum=0;
				f[x][j]=0;
				for (int k=j;k<=m;k+=w[i])
				{
					f[x][k]+=f[x^1][k];
					while (head<=tail && k-c[i]*w[i]>q[head]) sum-=f[x^1][q[head]],head++;
					if (head<=tail)  f[x][k]+=sum;
					q[++tail]=k;sum+=f[x^1][k];
				} 
			}
		}
		int ans=0;
		for (int i=1;i<=m;i++) if (f[x][i]) ans++;
		cout<<ans<<"\n";
	}
}
int main()
{
	T=1;
	while (T--) work();
	return 0;
}

分组背包

每组至多选一个,正常推就可以了,很简单

完结!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值