动态规划(DP:Dynamic Programming)
动态规划是求解包含重复子问题的最优化方法,把原问题分解为相对简单的子问题。动态规划只能应用于有最优子结构的问题(即局部最优解能决定全局最优解,或问题能分解成子问题来求解)。
基本思想
将原问题分解为相似的子问题,再合并子问题的解以得出原问题的解。 动态规划在求解的过程中通过保存子问题的解求出原问题的解(而不是简单地分而治之),仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。 这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
动态规划 = 记忆化搜索
分治与动态规划
共同点:二者都要求原问题具有最优子结构性质,都是将原问题分而治之,分解成若干个规模较小(小到很容易解决的程序)的子问题.然后将子问题的解合并,形成原问题的解.
不同点:分治法将分解后的子问题看成相互独立的,通过用递归来做。
动态规划将分解后的子问题理解为相互间有联系,有重叠部分,需要记忆,通常用迭代来做。
问题特征
最优子结构:当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。
重叠子问题:在用递归算法自顶向下解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只解一次,而后将其解保存在一个表格中,在以后尽可能多地利用这些子问题的解。
递推求解问题就是最简单的一类动规问题,状态转移方程就是递推方程。
动规解题的一般思路
1.将原问题分解为子问题
- 把原问题分解为若干个子问题,子问题经常和原问题形式类似,有时甚至完全一样,只不过规模变小了。
- 子问题的解一旦求出就会被保存,所以每个子问题只需求解一次。
2.确定状态
- 将和子问题相关的各个变量的一组取值,称之为一个状态。一个状态对应于一个或多个子问题。
- 所谓某个状态下的“值”,就是这个状态对应的子问题的解。
- 所有状态的集合构成问题的“状态空间”。“状态空间”的大小,与用动态规划解题的时间复杂度直接相关。
- 用动态规划解题,经常遇到的情况是:K个整型变量能构成一个状态,这K个整型变量的取值范围是N1,N2...Nk,那么,我们可以用一个K维数组array[N1][N2]...[Nk]来存储各个状态的值。这个值未必是整数或者浮点数,也可能需要一个结构体才能表示,那么array就可以是一个结构数组。
- 一个“状态”下的“值”通常会是一个或多个子问题的解。
3.确定状态转移方程
- 定义出什么是“状态”,以及在该”状态“下的”值“后,就要找出不同状态之间如何迁移->即如何从一个或多个”值“已知的”状态“,求出另一个”状态“的”值“。
- 状态的迁移可以用递推公式表示,此递推公式也可被称作“状态转移方程”。
典型问题
最长递增子序列
问题:设L=<a1,a2,…,aN>是n个不同的实数的序列,L的递增子序列是这样一个子序列LK=<ai1,ai2,…,aiK>,其中i1<i2<…<iK且ai1<ai2<…<aiK。求最大的K值。
步骤1-找子问题:发现以ak(k=1,2,3...N)为终点的最长递增子序列的长度是个好的子问题。将一个递增子序列最右边的数,称为该子序列的“终点”。虽然和原问题形式不太一样,但只要这N个子问题都解决了,N个子问题的解中最大的就是整个问题的解。
步骤2-确定状态:上述子问题只和一个变量有关,就是数字的位置。因此序列中数的位置k就是“状态”。状态k对应的“值”,就是以ak为“终点”的最长递增子序列的长度。该问题的状态一共有N个。
步骤3-确定状态转移方程:状态定义出来后,转移方程就不难想了。假定MaxLen(k)表示以ak为“终点”的最长递增子序列的长度,那么
- MaxLen(1) = 1
- MaxLen(k) = Max{ MaxLen(i): 1<i<k 且 ai<ak 且 k≠1 } +1
这个转移方程的意思就是,MaxLen(k)的值,就是在ak左边,“终点”数值小于ak,且长度最大的那个上升子序列的长度再加1。因为ak左边任何“终点”小于ak的子序列,加上ak后就能形成一个更长的子序列。
实际实现的时候不用编写递归函数,因为从MaxLen(1)就能推算出MaxLen(2),进而持续递推。
示例代码:(题目选自刘家瑛老师课程PPT)
#include<cstdio> const int MAX = 1010; int b[MAX]; int MaxLen[MAX]; int main() { int N; scanf("%d", &N);//读入序列的长度N for (int i = 1; i <= N; i++) scanf("%d", &b[i]);//读入序列 MaxLen[1] = 1;//初始化MaxLen for (int i = 2; i <= N; i++)//每次求以第i个数为终点的最长递增子序列的长度 { int maxL = 0;//记录满足条件的,第i个数左边的递增子序列的最大长度 for (int j = 1; j < i; j++)//查看以第j个数为终点的最长递增子序列,实现状态转移 { if (b[i] > b[j])//ai>aj { if (maxL < MaxLen[j]) maxL = MaxLen[j];//找到满足ai>aj且最长的递增子序列的长度 } } MaxLen[i] = maxL + 1; } int ans = -1; for (int i = 1; i <= N; i++) { if (ans < MaxLen[i]) ans = MaxLen[i]; } printf("%d ", ans); return 0; }
最终总结
学习历程:枚举-->搜索(系统化)-->动态规划(记忆化)
搜索的实现
方式1:递归-剪枝
- 整个搜索过程中,最终状态始终不变。
- 不要考虑明显不能达到最终状态的路径。
方式2:动态规划
目的:
- 在搜索过程中,把计算的结果保留下来。
- 后面的搜索过程中,努力使用前面搜索过程的结果,避免重复计算。
方法:
- 把最终目标分解成一些相对简单的目标。
- 先实现这些相对简单的目标,在此基础上实现最终的目标。
具体使用哪种方式?依情况而定
- 没有什么重复的计算可使用:递归,为保持简洁。
- 重复的计算占得比重很大:动态规划,为提高效率。