【算法】动态规划详解

Part 1 什么是动态规划

首先我们需要明确一点,什么是动态规划?

将一个问题分解为若干规模较小的子问题,通过求出子问题并保存子问题的解,然后再通过子问题来推导出原问题的解,这种算法就叫动态规划。

那怎么分辨动态规划算法与其他类似算法的区别呢?

你需要知道这三点:

1.最优子结构
2.状态转移方程
3.无后效性

这就是

动态规划的性质

什么,你还是分不清?

请看Part 2。


Part 2 动态规划与常见算法的区别

动态规划vs递推

动态规划的代码与递推算法是非常相似的,很多人会分不清,包括作者本人

DP的一个重要特征就是需要保存状态,而且需要保存之前的状态,不一定是上一个状态,DP的状态可能需要使用很多次。

而递推只需要保存上一个状态,且一般只需要用到上一次的状态。

动态规划vs分治

这两个算法的共同点在于都需要分解为具备最优子结构的子问题,但分治一般分解为两个子问题。

就算分解为多个子问题,那这些子问题也是并列关系。

DP就不一样了,它的子问题关系更复杂,可以是重叠的、并列的或者互相包含的。


Part 3 线性DP和区间DP

让我们先来看一道题:

有一个棋盘,共n行,m列,有一个卒子,从A(0,0)点出发,每次只能往右或往下移动一步,求从A点到B(n,m)点的路径条数。

解题思路:
定义状态f(x,y),表示从起点走到(x,y)的路径条数。则最终状态为f(n,m),起始状态为f(0,0).
动态转移方程为:f(x,y)=f(x-1,y)+f(x,y-1).

代码如下:

int f[MAXN][MAXN];
int main(){
	int n,m;
	cin>>n>>m;
	for(int i=0;i<=n;i++)f[i][0]=1;
	for(int i=0;i<=m;i++)f[0][i]=1;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			f[i][j]=f[i-1][j]+f[i][j-1];
	cout<<f[n][m]<<endl;
}

由这道题的解题思路再推广到这一类题的解题思路,总结得出:

在动态规划中,解题的关键通常是两步:
第一步:定义好状态
第二步:找出状态转移方程。(注意,找状态转移方程的时候要找出初始状态)

线性DP例题 最长上升子序列

设有由n个整数组成的数列,任意删掉若干数后,剩下的部分称为子序列。
如果子序列是从左到右依次递增的,则称为上升子序列。求该数列的最长上升子序列的长度。

线性DP例题 解题过程

定义状态f(i)表示以i结束的最长上升序列的长度。
则状态转移方程为:
f(i)=max{f(j)+1) |j<i,a[j]<a[i] }
原问题的解则为max{f[i] | 0<=i<n }

这道题还可以拓展一下:如果要求输出方案,如何处理?

由于方案有可能存在多种,请输出最靠前的一种。

上题中我们已求出所有的数的f值,设其中最大的为f[i],则a[i] 即为最长上升子序列的尾数,

从a[i]处往左边找,若a[j]<a[i]并且f[j]=f[i]-1,则a[j]必为序列的倒数第二个;

从j再往前找k,使得a[k]<a[j]并且f[k]=f[j]-1,即a[k]为序列的倒数第三个,以此类推,即可找到最长上升序列中的所有数。

此题再次拓展,难度再次增加:如何求出最长上升子序列有多少个?

在上一题基础上,增加一个g数组,g[i]表示以a[i]结尾的最长不下降子序列的个数。则g[i]=sum(g[j]) {j|j<i&&a[j]<=a[i]&&f[j]=f[i]-1}.
则ans=sum(g[i]) {此处的g[i]要满足:f[i]最大}

区间DP例题 石子合并

设有n堆石子排成一排,其编号为1,2,3,…,n。每堆石子有一定的数量,例如: 13 7 8 16 21 4 18 现要将n堆石子归并为一堆。归并的过程为每次只能将相邻的两堆石子堆成一堆,这样经过n-1次归并之后最后成为一堆。对于上面的7堆石子,可以有多种方法归并成一堆。归并的代价是这样定义的:将两堆石子归并为一堆时,两堆石子数量的和称为归并2堆石子的代价。如上图中,将13和7归并为一堆的代价为20。归并的总代价指的是将沙子全部归并为一堆沙子的代价的和。如上面的2种归并方法中, 第1种的总代价为 20+24+25+44+69+87 = 267 第2种的总代价为 15+37+22+28+59+87 = 248 由此可见,不同归并过程得到的总的归并代价是不一样的。 当n堆石子的数量给出后,找出一种合理的归并方法,使总的归并代价为最小。

输入
第1行:1个整数n(1<=n<=100),表示石子的数量第
2行:n个用空格分开的整数,每个整数均小于10000,表示各堆石子的数量。
输出
第1行:1个整数,表示最小的归并代价
第2行:用括号表示的归并顺序。加括号的要求见样例。如果只有1堆石子,输出时不要加括号。
样例输入
3 13 7 8
样例输出
43 (13)((7)(8))

区间DP例题 石子合并

