这次来聊一聊DP中的子集型DP。
一.概述
DP,即动态规划。最优化问题是信息学竞赛的一大类题目, 动态规划是解决这类问题的有力武器, 但很多人在尝试理解并运用它时遇到极大的困难!当你学习了足够多的动态规划例题的时候,你会觉得它跟变魔术一样。
二.实质
动态规划实质就是记忆化搜索,是一种高效地实现回溯算法的方法,它的核心是用一个状态记忆表来记住所有中间状态的结果。因此我们要运用动态规划,首先需要发现朴素的回溯算法递归地一而再、再而三地计算一些相同的子问题,接下来需要把答案放在记忆表中而不重复计算。动态规划可以采用记忆化搜索,但记忆化搜索有其缺点,比如要用递归,不能优化空间。所以填表法作为动态规划实现的一种优化方法而广泛使用。
三.例题
1.背包问题
(1).01背包
有
分析:
[1].不正确的贪心
采用部分背包问题的贪心策略:按单位价值由大到小先装入背包,直到不能再装入物品为止。
设背包容量为
物体
物体
物体
如果按照贪心策略,选择物体
所以在
[2].动态规划
显然这是子集生成问题。
1). 状态函数:
2).问题的答案:
3).状态转移方程:
根据分步决策的分析方法,可得出:
4).边界分析:
5).优化:
通过状态转移数组可知,前
边界:
6).代码:
#include<stdio.h>
#include<string.h>
int c,n;
int v[101],p[101];
int num[101];
int f[10101];
int max(int a,int b)
{
if(a>b)
return a;
return b;
}
int min(int a,int b)
{
if(a<b)
return a;
return b;
}
int main()
{
scanf("%d%d",&c,&n);
for(int i=1;i<=n;i++)
scanf("%d%d",&v[i],&p[i]);
for(int i=1;i<=n;i++)
scanf("%d",&num[i]);
for(int i=1;i<=n;i++)
for(int j=c;j>=v[i];j--)//注意要倒着搜,填表从左向右填
f[j]=max(f[j],f[j-v[i]]+p[i]);
printf("%d\n",f[c]);
return 0;
}
这里为什么要倒着搜呢?
很简单的原因,假如我们正着搜,则会出现以下情况:
被 更新。 被 更新。
很明显,在同一阶段内(即同时处于第
(2).多重背包
有
1). 状态函数:
2).问题的答案:
3).状态转移方程:
4).边界分析:
5).优化
多重背包有一种优化方式,能使它的时间复杂度降低一个层次。
这种优化方法通常称为二进制优化。
我们可以利用二进制拆分的方式,把
然后多重背包就转换为
6).代码:
普通:
#include<stdio.h>
#include<string.h>
int c,n;
int v[101],p[101];
int num[101];
int f[10101];
int max(int a,int b)
{
if(a>b)
return a;
return b;
}
int min(int a,int b)
{
if(a<b)
return a;
return b;
}
int main()
{
scanf("%d%d",&c,&n);
for(int i=1;i<=n;i++)
scanf("%d%d",&v[i],&p[i]);
for(int i=1;i<=n;i++)
scanf("%d",&num[i]);
for(int i=1;i<=n;i++)
for(int j=c;j>=v[i];j--)//注意要倒着搜,填表从左向右填
{
int t=min(num[i],j/v[i]);
for(int l=0;l<=t;l++)
f[j]=max(f[j],f[j-v[i]*l]+p[i]*l);
}
printf("%d\n",f[c]);
return 0;
}
优化:
#include<stdio.h>
#include<string.h>
int n,v,m[5010],w[5010],s[5010],w1[50100],s1[50100],cnt;
int f[510];
int max(int a,int b)
{
if(a>b)
return a;
return b;
}
int main()
{
scanf("%d%d",&n,&v);
for(int i=1;i<=n;i++)
scanf("%d%d%d",&m[i],&w[i],&s[i]);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m[i];j<<=1)
{
w1[++cnt]=j*w[i];
s1[cnt]=j*s[i];
m[i]-=j;
}
if(m[i])
{
w1[++cnt]=m[i]*w[i];
s1[cnt]=m[i]*s[i];
}
}
for(int i=1;i<=cnt;i++)
for(int j=v;j>=w1[i];j--)
f[j]=max(f[j],f[j-w1[i]]+s1[i]);
printf("%d",f[v]);
return 0;
}
(3).完全背包
有
1). 状态函数:
2).问题的答案:
3).状态转移方程:
4).边界分析:
5).代码:
#include<stdio.h>
#include<string.h>
int c,n;
int v[101],p[101];
int num[101];
int f[10101];
int max(int a,int b)
{
if(a>b)
return a;
return b;
}
int min(int a,int b)
{
if(a<b)
return a;
return b;
}
int main()
{
scanf("%d%d",&c,&n);
for(int i=1;i<=n;i++)
scanf("%d%d",&v[i],&p[i]);
for(int i=1;i<=n;i++)
scanf("%d",&num[i]);
for(int i=1;i<=n;i++)
for(int j=v[i];j<=c;j++)//注意!!!完全背包要正着搜,填表从右向左填,保证物品数量无限
f[j]=max(f[j],f[j-v[i]]+p[i]);
printf("%d\n",f[c]);
return 0;
}
很明显,在
(4).多费用背包
有
1). 状态函数:
相对于之前,选择一个物品装入背包,不仅要消耗背包的容积,还要消耗背包的承重量,所以这时设置状态函数应该有两个费用——体积和重量。于是状态函数相应增加一维。
2).问题的答案:
3).状态转移方程:
4).边界分析:
5).代码:
#include<stdio.h>
int c,g,n;
int v[101],w[101],p[101];
int f[2010][2010];
int max(int a,int b)
{
if(a>b)
return a;
return b;
}
int main()
{
scanf("%d%d%d",&c,&g,&n);
for(int i=1;i<=n;i++)
scanf("%d%d%d",&v[i],&w[i],&p[i]);
for(int i=1;i<=n;i++)
for(int j=c;j>=v[i];j--)
for(int k=g;k>=w[i];k--)
f[j][k]=max(f[j][k],f[j-v[i]][k-w[i]]+p[i]);
printf("%d",f[c][g]);
return 0;
}
(5).多数量背包
有
1). 状态函数:
有两个背包,意味着每步有三种选择:物品
2).问题的答案:
3).状态转移方程:
4).边界分析:
5).代码:
#include<stdio.h>
#include<string.h>
int c1,c2,n;
int v[110],w[110];
int f[2010][2010];
int max(int a,int b)
{
if(a>b)
return a;
return b;
}
int main()
{
scanf("%d%d%d",&c1,&c2,&n);
for(int i=1;i<=n;i++)
scanf("%d%d",&v[i],&w[i]);
for(int i=1;i<=n;i++)
for(int j=c1;j>=0;j--)
for(int k=c2;k>=0;k--)
{
int x=-1e9,y=-1e9;
if(j>=v[i])
x=f[j-v[i]][k]+w[i];
if(k>=v[i])
y=f[j][k-v[i]]+w[i];
f[j][k]=max(f[j][k],max(x,y));
}
printf("%d",f[c1][c2]);
return 0;
}
(6).装满背包
给出
1). 状态函数:
本问题需要装满(而且必须装满),所以状态函数也应跟着改变:
2).问题的答案:
3).状态转移方程:
4).边界分析:
5).代码:
#include<stdio.h>
#include<string.h>
#define INF 0x7f7f7f7f
int c,n,k;
int v[101],p[101];
int f[20101];
int max(int a,int b)
{
if(a>b)
return a;
return b;
}
int main()
{
scanf("%d%d",&c,&n);
for(int i=1;i<=n;i++)
scanf("%d%d",&v[i],&p[i]);
scanf("%d",&k);
memset(f,-INF,sizeof(f));
f[0]=0;
for(int i=1;i<=n;i++)
for(int j=c;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+p[i]);
if(f[c]==-2122219118)
printf("-1\n");
else
printf("%d\n",f[c]);
return 0;
}
(7).K背包
给出
1). 状态函数:
本问题需要选
2).问题的答案:
3).状态转移方程:
4).边界分析:
5).代码
#include<stdio.h>
#include<string.h>
#define INF 0x7f7f7f7f
int c,n,k;
int v[101],p[101];
int dp[101][20101],ans;
int max(int a,int b)
{
if(a>b)
return a;
return b;
}
int main()
{
scanf("%d%d",&c,&n);
for(int i=1;i<=n;i++)
scanf("%d%d",&v[i],&p[i]);
memset(dp,-INF,sizeof(dp));
for(int i=0;i<=k;i++)
dp[0][i]=0;
for(int i=1;i<=n;i++)
for(int j=k;j>=1;j--)
for(int k=c;k>=v[i];k--)
dp[j][k]=max(dp[j][k],dp[j-1][k-v[i]]+p[i]);
ans=dp[k][c];
for(int i=0;i<=c;i++)
ans=max(ans,dp[k][i]);
if(ans<0)
printf("-1\n");
else
printf("%d\n",ans);
return 0;
}
(8).分组背包
有
有
现在需要选择一些物品装入这些背包,要求编号大的背包装入的物品不能比编号小的背包装入物品的编号小,具体地说,不允许把编号为
请你计算
1).状态函数
这里我用了最笨拙的办法。
2).问题的答案
3).状态转移方程
决策有三个:
1.不选这个物品
2.选这个物品,装一个新的空背包
3.选这个物品,装正在放未放满的背包
故状态转移方程如下:
int t=f[i][j-1][k];//不选
if(k>=v[j])
{
t=max(t,f[i-1][j-1][c]+p[j]);//装一个新的空背包
t=max(t,f[i][j-1][k-v[j]]+p[j]);//装正在放未放满的背包
}
f[i][j][k]=t;
4).边界条件分析
5).代码
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
using namespace std;
int n,m,c;
int v[25],p[25];
int f[25][25][205];
int main()
{
memset(f,0,sizeof(f));
scanf("%d%d%d",&n,&m,&c);
for(int i=1;i<=n;i++)
scanf("%d%d",&v[i],&p[i]);
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
for(int k=0;k<=c;k++)
{
int t=f[i][j-1][k];
if(k>=v[j])
{
t=max(t,f[i-1][j-1][c]+p[j]);
t=max(t,f[i][j-1][k-v[j]]+p[j]);
}
f[i][j][k]=t;
}
printf("%d",f[m][n][c]);
return 0;
}
(9).背包问题总结
1).动态规划是解决最优性问题的有力武器,其的关键是状态函数和状态转移方程
状态函数的定义一般根据题目的问题,问什么设什么,且要充分包含题目描述中的各“变量”。状态转移方程的就是要决策达到当前状态的最优值,有那些选项,并从这些选项中决策出最优的选项。
2).动态规划状态函数的边界分析是基础
动态规划通过状态函数的逐步转移来计算目标状态的最优值的,但必须要有一个转移的起点,分析边界时一般根据状态函数中变量的意义来确定,或者根据填表法或记忆化搜索的过程来分析。
另外,边界一般可能的值是:
2.多米诺骨牌
多米诺骨牌有上下

