动态规划及其应用

Posted 三月旅途

tags:

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

1 前言

动态规划(Dynamic Programming)是运筹学的一个分支,是求解决策过程最优化的过程。

1.1  三个特征

能够使用动态规划思想来解决的问题,需要符合如下三个特征:  

  • 最优子结构:对于多阶段决策问题,如果每一个阶段的最优决策序列的子序列也是最优的,称其具有最优子结构。

  • 无后效性:当前阶段的最优解只与它上个阶段的最优解有关,它不关心上个阶段的最优解是怎么得来的,之后阶段的决策(最优解)也不会影响之前阶段问题的求解。

  • 重复子问题: 如果问题采用自顶向下的方式解决,通常都会有重复子问题的,即我们所说的递归重复,此时我们可以采用自下而上的套路来求解问题。

1.2  三类问题

动态规划一般可以分为如下三类问题:

  • 区间规划

    dp[i][j]表示第i个位置到第j个位置的最优解,最终结果一般在dp[0][n-1]。见问题:回文子串、回文子序列、公共子序列、最小编辑距离。

  • 线性规划

    采用dp[i]或者dp[i][j]形式,表示从开始到i为止,或者从起点到(i,j)点的最优解。

  • 约束规划

    常见问题:01背包问题。


2 区间规划

2.1 LCD5 最长回文字串