这是一个经典的区间动态规划问题。
首先本题不能使用贪心。因为题中限定了必须是相邻的两堆才能合并。
从最后一次合并开始思考:
最后一次合并前,石子肯定只有两堆,第一堆是原来的1到k堆,第二堆是原来的第k+1到第n堆。显然,要使得总代价最小,必然是该两堆式子之前的合并代价最小。
定义状态f[i] [j]表示第i堆石子到第j堆石子的最小合并代价。则目标状态即为f[1][n]。
状态转移方程为:
f[i] [j]=min(f[i] [k]+f[k+1] [j])+sum(i,j)
初始状态为f[i] [i]=0

注意状态转移方程
f[i] [j]=min(f[i] [k]+f[k+1] [j])+sum(i,j)
求f[i] [j]时,必须保证f[i] [k]、f[k+1] [j]已经算出,但是k+1比i大。所以循环的顺序应当是i递减,j递增。
或者换状态。
以f[i] [len]表示从i开始的长度为len的石子合并的最小代价。
则f[i] [len]=min(f[i] [k]+f[i+k] [len-k]+sum(I,i+len-1))

题目中需要输出合并的方案。
所以求f[i] [j]的同时,要把k值也保存下来。
可以另开一个数组pos[i] [j],将k值保存在pos[i] [j]中。输出时,从pos[1] [n]开始递归输出找k值,然后输出答案。


Part 4 资源分配

工作分配问题

总公司拥有高效生产设备M台,准备分给下属的N个公司。各分公司若获得这些设备,可以为国家提供一定的盈利。问:如何分配这M台设备才能使国家得到的盈利最大?求出最大盈利值。其中M<=15,N<=10。分配原则:每个公司有权获得任意数目的设备,但总台数不得超过总设备数M
数据文件格式为:第一行保存两个数,第一个数是设备台数M,第二个数是分公司数N。接下来是一个M*N的矩阵,表明了第i个公司分配j台机器的盈利。

这种问题就是经典的资源分配问题,定义状态f[i] [j]表示前i个公司分配j台机器的最大盈利。

则状态转移方程为:
f(i,j)=Max{f(i-1,k)+value(i,j-k)} (1<=i<=n,1<=j<=m,0<=k<=j )
初始值: f(0,0)=0
目标状态为f[n] [m]
时间复杂度O(N·M·M)


Part 5 多维DP

有时候我们会遇到一些状态表示较为复杂的题,其维度达到或超过3维,这类题我们一般称之为多维dp。
对于多维dp,要估算其时间复杂度和空间复杂度,保证其均不超过题目要求。否则,一定要努力降维,寻求优化。

比如这一道非常经典的真题:

NOIP2008 传纸条

小渊和小轩是好朋友也是同班同学,他们在一起总有谈不完的话题。一次素质拓展活动中,班上同学安排做成一个m行n列的矩阵,而小渊和小轩被安排在矩阵对角线的两端,因此,他们就无法直接交谈了。幸运的是,他们可以通过传纸条来进行交流。纸条要经由许多同学传到对方手里,小渊坐在矩阵的左上角,坐标(1,1),小轩坐在矩阵的右下角,坐标(m,n)。从小渊传到小轩的纸条只可以向下或者向右传递,从小轩传给小渊的纸条只可以向上或者向左传递。 在活动进行中,小渊希望给小轩传递一张纸条,同时希望小轩给他回复。班里每个同学都可以帮他们传递,但只会帮他们一次,也就是说如果此人在小渊递给小轩纸条的时候帮忙,那么在小轩递给小渊的时候就不会再帮忙。反之亦然。 还有一件事情需要注意,全班每个同学愿意帮忙的好感度有高有低(注意:小渊和小轩的好心程度没有定义,输入时用0表示),可以用一个0-100的自然数来表示,数越大表示越好心。小渊和小轩希望尽可能找好心程度高的同学来帮忙传纸条,即找到来回两条传递路径,使得这两条路径上同学的好心程度只和最大。现在,请你帮助小渊和小轩找到这样的两条路径。

输入
第一行有2个用空格隔开的整数m和n,表示班里有m行n列(1≤m,n≤50) 接下来的m行是一个m*n的矩阵,矩阵中第i行j列的整数表示坐在第i行j列的学生的好心程度。每行的n个整数之间用空格隔开。
输出
共一行,包含一个整数,表示来回两条路上参与传递纸条的学生的好心程度之和的最大值
样例输入
3 3
0 3 9
2 8 5
5 7 0
样例输出
34

题目要求找两条来回的最大值路径,也就相当于找两条从左上角同时出发,到达右下角的路径,再简单点说,就是两个人同时从左上角出发到右下角。

首先定义状态为

f[s][i1][i2]

表示甲走到第i1行,乙走到第i2行,且甲乙各走了s步时,两人所取得的数之和的最大值。

则状态转移方程为:
f[s][i1][i2]=max(f[s-1][i1][i2],f[s-1][i1-1][i2-1],f[s-1][i1-1][i2],f[s-1][i1][i2-1])+num[s+2-i1]+num[i2][s+2-i2];
if(i1==i2)f[s][i1][i2]-=num[i1][s+2-i1];
初始状态f[0][1][1]=num[1][1]
目标状态f[m+n-2][m][n]
 m,n表示表格的行数、列数。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值