【5】背包类型动态规划学习笔记
前言
学习了简单背包DP,这一篇博客主要是讲背包问题进阶。所以这篇博客的难度系数很有可能不止
基础背包
01背包
给定
设
其中,
模板如下:
for(int i=1;i<=m;i++)
for(int j=t;j>=c[i];j--)
f[j]=max(f[j],f[j-c[i]]+w[i]);
注意此时对于
时间复杂度:
完全背包
给定
设
没错,和01背包一模一样。
模板如下:
for(int i=1;i<=m;i++)
for(int j=c[i];j<=t;j++)
f[j]=max(f[j],f[j-c[i]]+w[i]);
注意此时对于
时间复杂度:
满背包
给定
设
没错,还是和01背包一模一样。
模板如下:
for(int i=1;i<=t;i++)f[i]=-99999999;
f[i]=0;
for(int i=1;i<=m;i++)
for(int j=t;j>=c[i];j--)
f[j]=max(f[j],f[j-c[i]]+w[i]);
由于必须装满的原因,把未使用的空缺赋上负无穷,表示不能从此处转移,保证没有空余。
时间复杂度:
基础例题
例 1 :
P1048 [NOIP2005 普及组] 采药
01背包模板题,不多赘述。
#include <bits/stdc++.h>
using namespace std;
int c[2000],w[2000],f[2000];
int main()
{
int t,m;
scanf("%d%d",&t,&m);
for(int i=1;i<=m;i++)
scanf("%d%d",&c[i],&w[i]);
for(int i=1;i<=m;i++)
for(int j=t;j>=c[i];j--)
f[j]=max(f[j],f[j-c[i]]+w[i]);
printf("%lld",f[t]);
return 0;
}
例 2 :
P1616 疯狂的采药
完全背包模板题,不多赘述。
#include <bits/stdc++.h>
using namespace std;
long long c[20000000],w[20000000],f[20000000];
int main()
{
long long t,m;
scanf("%lld%lld",&t,&m);
for(long long i=1;i<=m;i++)
scanf("%lld%lld",&c[i],&w[i]);
for(long long i=1;i<=m;i++)
for(long long j=c[i];j<=t;j++)
f[j]=max(f[j],f[j-c[i]]+w[i]);
printf("%lld",f[t]);
return 0;
}
例 3 :
P1049 [NOIP2001 普及组] 装箱问题
由于没有价值,题目要求的是最大体积,所以把体积直接看成价值,然后就是01模板了。
#include <bits/stdc++.h>
using namespace std;
int n,v,t[30000],f[30000];
int main()
{
scanf("%d",&v);
scanf("%d",&n);
for(int i=0;i<n;i++)
scanf("%d",&t[i]);
for(int i=0;i<n;i++)
for(int j=v;j>=t[i];j--)
f[j]=max(f[j],f[j-t[i]]+t[i]);
printf("%d",v-f[v]);
return 0;
}
例 4 :
P1855 榨取kkksc03
在原先体积限制的基础下,增加了金钱限制。设状态
其中,
#include <bits/stdc++.h>
#define INF 999999999
using namespace std;
int n,m,t,a[300],b[300],f[300][300];
int main()
{
scanf("%d%d%d",&n,&m,&t);
for(int i=0;i<n;i++)
scanf("%d%d",&a[i],&b[i]);
for(int i=0;i<n;i++)
for(int j=m;j>=a[i];j--)
for(int k=t;k>=b[i];k--)
f[j][k]=max(f[j][k],f[j-a[i]][k-b[i]]+1);
printf("%d",f[m][t]);
return 0;
}
例 5 :
P1164 小A点菜
设状态
其中,
注意题目中
一定刚好把 uim 身上所有钱花完
所以是满背包,代码中是另一种实现方式:
#include <bits/stdc++.h>
using namespace std;
int t,m,f[20000],w[20000];
int main()
{
scanf("%d%d",&m,&t);
for(int i=1;i<=m;i++)scanf("%d",&w[i]);
f[0]=1;
for(int i=1;i<=m;i++)
for(int j=t;j>=w[i];j--)
if(f[j-w[i]]!=0)f[j]=f[j]+f[j-w[i]];
printf("%d",f[t]);
return 0;
}
进阶背包
分组背包
给定
设
避免冲突的方法,就是每次枚举组数,然后在枚举
模板如下:
for(int i=0;i<=2000;i++)
{
int l=c[i].size();
if(l==0)continue;
for(int j=m;j>=0;j--)
for(int k=0;k<l;k++)
if(j>=a[c[i][k]])f[j]=max(f[j],f[j-a[c[i][k]]]+b[c[i][k]]);
}
时间复杂度:
多重背包
给定
朴素求法:
按照朴素01背包来做,每次枚举数量,时间复杂度如下:
很明显,复杂度非常高,需要考虑优化。
二进制拆分优化:
还记得在 【6】ST表学习笔记 中提到过的 倍增 思想吗?
倍增思想常用来优化算法的时空复杂度,和其他算法搭配使用。
其实,二进制拆分也是一种倍增。
首先,考虑将数量拆分成
例如,当
也就是
那拆分的数拆不尽怎么办?设拆分的数为
代码如下:
int now=0;
for(int j=1;now<=e[d[i]];j*=2)
if(now+j<=e[d[i]])c1[++q]=d[i]*j,now+=j;
else break;
if(e[d[i]]>now)c1[++q]=(e[d[i]]-now)*d[i];
代码中 now 是目前可以表示的量的范围,表示
时间复杂度如下:
单调队列优化:
听说可以做到
进阶例题
例 6 :
P1757 通天之分组背包
分组背包模板题,不多赘述。
#include <bits/stdc++.h>
using namespace std;
int n,m;
int ans=0,a[2000],b[2000],f[2000];
vector<int>c[2000];
int main()
{
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++)
{
int d;
scanf("%d%d%d",&a[i],&b[i],&d);
c[d].push_back(i);
}
for(int i=0;i<=2000;i++)
{
int l=c[i].size();
if(l==0)continue;
for(int j=m;j>=0;j--)
for(int k=0;k<l;k++)
if(j>=a[c[i][k]])f[j]=max(f[j],f[j-a[c[i][k]]]+b[c[i][k]]);
}
for(int i=0;i<=m;i++)
ans=max(ans,f[i]);
printf("%d",f[m]);
return 0;
}
例 7 :
CF864E Fire
这题在01背包下增加了一个限制:时限
设
另外,此题要求输出带走的物品。可以使用STL中的 vector 来存储可以带走的物品。如果在转移的过程中,发现
#include <bits/stdc++.h>
using namespace std;
struct item
{
int t,d,p,h;
}a[200];
int n,k,ans=0,f[5000],q=0;
vector<int>b[5000];
bool cmp(struct item a,struct item b)
{
return a.d<b.d;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d%d%d",&a[i].t,&a[i].d,&a[i].p);
a[i].h=i;
k=max(k,a[i].d);
}
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++)
for(int j=a[i].d-1;j>=a[i].t;j--)
if(f[j-a[i].t]+a[i].p>f[j])
{
f[j]=f[j-a[i].t]+a[i].p;
b[j].clear();
b[j]=b[j-a[i].t];
b[j].push_back(a[i].h);
}
for(int i=0;i<=k;i++)
if(f[i]>ans)ans=f[i],q=i;
printf("%d\n",ans);
int l=b[q].size();
printf("%d\n",l);
for(int i=0;i<l;i++)
printf("%d ",b[q][i]);
printf("\n");
return 0;
}
例 8 :
P1417 烹调方案
如果要先选第
移项抵消得
去掉负号得
所以把物品按照
#include <bits/stdc++.h>
using namespace std;
struct item
{
int a,b,c;
}a[200];
int n,t,ans=0,f[100010];
bool cmp(struct item a,struct item b)
{
return (long long)b.b*a.c<(long long)a.b*b.c;
}
int main()
{
scanf("%d%d",&t,&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i].a);
for(int i=1;i<=n;i++)scanf("%d",&a[i].b);
for(int i=1;i<=n;i++)scanf("%d",&a[i].c);
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++)
for(int j=t;j>=a[i].c;j--)
if(a[i].a-(long long)j*a[i].b>0)f[j]=max(f[j],(int)(f[j-a[i].c]+a[i].a-(long long)j*a[i].b));
else f[j]=max(f[j],f[j-a[i].c]);
for(int i=0;i<=t;i++)
ans=max(f[i],ans);
printf("%d",ans);
return 0;
}
例 9 :
CF366C Dima and Salad
由
移项得
将
移项得
把
问题就转换为了每个物品体积为
由于体积
#include <bits/stdc++.h>
using namespace std;
int n,k,a[300],b[300],f[10020],g[10020],ans;
int main()
{
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++)scanf("%d",&a[i]);
for(int i=0;i<n;i++)scanf("%d",&b[i]);
for(int i=1;i<=10001;i++)f[i]=-99999,g[i]=-99999;
for(int i=0;i<n;i++)
{
int c=a[i]-k*b[i];
if(c>=0)
for(int j=10000;j>=c;j--)
f[j]=max(f[j],f[j-c]+a[i]);
else
for(int j=10000;j>=-c;j--)
g[j]=max(g[j],g[j+c]+a[i]);
}
for(int i=0;i<=10000;i++)
ans=max(ans,f[i]+g[i]);
if(ans!=0)printf("%d",ans);
else printf("-1");
return 0;
}
例 10 :
CF755F PolandBall and Gifts
由于
有 n 个人要互相送礼物
可以知道,如果把
对于最大的情况,可以使用贪心。由题目中
一个人能收到礼物,当且仅当她带了礼物,并且应该送给她礼物的人也带了礼物。
所以如果要最大化收不到礼物的人数,可以隔一个人没带一次,这样一次就有两个人收不到礼物。对于人数为偶数的环(以后简写为偶环),可以刚刚好隔一个人没带一次;对于人数为奇数的环(以后简写为奇环),会多出一个人来,如果这个人没带,只会有一个人收不到礼物。所以对于奇环多余的人,可以放在后面处理。优先处理偶环以及奇环中能一次有两个人收不到礼物的人,再处理奇环中多出来的人。
对于最小化的情况,可以使一整个环的人都没带礼物。这样就可以使每个没带礼物的人只能对答案产生
判断是否有若干个环人数和
注意这里每个环的数量可能出现多次,实质上是一个多重背包。朴素做法会超时,要进行二进制拆分优化。
#include <bits/stdc++.h>
using namespace std;
int n,k,p,q,l,a[1000020],vis[1000020],c[1000020],c1[1000020],f[1000020],book[1000020],d[1000020],e[1000020];
inline int read()
{
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
int dfs(int s,int now,int cnt)
{
vis[now]=1;
if(s==now&&cnt!=1)return cnt-1;
else dfs(s,a[now],cnt+1);
}
void least()
{
for(int i=1;i<=p;i++)
if(!book[c[i]])d[++l]=c[i],book[c[i]]=1,e[c[i]]++;
else e[c[i]]++;
for(int i=1;i<=l;i++)
{
int now=0;
for(int j=1;now<=e[d[i]];j*=2)
if(now+j<=e[d[i]])c1[++q]=d[i]*j,now+=j;
else break;
if(e[d[i]]>now)c1[++q]=(e[d[i]]-now)*d[i];
}
for(int i=1;i<=k;i++)f[i]=-99999999;
for(int i=1;i<=q;i++)
for(int j=k;j>=c1[i];j--)
f[j]=max(f[j],f[j-c1[i]]+c1[i]);
if(f[k]>=0)printf("%d ",k);
else printf("%d ",k+1);
}
void most()
{
int h=0,cnt=0;
bool flag=0;
for(int i=1;i<=p;i++)
{
if(h+c[i]/2<=k)h+=c[i]/2;
else
{
h=k;
flag=1;
}
if(c[i]%2)cnt++;
}
h*=2;
if(flag==0)h+=min(k-h/2,cnt);
printf("%d",h);
}
int main()
{
n=read();k=read();
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<=n;i++)
if(!vis[i])c[++p]=dfs(i,i,1);
least();
most();
return 0;
}
后记
也没什么好说的,DP之难难于上青天。