public static String longestPalindrome(String s) { if(s == null || s.length() <= 1) return s; String res = ""; int size = s.length(); boolean[][] dp = new boolean[size][size]; //dp[i][j]表示i到j是否是回文 for(int i = size-1; i >= 0; i--){ dp[i][i] = true; if(res.length() == 0) res = s.substring(i,i+1); for(int j= i + 1; j < size ; j++) if( s.charAt(i) == s.charAt(j) && ( j-i<=1 || dp[i+1][j-1])){ dp[i][j] = true; if(j-i >= res.length()) res = s.substring(i,j+1); } } return res; }

2.2 LCD647 回文子窜个数

public int countSubstrings(String s) { int res = 0; if( s == null ) return res; int size = s.length(); boolean[][] dp = new boolean[size][size]; // dp[i][j]表示i到j是否是回文 for(int i = size -1; i >=0; i--) { dp[i][i] = true; res ++; for (int j = i + 1; j <= size - 1; j++){ if( s.charAt(i) == s.charAt(j) && (j - i <= 1 || dp[i+1][j-1])) { dp[i][j] = true; res++; } } } return res;}

2.3 LCD516 最长回文子序列

public int longestPalindromeSubseq(String s) { if (s == null || s.length() == 0) return 0; int len = s.length(); int[][] dp = new int[len][len]; //dp[i][j]表示从i到j的回文序列长度 for (int i = len - 1; i >= 0; i--) { dp[i][i] = 1; for (int j = i + 1; j < len; j++) { if (s.charAt(i) == s.charAt(j)) dp[i][j] = dp[i + 1][j - 1] + 2; else dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]); } } return dp[0][len - 1];}

2.4 LCD1143 最长公共子序列

public int longestCommonSubsequence(String text1, String text2) { if( text1 == null || text1.length() == 0 || text2 == null || text2.length() == 0) return 0; int size1 = text1.length(); int size2 = text2.length(); int[][] dp = new int[size1][size2]; // dp[i][j]表示text1[0...i]和text2[0...j]的最长公共子序列长度 //初始化 for(int i = 0; i < size1; i++) if(text1.substring(0,i+1).indexOf(text2.charAt(0)) != -1) dp[i][0] = 1; for(int j = 0; j < size2; j++) if(text2.substring(0,j+1).indexOf(text1.charAt(0)) != -1) dp[0][j] = 1; //状态转移 for(int i = 1; i< size1; i++) for(int j = 1; j <size2; j++) if( text1.charAt(i) == text2.charAt(j)) dp[i][j] = dp[i-1][j-1] + 1; else dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]); return dp[size1-1][size2-1];}

2.5 LCD748 最长公共子数组

public int findLength(int[] A, int[] B) { if( A == null || A.length == 0 || B == null || B.length == 0) return 0; int size1 = A.length; int size2 = B.length; int[][] dp = new int[size1][size2]; // dp[i][j]表示A[0...i]和B[0...j]的最长公共子数组长度,若A[i]!=B[j],则dp[i][j]==0 int res = 0; //初始化 for(int i = 0; i < size1; i++) if( A[i] == B[0]) { dp[i][0] = 1; res = 1;} for(int j = 0; j < size2; j++) if( B[j] == A[0]) { dp[0][j] = 1; res = 1;} //状态转移 for(int i = 1; i< size1; i++) for(int j = 1; j <size2; j++) if( A[i] == B[j]){ dp[i][j] = dp[i-1][j-1] + 1; res = Math.max(dp[i][j],res); } return res;}

为了便于理解,也可以参考下图,连续红色对角的最大长度即为所求。

                      

最长公共子窜和最长公共子数组解法类似,LeetCode并没有对应的原题,这里给出解法如下:

public int longestCommonString(String A, String B) { if( A == null || A.length() == 0 || B == null || B.length() == 0) return 0; int size1 = A.length(); int size2 = B.length(); int[][] dp = new int[size1][size2]; // dp[i][j]表示A[0...i]和B[0...j]的最长公共子窜,若A.charAt(i)!= B.charAt(j),则dp[i][j]==0 int res = 0; //初始化 for(int i = 0; i < size1; i++) if( A.charAt(i) == B.charAt(0)) { dp[i][0] = 1; res = 1;} for(int j = 0; j < size2; j++) if( B.charAt(j) == A.charAt(0)) { dp[0][j] = 1; res = 1;} //状态转移 for(int i = 1; i< size1; i++) for(int j = 1; j <size2; j++) if( A.charAt(i) == B.charAt(j)){ dp[i][j] = dp[i-1][j-1] + 1; res = Math.max(dp[i][j],res); } return res;}

2.6 LCD72 最小编辑距离

//word1变为word2的最小操作数public int minDistance(String word1, String word2) { if (word1 == null || word1.length() == 0) return word2.length(); if (word2 == null || word2.length() == 0) return word1.length(); int[][] dp = new int[word1.length() + 1][word2.length() + 1]; // "", "ABCD" //dp[j][i]表示长度为j的word1和长度为i的word2之间的最小编辑距离 for (int j = 1; j <= word1.length(); j++) dp[j][0] = j; for (int i = 1; i <= word2.length(); i++) dp[0][i] = i; //状态转移 for (int j = 1; j <= word1.length(); j++) for (int i = 1; i <= word2.length(); i++) { if( word1.charAt(j-1) == word2.charAt(i-1)) dp[j][i] = dp[j-1][i-1]; else dp[j][i] = Math.min(Math.min(dp[j - 1][i], dp[j][i - 1]), dp[j - 1][i - 1]) + 1; } return dp[word1.length()][word2.length()];}

3 线性规划

3.1 LCD53 连续子数组最大和

public int maxSubArray(int[] nums) { int size = nums.length; if( size == 0) return 0; int[] dp = new int[size];  dp[0] = nums[0];//dp[i]表示以第i元素为结尾序列的最大的连续子数组和 int max = nums[0];//保存dp[i]的最大值 for(int i=1;i<size;i++){ dp[i] = Math.max(dp[i-1]+nums[i],nums[i]); max=Math.max(max,dp[i]);  } return max; }

3.2 LCD300 最长上升子序列

public int lengthOfLIS(int[] nums) { int size = nums.length; int[] dp= new int[size]; //dp[i]表示i为结尾的最长上升序列的长度 Arrays.fill(dp,1); //初始化为1 int res = 0; for(int i = 0 ; i < size; i++){ for(int j = 0 ;j < i; j++){ if( nums[i] > nums[j]) dp[i] = Math.max(dp[i],dp[j] +1); //dp[i]=max{dp[j]+1,dp[i]}(1<=j<i,nums[j]<A[i]) } res = Math.max(res,dp[i]); } return res;}

3.3 LCD673 最长上升子序列个数

public int findNumberOfLIS(int[] nums) { int size = nums.length; int[] dp = new int[size]; //dp[i]表示i为结尾的最长上升序列的长度 int[] ct = new int[size]; //ct[i]表示i为结尾的最长子序列个数 Arrays.fill(dp, 1); //初始化为1 Arrays.fill(ct, 1); //初始化为1 int res = 0; for (int i = 0; i < size; i++) { for (int j = 0; j < i; j++) if (nums[i] > nums[j]) { if (dp[i] == dp[j] + 1) { ct[i] += ct[j]; } else if (dp[j] +1 > dp[i]) { dp[i] = dp[j] + 1; ct[i] = ct[j]; } } res = Math.max(res, dp[i]); } int count = 0; for (int i = 0; i < size; i++) if (dp[i] == res) count += ct[i]; return count;}

3.4  LCD62 不同路径

public int uniquePaths(int m, int n) { int[][] dp = new int[m][n]; //dp[i][j]表示从(0,0)到达(i,j)的不通路径数 for(int i = 0 ; i < m; i++) dp[i][0] = 1; for(int j = 0; j < n; j++) dp[0][j] = 1; for(int i = 1; i < m; i++) for(int j = 1; j < n; j++){ dp[i][j]= dp[i][j-1] + dp[i-1][j]; } return dp[m-1][n-1];}

3.5 LCD63 不同路径2

public int uniquePathsWithObstacles(int[][] obstacleGrid) { int m = obstacleGrid.length; int n = obstacleGrid[0].length; int[][] dp = new int[m][n]; //dp[i][j]表示从(0,0)到达(i,j)的不通路径数 if(obstacleGrid[0][0] == 0) dp[0][0]=1; for(int i = 1 ; i < m; i++) if(obstacleGrid[i][0] == 0) dp[i][0] = dp[i-1][0]; //初始化第0 for(int j = 1; j < n; j++) if(obstacleGrid[0][j] == 0) dp[0][j] = dp[0][j-1]; //初始化第0 for(int i = 1; i < m; i++) //从第1行开始 for(int j= 1; j< n; j++){ //从第1列开始 if(obstacleGrid[i][j] == 0) dp[i][j]= dp[i][j-1] + dp[i-1][j]; } return dp[m-1][n-1];}


4 总结

运用动态规划解决问题的难点在于采用何种数据结构保存什么的结果,以及利用保存结果推导出状态转移方程。要想掌握好动态规划解题技巧,还是需要多练习多总结才行。本文主要目的是为了便于个人理解和总结,也欢迎指正。


以上是关于动态规划及其应用的主要内容,如果未能解决你的问题,请参考以下文章

动态规划及其应用

宿舍小讲堂 || 动态规划算法及其应用

动态规划整数划分及其变种

详细实例说明+典型案例实现 对动态规划法进行全面分析 | C++

动态规划及其优化

关于动态规划算法,哪位可以讲一下自己心得体会?