上方块中点数之和记为:
编程用最少的旋转次数使多米诺骨牌上下
对于图中的例子,只要将最后一个多米诺骨牌旋转
1).状态函数
根据总结中的“题目问什么就设什么意义的状态函数”,于是定如下意义的状态函数
2).问题的答案
在求答案之前,请深刻理解下面这些状态表达式的含义:
因为题目中要得到的答案是“在上下两行点数差的绝对值最小的前提下,需要的最少旋转次数”,所以答案应按如下方式计算:
int ans=0;
for(int j=0;j<=cnt;j++)
{
if(f(n,j)<INF)
{
ans=f(n,j);
break;
}
if(f(n,-j)<INF)
{
ans=f(n,-j);
break;
}
}
3).状态转移方程
按照总结中说的“要达到
选项
选项
所以,要达到
4).边界条件
根据总结中说的
5).代码实现技巧
根据状态函数

在使用平移数组时,需要注意数组定义平移后的大小:int f[1005][2*6000];
6).代码
#include<cstdio>
#include<algorithm>
#define f(x,y) f[(x)][(y)+cnt]
#define INF 1e9
using namespace std;
int n,a[1010],b[1010];
int f[1010][12010],cnt;
int min(int a,int b)
{
if(a<b)
return a;
return b;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d%d",&a[i],&b[i]);
cnt+=a[i];
cnt+=b[i];
}
for(int i=-cnt;i<=cnt;i++)
f(0,i)=INF;
f(0,0)=0;
for(int i=1;i<=n;i++)
for(int j=cnt;j>=-cnt;j--)
f(i,j)=min(f(i-1,j-(a[i]-b[i])),1+f(i-1,j-(b[i]-a[i])));
int ans=0;
for(int j=0;j<=cnt;j++)
{
if(f(n,j)<INF)
{
ans=f(n,j);
break;
}
if(f(n,-j)<INF)
{
ans=f(n,-j);
break;
}
}
printf("%d",ans);
return 0;
}
3.最大约数和
选取和不超过
1).状态函数
通过仔细观察题目可知,这道题其实是一个稍微变换了一下的
这就很好办了,我们可以很快设出状态函数:
2).代码
因为这是一个裸的
#include<stdio.h>
int max(int a,int b)
{
if(a>b)
return a;
return b;
}
int f[1001],n,c[1001],w[1001],i,j;
int main()
{
scanf("%d",&n);
for(i=1;i<=n;i++)
{
w[i]=i;
for(j=1;j<i;j++)
if(i%j==0)
c[i]+=j;
}
for(i=1;i<=n;i++)
for(j=n;j>=w[i];j--)
f[j]=max(f[j],f[j-w[i]]+c[i]);
printf("%d",f[n]);
}
4.金明的预算
金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过
| 主件 | 电脑 | 书柜 | 书桌 | 工作椅 |
|---|---|---|---|---|
| 附件 | 打印机,扫描仪 | 图书 | 台灯,文具 | 无 |
如果要买归类为附件的物品,必须先买该附件所属的主件。每个主件可以有
设第
请你帮助金明设计一个满足要求的购物单。
1).状态函数
这其实也是一个背包问题,它属于有依赖的背包问题。
所以我们的状态函数和普通背包问题的状态函数相同。
2).问题的答案
3).代码实现技巧
我们用
即
我们用
如果
如果
4).状态转移方程
这道题和普通背包问题的不同点在于它的决策不同。
普通背包问题只有两个决策:
1.不选,考虑下一个物品
2.选,背包容量减去物品体积,总价值加上物品价值。
但这道题决策有五个:
1.不选,考虑下一个物品
2.选且只选这个主件
3.选这个主件,并且选附件
4.选这个主件,并且选附件
5.选这个主件,并且选附件
所以,状态方程如下所示:
f[j]=max(f[j],f[j-zw[i]]+zv[i]);//不选和只选主件
if(j-zw[i]-fw[i][1]>=0)//选主件和附件1
f[j]=max(f[j],f[j-zw[i]-fw[i][1]]+zv[i]+fv[i][1]);
if(j-zw[i]-fw[i][2]>=0)//选主件和附件2
f[j]=max(f[j],f[j-zw[i]-fw[i][2]]+zv[i]+fv[i][2]);
if(j-zw[i]-fw[i][1]-fw[i][2]>=0)//选主件和附件1,2
f[j]=max(f[j],f[j-zw[i]-fw[i][1]-fw[i][2]]+zv[i]+fv[i][1]+fv[i][2]);
5).边界条件
6).代码
#include<stdio.h>
int n,m,zv[32001],zw[32001],fv[32001][3],fw[32001][3],dp[32001];
int v,p,q;
int max(int a,int b)
{
if(a>b)
return a;
return b;
}
int main()
{
int i,j;
scanf("%d%d",&n,&m);
for(i=1;i<=m;i++)
{
scanf("%d%d%d",&v,&p,&q);
if(q==0)
{
zw[i]=v;
zv[i]=v*p;
}
else
{
fw[q][0]++;
fw[q][fw[q][0]]=v;
fv[q][fw[q][0]]=v*p;
}
}
for(i=1;i<=m;i++)
for(j=n;j-zw[i]>=0&&zw[i]!=0;j--)
{
dp[j]=max(dp[j],dp[j-zw[i]]+zv[i]);
if(j-zw[i]-fw[i][1]>=0)
dp[j]=max(dp[j],dp[j-zw[i]-fw[i][1]]+zv[i]+fv[i][1]);
if(j-zw[i]-fw[i][2]>=0)
dp[j]=max(dp[j],dp[j-zw[i]-fw[i][2]]+zv[i]+fv[i][2]);
if(j-zw[i]-fw[i][1]-fw[i][2]>=0)
dp[j]=max(dp[j],dp[j-zw[i]-fw[i][1]-fw[i][2]]+zv[i]+fv[i][1]+fv[i][2]);
}
printf("%d",dp[n]);
return 0;
}
5.物品选取
小沐同学确信所有问题都有个多项式时间算法,为了证明,他决定自己去当一次旅行商,在上路之前,小
小沐可以选择的物品有
- 甲类物品的价值随着你分配给他的背包体积变化,它的价值与分配给它的体积满足函数关系式,
, 表示分配给该物品的体积,为非负整数, , 是每个甲类物品的两个参数。注意每个体积的甲类物品只有一个。 - 乙类物品的价值
和体积 都是固定的,但是每个乙类物品都有个参数 ,表示这个物品可供选择的个数。 - 丙类物品的价值
和体积 也是固定的,但是每个丙类物品可供选择的个数都是无限多个。
你最终的任务是确定小沐的背包最多能装有多大的价值上路。
1).状态函数
很明显这也是一个背包问题,这是一个混合背包问题。
容易可以看出,第
第
所以我们还是和以前一样的状态函数:
2).问题的答案
这里由于第
3).状态转移方程
这要看物品的类型。
对于类型
对于类型
对于类型
4).边界条件
因为这里类型
5).代码
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n,m;
struct thing
{
int x;
int a;
int b;
int c;
}s[110];
int f[2010];
int main()
{
memset(f,-0x3f,sizeof(f));
f[0]=0;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&s[i].x);
if(s[i].x==2)
scanf("%d%d%d",&s[i].a,&s[i].b,&s[i].c);
else
scanf("%d%d",&s[i].a,&s[i].b);
}
for(int i=1;i<=n;i++)
{
if(s[i].x==1)
{
for(int j=m;j>=0;j--)
for(int k=j;k>=0;k--)
f[j]=max(f[j],f[j-k]+s[i].a*k*k-s[i].b*k);
}
else if(s[i].x==2)
{
for(int j=m;j>=s[i].b;j--)
for(int k=0;k<=s[i].c&&k*s[i].b<=j;k++)
f[j]=max(f[j],f[j-k*s[i].b]+k*s[i].a);
}
else
{
for(int j=s[i].b;j<=m;j++)
f[j]=max(f[j],f[j-s[i].b]+s[i].a);
}
}
int ans=0;
for(int i=0;i<=m;i++)
ans=max(ans,f[i]);
printf("%d",ans);
return 0;
}