五种常用算法之二:动态规划算法

Posted Will_Don

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了五种常用算法之二:动态规划算法相关的知识,希望对你有一定的参考价值。

动态规划算法:

基本思想:

  动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式

   动态规划算法与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)


 

应用场景:

适用动态规划的问题必须满足最优化原理、无后效性和重叠性。
1.最优化原理(最优子结构性质) 最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。

2.无后效性  将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。

3.子问题的重叠性  动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。


 

动态规划算法经典案例:

  案例一:

  有n级台阶,一个人每次上一级或者两级,问有多少种走完n级台阶的方法。

  分析:动态规划的实现的关键在于能不能准确合理的用动态规划表来抽象出 实际问题。在这个问题上,我们让f(n)表示走上n级台阶的方法数。

  那么当n为1时,f(n) = 1,n为2时,f(n) =2,就是说当台阶只有一级的时候,方法数是一种,台阶有两级的时候,方法数为2。那么当我们要走上n级台阶,必然是从n-1级台阶迈一步或者是从n-2级台阶迈两步,所以到达n级台阶的方法数必然是到达n-1级台阶的方法数加上到达n-2级台阶的方法数之和。即f(n) = f(n-1)+f(n-2),我们用dp[n]来表示动态规划表,dp[i],i>0,i<=n,表示到达i级台阶的方法数。

  

 1 public class CalculationSteps {
 2     //动态规划表,用来记录到达i级台阶的方法数
 3     public static int[] steps = new int[11];
 4     
 5     public static void main(String[] args) {
 6         steps[10] = calStep(10);
 7         
 8         for (int i = 0; i < steps.length; i++) {
 9             System.out.print(steps[i]+" ");
10         }
11         System.out.println();
12         System.out.println(steps[10]);
13     }
14     
15     //计算到达i级台阶的方法数
16     public static int calStep(int n){
17         //如果为第一级台阶或者第二级台阶 则直接返回n
18         if(n==1||n==2){
19             return n;
20         }
21         //计算到达n-1级台阶的方法数
22         if(steps[n-1]==0){
23             steps[n-1] = calStep(n-1);
24         }
25         //计算到达n-2级台阶的方法数
26         if(steps[n-2] == 0){
27             steps[n-2] = calStep(n-2);
28         }
29         //到达第n级台阶=到达n-1级台阶+到达n-2级台阶
30         return steps[n-1]+steps[n-2];
31     }
32 }

运行结果如下:

0 1 2 3 5 8 13 21 34 55 89 
89

案例2:

给定一个矩阵m,从左上角开始每次只能向右走或者向下走,最后达到右下角的位置,路径中所有数字累加起来就是路径和,返回所有路径的最小路径和,如果给定的m如下,那么路径1,3,1,0,6,1,0就是最小路径和,返回12.

1 3 5 9

8 1 3 4

5 0 6 1

8 8 4 0

分析:对于这个题目,假设m是m行n列的矩阵,那么我们用dp[m][n]来抽象这个问题,dp[i][j]表示的是从原点到i,j位置的最短路径和。我们首先计算第一行和第一列,直接累加即可,那么对于其他位置,要么是从它左边的位置达到,要么是从上边的位置达到,我们取左边和上边的较小值,然后加上当前的路径值,就是达到当前点的最短路径。然后从左到右,从上到下依次计算即可。

