动态规划理论:一篇文章带你彻底搞懂最优子结构、无后效性和重复子问题
什么样的问题可以用动态规划解决?解决动态规划问题的一般思考过程是什么样的?
一个模型三个特征理论讲解
动态规划能解决的问题有什么规律可循呢?总结为“一个模型三个特征”
一个模型:动态规划适合解决的问题的模型,该模型定义为“多阶段决策最优解模型”
一般用动态规划来解决最优问题,解决问题的过程,需要经历多个决策阶段,每个决策阶段对应一组状态,然后寻找一组决策序列,经过这组决策序列,能产生最终期望求解的最优解
三个特征:最优子结构、无后效性和重复子问题
1,最优子结构
问题的最优解包含子问题的最优解,即后面阶段的状态可以通过前面阶段的状态推导出来
2.无后效性
推导我们阶段的状态时候,只关心前面阶段的状态值,不关心这个是怎么一步步推导出来的,只有满足前面提到的动态规划问题模型,基本都会满足无后效性
3.重复子问题
不同决策序列,到达某个相同的阶段时,可能会产生重复的状态
“一个模型三个特征”实例剖析
有一个n*
n的矩阵w[n][n]
,矩阵中存储的都是正整数,需要将棋子从左上角移动到右下角,每次只能往右或者向下移动一位,把每条路径经过的数字加起来看做路径的长度,那么从左上角移动到右下角的最短路径长度是多少?
1 3 5 9
2 1 3 4
5 2 6 7
6 8 4 3 (1是起点,3是终点)
这个问题是否符合“一个模型”?
从(0,0)移动到(n-1,n-1)总和走2*(n-1)
步,每个阶段都有向右或者向下走两种决策,每个阶段都会对应一个状态集合,把状态定义为min_dist(i,j),i表示行,j表示列,min_dist(i,j)表达式的值表示从(0,0)到(i,j)的最短路径长度,所以这个问题是一个多阶段决策最优解问题,符合动态规划的模型
初始阶段0:1
阶段1:2 or 3
阶段2:5 or 1 or 5
阶段3:6 or 2 or 3 or 9
阶段4:8 or 6 or 4
阶段5:4 or 7
阶段6:3
是否符合“三个特征”?
用回溯算法解决这个问题,比如写一下代码,画一下递归树,发现递归树中有重复节点,重复节点表示从左上角到节点对应的位置有多种路线,说明这个问题有重复子问题
走到(i,j)这个位置,只需要关心(i-1,j),(i,j-1)两个位置对应的状态,并不关心棋子是通过什么样的路线到达这两个位置的,符合无后效性这个特征
min_dist(i,j)可以通过min_dist(i,j-1)和min_dist(i-1,j)两个状态推导出,符合最优子结构
两种动态规划解题思路总结
把解决动态规划问题一般有两个思路,状态转移表法和状态转移方程法
1.状态转移表法
能用动态规划解决的问题都可以使用回溯算法的暴力搜索解决,当我们拿到问题的时候先用简单的回溯算法解决然后定义状态,每个状态表示一个节点,画出递归树,看是否存在重复子问题,以此来寻找规律,看是否能用动态规划解决
找到重复子问题之后,有两种思路,一是直接用回溯加“备忘录”来避免重复子问题,二是使用动态规划解决方案,状态转移表法
状态转移表法是:先画出一个状态表,表一般是二维的,每个状态包含三个变量,行、列、数组值,根据决策的先后过程,从前往后,分阶段填充每个状态,将这个递推填表的过程,翻译成代码,就是动态规划代码
尽管大部分状态表都是二维的,如果问题的状态比较复杂,需要很多变量来表示,对应的状态表可能就是高维的,那么就不适合用状态转移表法
如何套用这个状态转移表法,来解决之前那个矩阵最短路径的问题?
从起点到终点,有很多不同走法,可以穷举所有走法,对比找出一个最短走法,如何才能无重复又不遗漏穷举所有走法?用回溯算法这个有规律的穷举算法:
private int minDist = Integer.MAX_VALUE; //全局变量或者成员变量
//调用方式:minDistBacktracing(0,0,0,w,n):
public void minDistBT(int i ,int j,int dist,int[][] w ,int n){
//到达了n-1,n-1这个位置
if(i == n && j == n){
if(dist < minDist) minDist = dist;
return;
}
if(i < n ){ //往下走,更新i = i+1;j=j
minDistBT(i + 1,dist+w[i][j],w,n);
}
if(j<n){ // 往右走,更新i=i, j = j +1
minDistBT(i,j+1,dist+w[i][j],w,n);
}
}
有了回溯代码,画出递归树,以此来寻找重复子问题,一个状态包含三个变量(i,j,dist),尽管(i,j,dist)没有重复的,但是(i,j)有很多重复的,对于(i,j)重复的节点,选择dist最小 的节点继续递归求解,其他节点舍弃
是否能用动态规划区解决?
画出二维状态表,行和列代表棋子所在位置,数值表示从起点到这个位置的最短路径
先初始化第0行、第0列
1 4 9 18
3 * * * 第一行:min(3+1,4+1) min(4+3,9+3) min(7+4,18+4)
8 * * * 第二行:min(8+2,4+2) min(6+6,7+6) min(12+7,11+7)
14 * * * 第三行:min(14+8,6+8) min(14+4,12+4) min(16+3,18+3)
所以代码为:
public int minDistDP(int[][] matrix , int n ){
int[][] states = new int[n][n];
int sum = 0 ;
for(int j = 0 ; j < n ; ++j){ //初始化states的第一行数据
sum += matrix[0][j];
states[0][j] = sum;
}
sum = 0;
for(int i = 0 ; i < n ; ++i){ //初始化states的第一列数据
sum += matrix[i][0];
states[i][0] = sum;
}
for(int i = 1;i < n ;++i){
for(int j = 1 ; j < n ; ++j){
states[i][j] = matrix[i][j] +Math.min(states[i][j-1],states[i-1][j]);
}
}
return states[n-1][n-1];
}
2.状态转移方程法
需要分析某个问题如何通过子问题来递归求解,即最优子结构,根据最优子结构,写出递归公式,即状态转移方程,有了方程,代码实现简单了,一种是递归+备忘录,一种是迭代递推
递归方程是;
第一种:递归+备忘录 将方程翻译成代码
private int[][] matrix = {{1,3,5,9},{2,1,3,4},{5,2,6,7},{6,8,4,3}};
private int n = 4;
private int[][] mem = new int[4][4];
public int minDist(int i , int j){ //调用minDist(n-1,n-1)
if( i == 0 && j == 0) return matrix[0][0]
if(mem[i][j] > 0) return mem[i][j];
int minLeft = Integer.MAX_VALUE;
if(j-1 >= 0 ){
minLeft = minDist(i,j-1);
}
int minUp = Integer.MAX_VALUE;
if(i-1 >= 0){
minUp = minDist(i-1,j);
}
int currMinDist = matrix[i][j] + Math.min(minLeft,minUp);
mem[i][j] = currMinDist;
return currMinDist;
}
四种算法思想比较分析
贪心、分治、回溯和动态规划之间的区别和联系
将贪心、回溯、动态规划分为一类,将分治单独分为一类,前三个算法解决问题的模型可以抽象成多阶段决策最优解模型,分治算法解决的问题尽管大部分都是最优解问题,但是并不能抽象成多阶段决策模型
硬币找零问题
有几种不同币值的硬币v1 , v2,……,vn(单位是元),要支付w元,求最少需要多少个硬币,比如,我们有3种不同的硬币,1¥,3¥,5¥,我们要支付9元,最少需要3个硬币(3个3元的硬币)
可以看做爬阶梯问题可以分为1,3,5步,怎么最少走到9步,动态转移方程f(9) = 1+min(f(8),f(6),f(4))
即f(n) = 1 + min(f(n-1) , f(n-3) ,f(n-5))