背包九讲
背包九讲
01背包
经典问题。
设 \(f[i][j]\) 表示前 \(i\) 种物品放入容量为 \(j\) 的背包能获得的最大价值,则:
优化空间复杂度,把第一维滚动掉,因为不能重复选,所以状态 \(i\) 只能 \(i-1\) 转,压到一维里只能从后往前枚举保证不会用更新过的状态更新后面的。
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
//#define int long long
const int N=1e6+7;
using namespace std;
inline int read()
{
int x(0),f(0);
char ch=getchar();
while(isspace(ch))f|=(ch=='-'),ch=getchar();
while(isdigit(ch))x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return f?-x:x;
}
int n,V,w[N],v[N],f[N];
signed main()
{
n=read(),V=read();
for(int i=1;i<=n;i++)v[i]=read(),w[i]=read();
for(int i=1;i<=n;i++)
for(int j=V;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+w[i]);
printf("%d\n",f[V]);
return 0;
}
完全背包
话说刚开始只知道和 01 背包的枚举顺序不一样,原理不知道。
其实完全背包的基本 DP 柿子应该是:
复杂度很高,所以要优化。
其实也可以不枚举 \(k\):
可以说是继承上一维的状态拿到这一维继续更新。
然后滚动掉第一维,正序枚举即可。
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
//#define int long long
const int N=1e6+7;
using namespace std;
inline int read()
{
int x(0),f(0);
char ch=getchar();
while(isspace(ch))f|=(ch=='-'),ch=getchar();
while(isdigit(ch))x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return f?-x:x;
}
int n,V,w[N],v[N],f[N];
signed main()
{
n=read(),V=read();
for(int i=1;i<=n;i++)v[i]=read(),w[i]=read();
for(int i=1;i<=n;i++)
for(int j=v[i];j<=V;j++)
f[j]=max(f[j],f[j-v[i]]+w[i]);
printf("%d\n",f[V]);
return 0;
}
多重背包
就是在完全背包的基础上限制了数量。
Ⅰ:
最简单的思路就是把物品拆开跑 01背包,复杂度 \(\mathcal{O(V\times \displaystyle\sum{s_i})}\):
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
//#define int long long
const int N=1e6+7;
using namespace std;
inline int read()
{
int x(0),f(0);
char ch=getchar();
while(isspace(ch))f|=(ch=='-'),ch=getchar();
while(isdigit(ch))x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return f?-x:x;
}
int n,V,w[N],v[N],s[N],f[N];
signed main()
{
n=read(),V=read();
for(int i=1;i<=n;i++)v[i]=read(),w[i]=read(),s[i]=read();
for(int i=1;i<=n;i++)
for(int j=V;j>=v[i];j--)
for(int k=0;k<=s[i]&&k*v[i]<=j;k++)
f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
printf("%d\n",f[V]);
return 0;
}
Ⅱ:
当然对于 Ⅰ 中的实现方式的复杂度很难接受,所以要进行优化,而二进制拆分可以很好的解决这个问题,因为每个 \(s[i]\) 都能用二进制表示出来,那么将 \(s[i]\) 进行二进制拆分,能组成 \(1\) 到 \(s[i]\) 的所有数,也就间接的选了一些数量的物品。
复杂度 \(\mathcal{O(V\times \displaystyle \sum{\log_{s_i}})}\)
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
//#define int long long
const int N=1e6+7;
using namespace std;
inline int read()
{
int x(0),f(0);
char ch=getchar();
while(isspace(ch))f|=(ch=='-'),ch=getchar();
while(isdigit(ch))x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return f?-x:x;
}
int n,V,cnt,w[N],v[N],s[N],f[N];
signed main()
{
n=read(),V=read();
for(int i=1;i<=n;i++)
{
int vv=read(),ww=read(),ss=read();
for(int j=1;j<=ss;j<<=1)
{
v[++cnt]=j*vv;
w[cnt]=j*ww;
ss-=j;
}
if(s) v[++cnt]=ss*vv,w[cnt]=ss*ww;
}
for(int i=1;i<=cnt;i++)
for(int j=V;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+w[i]);
printf("%d\n",f[V]);
return 0;
}
Ⅲ:
二进制优化依然不优怎么办?
继续思考优化。
回到朴素的 DP 柿子:\(f[j]=max(f[j],f[j-k\times v]+k\times w)\)
不难发现 \(j\) 的状态跟 \(j-k\times v\) 有关,突然发现它们模 \(v\) 之后的余数是一样的,也就是说 DP 数组是按类更新的,它跟前面 \(s\) 个 \(j-k\times v\) 有关。
既然是一类一类的递推,考虑单调队列,因为单调队列是顺序更新的,所以先 copy 一遍 DP 数组,再用 copy 出来的数组更新就做到了顺序更新。
考虑空间带来价值的影响更新单调队列即可。
复杂度 \(\mathcal{O(nV)}\)
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
//#define int long long
const int N=1e6+7;
using namespace std;
inline int read()
{
int x(0),f(0);
char ch=getchar();
while(isspace(ch))f|=(ch=='-'),ch=getchar();
while(isdigit(ch))x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return f?-x:x;
}
int n,V,w[N],v[N],s[N],f[N],g[N],q[N];
signed main()
{
n=read();V=read();
for(int i=1;i<=n;i++) v[i]=read(),w[i]=read(),s[i]=read();
for(int i=1;i<=n;i++)
{
memcpy(g,f,sizeof f);
for(int j=0;j<v[i];j++)
{
int head=1,tail=0;
for(int k=j;k<=V;k+=v[i])
//分组f[j-s*v[i]] -- f[j]
{
if(head<=tail&&q[head]+s[i]*v[i]<k)head++;
if(head<=tail) f[k]=max(g[k],g[q[head]]+(k-q[head])/v[i]*w[i]);
while(head<=tail&&g[k]>=g[q[tail]]+(k-q[tail])/v[i]*w[i]) tail--;
q[++tail]=k;
}
}
}
printf("%d\n",f[V]);
return 0;
}
我们比较一下三种方法的效率:
朴素:\(\mathcal{O(V\sum s_i)}\)
二进制:\(\mathcal{O(V\log{s_i})}\)
单调队列:\(\mathcal{O(nV)}\)
假若 \(n=1e3,s_i=1e3\)
那么二进制较朴素快了 \(100\) 倍,单调队列相较于二进制又提高 \(10\) 倍!当然数据越大差距越大。
混合背包
很简单,把多重背包二进制拆解成 01 背包,然后分 01 和完全两类类转移。
或者分别写三个函数,是哪个就套哪个。
不是很想写,就把之前代码粘过来了。
#include<cmath>
#include<queue>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e6+2049,M=2049;
const int INF=0x3f3f3f3f;
const int Mod=1e9+7;
int read() {
int x=0,f=0;char ch=getchar();
for(;!isdigit(ch);ch=getchar()) f|=(ch=='-');
for(;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
return f?-x:x;
}
int n,w[N],v[N],C[N],sc,f[N];
signed main() {
int n=read(),V=read();
for(int i=1,v_,w_,s;i<=n;i++){
v_=read();w_=read();s=read();
if(!s)v[++sc]=v_,w[sc]=w_,C[sc]=INF;
else{
if(s==-1) s=1;
for(int k=1;k<=s;k<<=1){
v[++sc]=v_*k;w[sc]=w_*k;C[sc]=-INF,s-=k;
}
if(s) v[++sc]=v_*s,w[sc]=w_*s,C[sc]=-INF;
}
}
for(int i=1;i<=sc;i++){
if(C[i]==-INF)
for(int j=V;j>=v[i];j--)f[j]=max(f[j],f[j-v[i]]+w[i]);
else
for(int j=v[i];j<=V;j++)f[j]=max(f[j],f[j-v[i]]+w[i]);
}
printf("%d\n",f[V]);
return 0;
}
二维费用背包
类比 01 背包,再加一维即可。
设 \(f[i][j]\) 表示使用 \(i\) 的容积,\(j\) 的承重能装的最大价值。
复杂度 \(\mathcal{O(nVM)}\)
代码也很好写,还是贴之前代码:
#include<cmath>
#include<queue>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=2049;
const int INF=0x3f3f3f3f;
const int Mod=1e9+7;
int read() {
int x=0,f=0;char ch=getchar();
for(;!isdigit(ch);ch=getchar()) f|=(ch=='-');
for(;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
return f?-x:x;
}
void print(int x) {
if(x<0) putchar('-'),x=-x;
if(x>9) print(x/10);
putchar(x%10+48);
}
int f[N][N],n,V,M;
int v[N],m[N],w[N];
signed main() {
n=read();V=read();M=read();
for(int i=1;i<=n;i++)v[i]=read(),m[i]=read(),w[i]=read();
for(int i=1;i<=n;i++){
for(int j=V;j>=v[i];j--){
for(int k=M;k>=m[i];k--){
f[j][k]=max(f[j][k],f[j-v[i]][k-m[i]]+w[i]);
}
}
}
print(f[V][M]);
return 0;
}
分组背包
设 \(f[i][j]\) 表示前 \(i\) 组物品,能放入容量为 \(j\) 的背包的最大价值。
朴素算法就是循环组,循环背包容量。
先寄了再说……