Java代码实现:

 1 /**
 2  * 给定一个矩阵m,从左上角开始每次只能向右走或者向下走
 3  * 最后达到右下角的位置,路径中所有数字累加起来就是路径和,
 4  * 返回所有路径的最小路径和
 5  */
 6 public class MinSteps {
 7     
 8     public static int[][] steps=new int[4][4];
 9     
10     public static void main(String[] args) {
11         int[][] arr = {{4,1,5,3},{3,2,7,7},{6,5,2,8},{8,9,4,5}};
12         steps[3][3] = minSteps(arr, 3, 3);
13         print(steps);
14     }
15     
16     
17     public static int minSteps(int[][] arr,int row,int col){
18         //如果为起始位置,则直接返回
19         if(row==0&&col==0){
20             steps[row][col] = arr[row][col];
21             return steps[row][col];
22         }
23         
24         //计算到arr[row][col]的左面位置的值
25         if(col>=1&&steps[row][col-1]==0){
26             steps[row][col-1]=minSteps(arr, row, col-1);
27         }
28         //计算到arr[row][col]的上面位置的值
29         if(row>=1&&steps[row-1][col]==0){
30             steps[row-1][col]=minSteps(arr, row-1, col);
31         }
32         //如果为第一行,则直接加左面位置上的值
33         if(row==0&&col!=0){
34             steps[row][col] = arr[row][col]+steps[row][col-1];
35         }else if(col == 0&&row!=0){
36             //如果为第一列,则直接加上上面位置上的值
37             steps[row][col] = arr[row][col]+steps[row-1][col];
38         }else{
39             //比较到达左面位置和到达上面位置的值的大小,加上两者的最大值
40             steps[row][col] =arr[row][col]+min(steps[row][col-1],steps[row-1][col]);
41         }
42         return steps[row][col];
43     }
44     
45     private static int min(int minSteps, int minSteps2) {
46         return minSteps>minSteps2?minSteps:minSteps2;
47     }
48 
49 
50     static void print(int[][] arr){
51         for (int i = 0; i < arr.length; i++) {
52             for (int j = 0; j < arr[i].length; j++) {
53                 System.out.println("到达arr["+i+"]["+j+"]的最大路径:"+arr[i][j]);
54             }
55         }
56     }
57 }

运行结果:

