简述
最近看了一些动态规划的内容,趁着周末,来总结一些,揭一揭这神秘的面纱。
动态规划程序设计是对解最优化问题的一种途径、一种方法。因此读者在学习时,除了要对基本概念和方法正确理解外,必须具体问题具体分析处理,以丰富的想象力去建立模型,用创造性的技巧去求解。我们也可以通过对若干有代表性的问题的动态规划算法进行分析、讨论,逐渐学会并掌握这一设计方法。
何为动态规划
1】百度百科解释:
多阶段决策问题中,各个阶段采取的决策,一般来说是与时间有关的,决策依赖于当前状态,又随即引起状态的转移,一个决策序列就是在变化的状态中产生出来的,故有“动态”的含义,称这种解决多阶段决策最优化问题的方法为动态规划方法。
2】力扣解释
动态规划(英语:Dynamic programming,简称 DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。动态规划往往用于优化递归问题,例如斐波那契数列,如果运用递归的方式来求解会重复计算很多相同的子问题,利用动态规划的思想可以减少计算量。
通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,具有天然剪枝的功能,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。1
说白了,就是将一个大的过程中,拆分成一个一个小过程,每一个小过程中根据状态做出最优选择。
动态规划问题的适用条件
1】最优化原理:
一个最优化策略的子策略总是最优的。
2】无后效性
当前阶段的选择只与当前状态有关,不受以前状态和选择的影响。
3】子问题的重叠性
动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。
动态规划都有哪些类型呢?
动态规划一般可分为线性动规,区域动规,树形动规,背包动规四类。
举例:
线性动规:拦截导弹,合唱队形,挖地雷,建学校,剑客决斗等;
区域动规:石子合并, 加分二叉树,统计单词个数,炮兵布阵等;
树形动规:贪吃的九头龙,二分查找树,聚会的欢乐,数字三角形等;
背包问题:01背包问题,完全背包问题,分组背包问题,二维背包,装箱问题,挤牛奶(同济ACM第1132题)等;
应用实例:
最短路径问题 ,项目管理,网络流优化等;2
解题步骤
说了这么多,当我们遇到动态规划问题的时候,就能判断他是否适用了,那知道以后,我们怎么处理这类问题呢?
1】明确你最终想要的结果是什么(比如收益最大)
2】划分阶段(将大问题分割成子问题)(每个阶段怎么达到收益最大)
3】自底向上的分析最优解,直至边界(边界一般指某些变量可确定)
4】确定状态转移方程
5】根据状态转移方程,编写代码
案例实战
入门案例
给你一个数字三角形, 形式如下:
1
2 3
4 5 6
7 8 9 10
找出从第一层到最后一层的一条路,使得所经过的权值之和最小或者最大.
套用我们的步骤
1】明确你最终想要的结果是什么
求出权值之和最小(这是一个栗子)
2】划分阶段(确定状态和选择)
阶段就是对每层的计算。
状态:该层当前数,
选择:左边,右边
3】自底向上的分析最优解,直至边界
从倒数第二层开始,最后一次选择(决策)必然选择左右两边最小的那个,依次往上推,直到a[0][0] = 1 触碰边界。
4】确定状态转移方程
f[i][j] = a[i][j] + min(f[i+1][j], f[i+1][j+1]) (i <= 层数-1)
(a[i][j] 代表当前位置的值 ,f[i][j]表示指标函数)
状态方程的含义是:当前位置做出选择后的值 = 当前位置的值 + 左右两个最小的值。
5】根据状态转移方程,编写代码
核心代码:
for(int i=n-1;i>=1;--i)//从倒数第二行开始
{
for(int j=1;j<=i;j++)
{
if(a[i+1][j][1]>a[i+1][j+1][1])//左边大
{
a[i][j][2]=0;//选择左边,用于记录路径
a[i][j][1]+=a[i+1][j][1];
}
else//右边大
{
a[i][j][2]=1;//选择右边
a[i][j][1]+=a[i+1][j+1][1];
}
}
}
简单案例–爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
套用我们的步骤
1】明确你最终想要的结果是什么
求出到达第n阶总方法数
2】划分阶段(确定状态和选择)
阶段:每一阶为一个阶段
状态:到达第i阶
选择:爬1阶,爬2阶
3】自底向上的分析最优解,直至边界
想要达到第n阶,只有两种情况,要么1步上来,要么2步上来。
边界:到达1阶,只有1种方法
4】确定状态转移方程
dp[i] = dp[i-1] + dp[i-2] (i >=2)
dp[1] = 1
状态方程的含义是:到达i阶总方法数 = 1步上来之前的总方法数 + 2步上来之前的总方法数
5】根据状态转移方程,编写代码
public int ClimbStairs(int n) {
if(n == 1) return 1;
int[] dp = new int[n + 1];
dp[1] = 1;
for(int i = 2; i <= n; i++)
{
dp[i] = dp[i-2] + dp[i-1];
}
return dp[n];
}
题外话:
这个问题可以继续优化,比如,我们只需要记录n-1 和 n-2 方法数即可,也就是说空间复杂度可以降低到O(1)。
当然还有更多的优化方法,有兴趣的可以去力扣官网查看该题的所有解法,这里只讨论动态规划的解法。
总结
遇到动态规划问题,首先切割阶段,在小阶段寻找状态和选择,自底向上,边界是状态转换方程的基石。记录每个阶段每个状态每个选择的结果,最终返回你需要的结果。
最后,记得去掉每个阶段的冗余数据,只保存需要的数据即可。
综上,动态转换方程就是要确定全局最优解和最优子结构之间的关系,以及问题的边界。
曾经看到过一个万能动态转换方程框架
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 择优(选择1,选择2...)
算法无止境,愿诸君努力而行。