动态规划
<目录
动态规划 \(\sf\color{gray}Dynamic\ Programming\)
由来
动态规划是怎么来的?
我想,应该从递归说起……
数字三角形 Number Triangles
观察下面的数字金字塔。
写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。
在上面的样例中,从 \(\sf 7\rarr 3\rarr 8\rarr 7\rarr 5\) 的路径产生了最大
递归
递归是个好办法。
DFS从上往下遍历,记录答案。
#include<cstdio>
int Max(int a,int b){return a>b?a:b;}
int Map[1010][1010],N,Fans;
void Find(int x,int y,int Lsum)
{
Lsum+=Map[x][y];
if(x==N-1)
{Fans=Max(Lsum,Fans);return;}
Find(x+1,y,Lsum);
Find(x+1,y+1,Lsum);
}
int main()
{
scanf("%d",&N);
for(int i=0;i<N;i++)
for(int j=0;j<=i;j++)
scanf("%d",&Map[i][j]);
Find(0,0,0);
printf("%d",Fans);
return 0;
}
可是这样慢炸了啊,于是我们就想到了……
记忆化搜索
加上记忆化就挺不错。
#include<cstdio>
int Max(int a,int b){return a>b?a:b;}
int Map[1010][1010],Ans[1010][1010],N,Fans;
void Find(int Fx,int Fy,int x,int y)
{
if(Map[x][y]+Ans[Fx][Fy]>Ans[x][y])
Ans[x][y]=Map[x][y]+Ans[Fx][Fy];
else return;
if(x==N) return;
Find(x,y,x+1,y);
Find(x,y,x+1,y+1);
}
int main()
{
scanf("%d",&N);
for(int i=0;i<N;i++)
for(int j=0;j<=i;j++)
scanf("%d",&Map[i][j]);
Find(0,0,0,0);
for(int i=0;i<N;i++)
Fans=Max(Fans,Ans[N-1][i]);
printf("%d",Fans);
return 0;
}
可是还是慢啊……
诶?
我们注意一下这一行:
if(Map[x][y]+Ans[Fx][Fy]>Ans[x][y]) Ans[x][y]=Map[x][y]+Ans[Fx][Fy]; else return;
如果比原结果小,就停下。
递归肯定是没有递推优的。每一次调用函数都会新开空间,进入与返回也会花时间,递归层数一多,栈空间还可能炸。
我们换成递推试试?
动态规划
换成递归后,就成了动态规划。
#include<cstdio>
int Max(int a,int b){return a>b?a:b;}
int Map[1003][1003],N;
int main()
{
scanf("%d",&N);
for(int i=0;i<N;i++)
for(int j=0;j<=i;j++)
scanf("%d",&Map[i][j]);
for(int i=N-1;i>=0;i--)
for(int j=i;j>=0;j--)
Map[i][j]+=Max(Map[i+1][j],Map[i+1][j+1]);
printf("%d",Map[0][0]);
return 0;
}
从原始状态,经过决策,转移到最终状态,这就是动态规划。
动态规划三要素
\(\boxed{\sf最优子结构}\boxed{\sf状态转移方程}\boxed{\sf无后效性}\)
最优子结构:保证在子问题最优的基础上做出最优选择后父问题最优。
状态转移方程:决策方程,解决如何决策最优的问题。
无后效性:父问题的解不影响子问题的解。
满足这三者的,才可使用动态规划。
在上面的问题中:
最优子结构 | 上一个节点最大时,此节点在最优决策下最大 |
---|---|
状态转移方程 | \(\sf F[i][j]=\begin{cases}\sf\max\{F[i-1][j],F[i-1][j-1]\}+Map[i][j]&\sf i>0,j\not=i\\\sf Map[i][j]&\sf i=0,j=0\\\sf F[i-1][j]+Map[i][j]&\sf j=0\\\sf F[i][j-1]+Map[i][j]&\sf j=i\end{cases}\) |
无后效性 | \(\sf F[i][j]\) 不会影响 \(\sf F[i-1][j]\) 和 \(\sf F[i-1][j-1]\) |
因此可以用动态规划解决。
背包问题
背包是一个动态规划的大分支 (非常 \(\sf\Huge大\))
一般来讲,就是这么一个问题……
零一背包
有一个背包容量为 \(\sf V\) ,同时有 \(\sf N\) 个物品,第 \(\sf i\) 个物品有体积 \(\sf W_i\) ,价值 $ \sf C_i$ 。
现在从 \(\sf N\) 个物品中,任取若干个装入包内,使包里的物品价值总和最大。
我们用 \(\sf Pack[i][j]\) 来表示取前 \(\sf i\) 样物品装进 \(\sf j\) 容量的背包所能取到的最大价值。
这样,就构造了一个 \(\sf最优子结构\) 出来。
要么不装最大,要么从某一个状态装上这个物品后最大。
状态转移方程:
可能有点费空间,怎么办呢?
注意 \(\sf Pack[i][m]\) 只与 \(\sf Pack[i-1][n]\) 有关。
所以可以用滚动数组自然继承:
用 \(\sf Pack[j]\) 表示当前物品装进 \(\sf j\) 容量的背包所能取到的最大价值。
代码
#include<cstdio>
#include<algorithm>
using namespace std;
int N,V,Pack[1005],W[1005],C[1005];
int main()
{
scanf("%d%d",&V,&N);
for(int i=1;i<=N;i++)
scanf("%d%d",&W[i],&C[i]);
for(int i=1;i<=N;i++)
for(int j=V;j>=W[i];j--)
Pack[j]=max(Pack[j],Pack[j-W[i]]+C[i]);
printf("%d",Pack[V]);
return 0;
}
注意这里有个小细节:
for(int j=V;j>=W[i];j--)
为什么要倒着枚举 \(\sf j\) ?
因为要保证后面的答案要通过前面的计算。
前面答案的意义应该是 \(\sf Pack[i-1][j]\) ,如果被计算,意义就变成了 \(\sf Pack[i][j]\) 了,后面的答案不就错了吗?
完全背包
有一个背包容量为 \(\sf V\) ,同时有 \(\sf N\) 种物品,每种物品有若干个。
第 \(\sf i\) 种物品有体积 \(\sf W_i\) ,价值 $ \sf C_i$ 。
现在从 \(\sf N\) 种物品中,任取若干个装入包内,使包里的物品价值总和最大。
可以看到,只是每个物品有了无限个。
那么……
这是在干什么?
这是在迭代啊。
从 \(\sf Pack[i][j-w_i]\) 迭代过来,不就相当于可以无限装入了吗?
代码
#include<cstdio>
#include<algorithm>
using namespace std;
int N,V,W[1005],C[1005],Pack[1005];
int main()
{
scanf("%d%d",&V,&N);
for(int i=1;i<=N;i++)
scanf("%d%d",&W[i],&C[i]);
for(int i=1;i<=N;i++)
for(int j=W[i];j<=V;j++)
Pack[j]=max(Pack[j],Pack[j-W[i]]+C[i]);
printf("%d",Pack[V]);
return 0;
}
多重背包
有一个背包容量为 \(\sf V\) ,同时有 \(\sf N\) 种物品,每种物品有 \(\sf S_i\) 个。
第 \(\sf i\) 种物品有体积 \(\sf W_i\) ,价值 $ \sf C_i$ 。
现在从 \(\sf N\) 种物品中,任取若干个装入包内,使包里的物品价值总和最大。
有了数量限制,怎么办呢?
只好枚举数量了。
代码
#include<cstdio>
#include<algorithm>
using namespace std;
int N,V,W[1005],C[1005],S[1005],Pack[1005];
int main()
{
scanf("%d%d",&N,&V);
for(int i=1;i<=N;i++)
scanf("%d%d%d",&W[i],&C[i],&S[i]);
for(int i=1;i<=N;i++)
for(int j=V;j>=W[i];j--)
for(int k=0;k<=S[i];k++)
{
if(j-k*W[i]<0)
break;
Pack[j]=max(Pack[j],Pack[j-k*W[i]]+k*C[i]);
}
printf("%d",Pack[V]);
return 0;
}
多重背包 - 二进制优化
我们考虑把多重背包转化成零一背包。
我们把一些物品按 \(\sf 2^n\) 给捆绑起来,变成一些大的,只有一个的大物品。
而这些物品经过不同的组合可以等价于原来选 \(\sf n\) 个这种物品。
如:
而任意的数量都可以用 \(\sf 2^0\) , \(\sf 2^1\) , \(\sf 3\) 拼出来:
代码
#include<cstdio>
#include<algorithm>
using namespace std;
int N,V,W[100005],C[100005],Pack[100005];
int oW,oC,oS,ooS,ooo,nN=1;
int main()
{
scanf("%d%d",&N,&V);
for(int i=0;i<N;i++)
{
scanf("%d%d%d",&oW,&oC,&oS);
ooS=oS,ooo=1;
while(ooS>oS/2)
W[nN]=oW*ooo,C[nN]=oC*ooo,ooS-=ooo,ooo<<=1,nN++;
if(ooS!=0)
W[nN]=oW*ooS,C[nN]=oC*ooS,nN++;
}
for(int i=1;i<nN;i++)
for(int j=V;j>0;j--)
if(j>=W[i])
Pack[j]=max(Pack[j],Pack[j-W[i]]+C[i]);
printf("%d",Pack[V]);
return 0;
}
混合背包
有一个背包容量为 \(\sf V\) ,同时有 \(\sf N\) 种物品。
有些物品只有一个,有些物品有 \(\sf S_i\) 个,有些物品有无数个。
第 \(\sf i\) 种物品有体积 \(\sf W_i\) ,价值 $ \sf C_i$ 。
现在从 \(\sf N\) 种物品中,任取若干个装入包内,使包里的物品价值总和最大。
可以看到,这是一个大混合,我们只需要针对每个物品去处理就好。
代码
#include<cstdio>
#include<algorithm>
int N,V,W[1005],C[1005],S[1005],Pack[1005];
using namespace std;
int main()
{
scanf("%d%d",&N,&V);
for(int i=0;i<N;i++)
scanf("%d%d%d",&W[i],&C[i],&S[i]);
for(int i=1;i<=N;i++)
if(S[i]==0)
for(int j=W[i];j<=V;j++)
Pack[j]=max(Pack[j],Pack[j-W[i]]+C[i]);
else if(S[i]==-1)
for(int j=V;j>=W[i];j--)
Pack[j]=max(Pack[j],Pack[j-W[i]]+C[i]);
else
for(int j=V;j>=W[i];j--)
for(int k=0;k<=S[i];k++)
if(j-k*W[i]<0)
break;
else
Pack[j]=max(Pack[j],Pack[j-k*W[i]]+k*C[i]);
printf("%d",Pack[V]);
return 0;
}
二维费用背包
有一个背包容量为 \(\sf V_1\) ,承重量为 \(\sf V_2\) ,同时有 \(\sf N\) 种物品。
有些物品只有一个,有些物品有 \(\sf S_i\) 个,有些物品有无数个。
第 \(\sf i\) 种物品有体积 \(\sf {W_1}_i\) ,重量 \(\sf {W_2}_i\) ,价值 $ \sf C_i$ 。
现在从 \(\sf N\) 种物品中,任取若干个装入包内,使包里的物品价值总和最大。
可以看到,这里增加了重量这条费用。
那么,我们把我们的 \(\sf Pack\) 升一维吧。
压缩一维:
代码
#include<cstdio>
#include<algorithm>
using namespace std;
int N,V1,V2,W1[1005],W2[1005],C[1005],Pack[1005][1005];
int main()
{
scanf("%d%d%d",&N,&V1,&V2);
for(int i=1;i<=N;i++)
scanf("%d%d%d",&W1[i],&W2[i],&C[i]);
for(int i=1;i<=N;i++)
for(int j1=V1;j1>=W1[i];j1--)
for(int j2=V2;j2>=W2[i];j2--)
Pack[j1][j2]=max(Pack[j1][j2],Pack[j1-W1[i]][j2-W2[i]]+C[i]);
printf("%d",Pack[V1][V2]);
return 0;
}
分组背包
有一个背包容量为 \(\sf V\) ,同时有 \(\sf N\) 组物品。
第 \(\sf i\) 组物品里有 \(\sf S_i\) 个不同的物品,其中每一组的第 \(\sf j\) 个物品有体积 \(\sf W_{ij}\) ,价值 \(\sf C_{ij}\) 。
现在从 \(\sf N\) 组物品中,任取若干个装入包内,使包里的物品价值总和最大。
既然每组只能选一个那我就枚举这组的物品,与上一组的答案迭代,不就好了?
于是就有了:( \(\sf i\) 表示组号, \(\sf j\) 表示容量, \(\sf k\) 表示物品组内编号)
降一维:
代码
#include<cstdio>
#include<algorithm>
using namespace std;
int N,V,S[105],W[105][105],C[105][105],Pack[105];
int main()
{
scanf("%d%d",&N,&V);
for(int i=1;i<=N;i++)
{
scanf("%d",&S[i]);
for(int j=1;j<=S[i];j++)
scanf("%d%d",&W[i][j],&C[i][j]);
}
for(int i=1;i<=N;i++)
for(int j=V;j>=0;j--)
for(int k=1;k<=S[i];k++)
if(j-W[i][k]<0)
continue;
else
Pack[j]=max(Pack[j],Pack[j-W[i][k]]+C[i][k]);
printf("%d",Pack[V]);
return 0;
}
背包问题小变式
依赖背包
有一个背包容量为 \(\sf V\) ,同时有 \(\sf N\) 个物品,第 \(\sf i\) 个物品有体积 \(\sf W_i\) ,价值 $ \sf C_i$ 。
现在从 \(\sf N\) 个物品中,任取若干个装入包内,使包里的物品价值总和最大。
第 \(\sf i\) 件物品必须买了第 \(\sf D_i\) 件物品才能使用。
如果 \(\sf D_i=-1\) ,说明第 \(\sf i\) 件物品可以直接使用。
保证 \(\sf\forall i\) , \(\sf D_{D_i}=-1\) 。
其实就是分组背包。
对于一件被依赖的物品,只有买这件,买这件+它的依赖件和不买这一系列物品三种。
只需写一个迭代的(正着循环的)分组背包即可。
恰好背包
有一个背包容量为 \(\sf V\) ,同时有 \(\sf N\) 个物品,第 \(\sf i\) 个物品有体积 \(\sf W_i\) ,价值 $ \sf C_i$ 。
现在从 \(\sf N\) 个物品中,任取若干个装入包内,问能不能恰好装满。
这个问题有些不一样。
我们把 \(\sf Pack\) 数组换成 bool
,表示是否可以刚好装满。
那么,状态转移方程就变成了:
代码
#include<cstdio>
#include<algorithm>
using namespace std;
int N,V,W[3410],C[3410];
bool ans[3410][13000],Pack[13000];
int main()
{
scanf("%d%d",&N,&V);
for(int i=1;i<=N;i++)
scanf("%d",&W[i]);
Pack[0]=true;
for(int i=1;i<=N;i++)
for(int j=V;j>0;j--)
if(Pack[j-W[i]])
Pack[j]=true,ans[i][j]=true;
puts(Pack[V]?"Yes":"No");
return 0;
}
构造方法
就是输出如何背包。
就是倒着去找这个状态是从哪里迭代而来的。
代码
以恰好背包为例:
#include<cstdio>
#include<algorithm>
using namespace std;
int N,V,W[3410],C[3410];
bool ans[3410][13000],Pack[13000];
void Ans(int i,int j)
{
if(i==0||j==0)
return;
else if(ans[i][j])
Ans(i-1,j-W[i]),
printf("number:%d weight:%d\n",i,W[i]);
else
Ans(i-1,j);
return;
}
int main()
{
scanf("%d%d",&N,&V);
for(int i=1;i<=N;i++)
scanf("%d",&W[i]);
Pack[0]=true;
for(int i=1;i<=N;i++)
for(int j=V;j>0;j--)
if(Pack[j-W[i]])
Pack[j]=true,ans[i][j]=true;
if(!Pack[V]) {puts("not found");return 0;}
else Ans(N,V);
return 0;
}
求方案数
这种问题就是把求最大值换成求和即可。
\(\sf\Large\color{gray}背包完结\)
PCwqyy