到达arr[0][0]的最大路径:4
到达arr[0][1]的最大路径:5
到达arr[0][2]的最大路径:10
到达arr[0][3]的最大路径:13
到达arr[1][0]的最大路径:7
到达arr[1][1]的最大路径:9
到达arr[1][2]的最大路径:17
到达arr[1][3]的最大路径:24
到达arr[2][0]的最大路径:13
到达arr[2][1]的最大路径:18
到达arr[2][2]的最大路径:20
到达arr[2][3]的最大路径:32
到达arr[3][0]的最大路径:21
到达arr[3][1]的最大路径:30
到达arr[3][2]的最大路径:34
到达arr[3][3]的最大路径:39

 案例3:最长公共子序列问题

  最长公共子序列问题是要找到两个字符串间的最长公共子序列。假设有两个字符串sudjxidjs和xidjxidpolkj,其中djxidj就是他们的最长公共子序列。许多问题都可以看成是公共子序列的变形。例如语音识别问题就可以看成最长公共子序列问题。

  假设两个字符串分别为A=a1a2..am,B=b1b2..bn,则m为A的长度,n为B的长度。那么他们的最长公共子序列分为两种情况。

  1、am=bn,这时他们的公共子序列一定为的长度F(m,n)=F(m-1,n-1)+am

  2、am≠bn,这时他们的公共子序列一定为的长度F(m,n)=Max(F(m-1,n),F(m,n-1))

  

  1 /**
  2  * 求两个字符串之间的最长子序列
  3  */
  4 public class MaxCommonStr {
  5     // 数组用来存储两个字符串的最长公共子序列
  6     public static String[][] result = new String[10][15];
  7 
  8     public static void main(String[] args) {
  9         String strA = "sudjxidjs";
 10         String strB = "xidjxidpolkj";
 11         System.out.println(maxCommonStr(strA, strB));
 12         // System.out.println(strA.charAt(strA.length()-1));
 13     }
 14 
 15     /**
 16      * 获取两个字符串的最大公共子序列
 17      * 
 18      * @param strA
 19      * @param strB
 20      * @return
 21      */
 22     public static String maxCommonStr(String strA, String strB) {
 23         // 分别获取两个字符串的长度
 24         int lenA = strA.length();
 25         int lenB = strB.length();
 26 
 27         // 如果字符串strA的长度为1,那么如果strB包含字符串strA,则公共子序列为strA,否则为null
 28         if (lenA == 1) {
 29             if (strB.contains(strA)) {
 30                 result[lenA - 1][lenA - 1] = strA;
 31             } else {
 32                 result[lenA - 1][lenA - 1] = "";
 33             }
 34             return result[lenA - 1][lenA - 1];
 35         }
 36 
 37         // 如果字符串strB的长度为1,那么如果strA包含字符串strB,则公共子序列为strB,否则为null
 38         if (lenB == 1) {
 39             if (strA.contains(strB)) {
 40                 result[lenA - 1][lenA - 1] = strB;
 41             } else {
 42                 result[lenA - 1][lenA - 1] = "";
 43             }
 44             return result[lenA - 1][lenA - 1];
 45         }
 46 
 47         // 如果字符串strA的最后一位和strB的最后一位相同的话,
 48         if (strA.charAt(lenA - 1) == strB.charAt(lenB - 1)) {
 49             //先判断数组result[lenA - 2][lenB - 2] == null,这样可以减少一些重复运算
 50             if (result[lenA - 2][lenB - 2] == null) {
 51                 //求strA和strB都去除最后一位剩余字符串的最大公共子序列f
 52                 result[lenA - 2][lenB - 2] = maxCommonStr(strLenSub(strA), strLenSub(strB)) ;
 53             }
 54             //strA和strB的最大公共子序列就是他们各去除最后一位剩余字符串的最大公共子序列+strA或者strB的最后一位
 55             result[lenA-1][lenB-1] = result[lenA - 2][lenB - 2]+ strA.charAt(lenA - 1);
 56         } else {
 57             //否则
 58             if (result[lenA - 2][lenB-1] == null) {
 59                 //计算strA去除最后一位后和strB的最大子序列
 60                 result[lenA - 2][lenB-1] = maxCommonStr(strLenSub(strA), strB);
 61             }
 62             if (result[lenA-1][lenB - 2] == null) {
 63                 //计算strB去除最后一位后和strA的最大子序列
 64                 result[lenA-1][lenB - 2] = maxCommonStr(strA, strLenSub(strB));
 65             }
 66             //等于result[lenA - 2][lenB-1]和result[lenA-1][lenB - 2]中的最大数
 67             result[lenA-1][lenB-1] = max(result[lenA - 2][lenB-1], result[lenA-1][lenB - 2]);
 68         }
 69         return result[lenA-1][lenB-1];
 70     }
 71     
 72     /**
 73      * 使字符串去除最后一位,返回该新的字符串
 74      * @param str
 75      * @return
 76      */
 77     public static String strLenSub(String str) {
 78         return str.substring(0, str.length() - 1);
 79     }
 80 
 81     /**
 82      * 比较两个字符串长度,返回最长字符串 当两个字符串长度相等时,返回任意字符串
 83      * 
 84      * @param strA
 85      * @param strB
 86      * @return
 87      */
 88     public static String max(String strA, String strB) {
 89         if (strA == null && strB == null) {
 90             return "";
 91         } else if (strA == null) {
 92             return strB;
 93         } else if (strB == null) {
 94             return strA;
 95         }
 96         if (strA.length() > strB.length()) {
 97             return strA;
 98         } else {
 99             return strB;
100         }
101     }
102 }

运行结果:

djxidj

 

 

写在最后:
  此篇随笔仅用来记录我的学习内容,如有错误,欢迎指正。谢谢!!!

以上是关于五种常用算法之二:动态规划算法的主要内容,如果未能解决你的问题,请参考以下文章

五种常用的算法设计技巧之二:分治算法

五大常用算法:分治动态规划贪心回溯和分支界定

动态规划(Dynamic Programming)——算法三十六计之二

动态规划学习

五大常用算法之一:动态规划算法

五大常用算法 动态规划算法