Loading

动态规划

<目录

动态规划 \(\sf\color{gray}Dynamic\ Programming\)

由来

动态规划是怎么来的?
我想,应该从递归说起……

数字三角形 Number Triangles

观察下面的数字金字塔。
写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。

\[\begin{matrix} \sf 7\\ \sf 3\ 8\\ \sf 8\ 1\ 0\\ \sf 2\ 7\ 4\ 4\\ \sf 4\ 5\ 2\ 6\ 5 \end{matrix} \]

在上面的样例中,从 \(\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][j]=\max\{Pack[i-1][j],Pack[i-1][j-w_1]+c_i\} \]

可能有点费空间,怎么办呢?
注意 \(\sf Pack[i][m]\) 只与 \(\sf Pack[i-1][n]\) 有关。

所以可以用滚动数组自然继承:
\(\sf Pack[j]\) 表示当前物品装进 \(\sf j\) 容量的背包所能取到的最大价值。

\[\sf Pack[j]=\max\{Pack[j],Pack[j-w_i]+c_i\} \]

代码

#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]=\max\{Pack[i-1][j],Pack[i-1][j-w_i]+c_i,Pack[i][j-w_i]+c_i\} \]

这是在干什么?
这是在迭代啊。
\(\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 \begin{aligned} n&=6\\ &=1+2+3\\ &=2^0+2^1+3 \end{aligned} \]

而任意的数量都可以用 \(\sf 2^0\)\(\sf 2^1\)\(\sf 3\) 拼出来:

\[\def\arraystretch{1.5} \begin{array}{|c|c|c|c|c|c|c|} \hline \sf 0&\sf 1&\sf 2&\sf 3&\sf 4&\sf 5&\sf 6\\\hline \sf\varnothing&\sf 2^0&\sf 2^1&\sf 3&\sf 2^0+3&\sf 2^1+3&\sf 2^0+2^1+3\\\hline \end{array} \]

代码

#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\) 升一维吧。

\[\sf Pack[i][j_1][j_2]=\max\{Pack[i-1][j_1][j_2],Pack[i][j_1-{W_1}_i][j_2-{W_2}_i]+C_i\} \]

压缩一维:

\[\sf Pack[j_1][j_2]=\max\{Pack[j_1][j_2],Pack[j_1-{W_1}_i][j_2-{W_2}_i]+C_i\} \]

代码

#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\) 表示物品组内编号)

\[\sf Pack[i][j]=\max\{Pack[i-1][j],Pack[i-1][j-W_{ik}]+C_{ik}\} \]

降一维:

\[\sf Pack[j]=\max\{Pack[j],Pack[j-W_{ik}]+C_{ik}\} \]

代码

#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 ,表示是否可以刚好装满。
那么,状态转移方程就变成了:

\[\sf Pack[j]= \begin{cases} \sf true&\sf j=0\\ \sf Pack[j-W_i]&\sf j\not=0 \end{cases} \]

代码
#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}背包完结\)

posted @ 2022-10-18 15:57  PCwqyy  阅读(19)  评论(0)    收藏  举报  来源