背包DP 呀
本笔记按照蓝书顺序进行,方便以后直接查找题目
背包总结
花了两天的时间复习完了蓝书上背包,简略的说一下学到懂东西。
- 背包变形式:填数的方案总和,二者是一样的,因此在遇到填数的问题 时,可以往背包上去想
- 一些看起来很像背包的题目,要注意数据范围,动态的改变DP的维度。
- DP的路径输出,逆推法。
- 复杂问题转换成判定性问题对于统计个数,找到最值等等的DP,我们可以在了解值域的情况下,直接把答案放入DP维度,通过判断是否存在来找到所需答案。
虽然不能说对背包十分熟练,起码学到了不少其他的东西,还复习了单调队列。
总的来说,想必未来背包将是我解决填数的问题的主要思路。
01背包
滚动数组优化,倒序循环,都是基本知识,不说了,看题。
- 数字组合
- 给出 N N N 个数,选出一些数,使得和为 M M M ,求方案数
- N ≤ 100 , M ≤ 10000 , A i ≤ 100 N\le 100,M\le 10000,A_i\le 100 N≤100,M≤10000,Ai≤100
思路
特点:背包是允许不满的,但是此题要求必须装满,求解方案数,不是求解最值。
实际上,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[i−1][j]+f[i−1][j−a[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];
}
完全背包
把二维写出来,就明白滚动数组优化后的为什么这么写了。
- 自然数拆分
- 给定一个自然数 N N N ,要求把 N N N 拆分成若干正整数相加的形式,参与加法运算的数可以重复。
- 注意,拆成方案不考虑顺序,至少拆成两个数的和
- 对 m o d 2147483648 \mod 2147483648 mod2147483648
- N ≤ 4 × 1 0 3 N\le 4\times 10^3 N≤4×103
思路
一开始读题目的时候感觉没有思路,要不是看到重复,在自然状态下,没有完全背包的加持,我是绝对不会忘完全背包上去想的。变换题目,看到 N N N 的数据,稍微更改一下题目就很显然的做了。
- 从 1 − N 1-N 1−N 中选数,一个数可以选多次,使得相加之和为 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[j−i]
但是不是倒序循环,而是正序循环。
所以基本上,背包的方案数求解基本运用到选数,更恶心点是数字拆分…
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;
}
我有一个思考:
- 如果把背包体积改成乘法,哪咋做,很显然的,除法转移貌似不行,好像曾经做过,不过忘了。
- 陪审团
- ACWing280 题目还很长,不写了
做题时思路:
列一下自己做题时出现的难点:
-
路径计算,DP路径统计一直都是我很头疼的一件事,我的路径计算还停留在用一个新数组去记录,并且不知道是否正确。
-
绝对值,一开始我把公式化简成了
∣ ∑ D i − P i ∣ |\sum{D_i-P_i}| ∣∑Di−Pi∣
然后就按照每次 ∣ D i − P i ∣ |D_i-P_i| ∣Di−Pi∣ 去统计了,结果发现很明显是错误的,当时就炸了,这可没办法跑DP,因此状态与状态之间不可以转移,每个状态的答案取决于当前差的和的绝对值大小,因为绝对值,最小值,最大值维护都是错误的,当时就没有办法解决绝对值,因此也就不做了 -
还有这个题有个特点,就是先求 min { ∣ ∑ D i − P i ∣ } \min\{|\sum{D_i-P_i}|\} min{∣∑Di−Pi∣} ,再求 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 N≤1000,M≤2000 外加一个 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+1−1 所有的数字。
我们找到一个 p p p 满足 2 0 + 2 1 . . . + 2 p ≤ C 2^0+2^1...+2^p\le C 20+21...+2p≤C ,其中 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 20,21...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>C−R ,即 R R R 到 C C C 部分。
所以可知 2 0 , 2 1 . . . 2 p 2^0,2^1...2^p 20,21...2p 可以组成 C − R C-R C−R 任意部分。
进推得的 R + 2 0 , 2 1 . . . 2 p R+2^0,2^1...2^p R+20,21...2p 可以组成 R R R 到 C C C 部分
所以得出 R , 2 0 , 2 1 . . . 2 p R,2^0,2^1...2^p R,20,21...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[i−1][j],f[i−1][j−k×w[i]]+k×v[i]}
每次转移,体积差距都在
w
[
i
]
w[i]
w[i] 的倍数,说明转移和被转移的体积对
w
[
i
]
w[i]
w[i] 取模余数相同的。我们发现,余数不同的之间不会相互影响,因此我们可以考虑分组进行转移。

我再来对单调队列进行一下补充:为什么用单调队列,想必大家都已经明白了,这个算法的难点在于如何维护单调队列。单单这么讲,我们直观的看,就是滑动窗口模板,但是我们发现,从
j
→
j
+
w
j\to j+w
j→j+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[i−1][j]+v,f[i−1][j−w]+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[i−1][j−w]+v,f[i−1][j−2×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[i−1][j−w]+2×v
- f [ i ] [ j ] f[i][j] f[i][j] 中有 f [ i − 1 ] [ j − w ] + v f[i-1][j-w]+v f[i−1][j−w]+v
可是我们的单调队列维护的就是这个最大值。
不难发现,当体积发生变换的时候 j → j + w j\to j+w j→j+w,相同位置上的值发生改变了,这该如何办。
所以我们发现,这题目的真正的难点在这是一个数字发生变化的移动窗口
那么我们如何解决这个问题?
我们发现当 j → j + w j\to j+w j→j+w 的时候,队列中的每个状态都会 + v +v +v ,因为我们队列中存放的是每个转移的体积,维护的是 f [ i − 1 ] [ k ] + V f[i-1][k]+V f[i−1][k]+V(随机数) 同时加是不会影响单调性的,相当于没加,我们可以当成不操作,但是,有一个新数会加入到队列 f [ i − 1 ] [ j ] + v f[i-1][j]+v f[i−1][j]+v 此时,我们为保证大小,就必须更改队列在维护时的元素的 f + V f+V f+V 的值,使得他们都加 v v v。
我们可以反过来看,我们需要比较的只是大小关系,我把 f [ i − 1 ] [ j ] f[i-1][j] f[i−1][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];
}
- 硬币
- N N N 种硬币,每种面值为 A i A_i Ai ,数量为 C i C_i Ci
- 问可以组成面值的个数
- N ≤ 100 , M ≤ 100000 N\le 100,M\le 100000 N≤100,M≤100000
思路:
很明显的背包求解方案数模型,看数据范围只能线型时间复杂度,直接套用单调队列模板解决,事实上发现,并不需要队列维护,而是套用了分组转移的原理。
学到的小技巧:
- 当遇到的题目发现诚心卡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;
}
分组背包
每组至多选一个,正常推就可以了,很简单
完结!
1244

被折叠的 条评论
为什么被折叠?



