关于动态规划内容很多,这里只列举一些我觉都有意思的题目。
二维简单动态规划 最小路径和 leetcode 64
思路:状态转移方程:f[i][j] = Math.min(f[i-1][j],f[i][j-1]) + grid[i][j] 因为一个位置只能从上或者左转移过来,所以状态转移方程就是这个样子的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public int minPathSum (int [][] grid) { int n = grid.length; int m = grid[0 ].length; int [] dp = new int [m]; dp[0 ] = grid[0 ][0 ]; for (int i=1 ;i<m;i++){ dp[i] = grid[0 ][i]+dp[i-1 ]; } for (int i = 1 ;i<n;i++){ dp[0 ]+=grid[i][0 ]; for (int j = 1 ;j<m;j++){ dp[j] = Math.min(dp[j-1 ],dp[j]) + grid[i][j]; } } return dp[m-1 ]; }
最长公共子序列 leetcode 1143
思路:状态转移方程:f[i][j] = if(s1[i]==s2[j]) f[i-1][j-1]+1 else max(f[i-1][j],f[i][j-1])
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public int longestCommonSubsequence (String text1, String text2) { char [] s1 = text1.toCharArray(); char [] s2 = text2.toCharArray(); int n = s1.length; int m = s2.length; int [][] dp = new int [n+1 ][m+1 ]; for (int i=1 ;i<=n;i++){ for (int j = 1 ;j<=m;j++){ if (s1[i-1 ] == s2[j-1 ]){ 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[n][m]; }
最长回文子序列 leetcode 516
思路:第一种思路,把原字符串翻转,然后求最长子序列。
第二种思路,二维dp,记录从l到r的回文子序列的长度。 初始化dp[i][i] = 1;,dp[i][i+1] = s[i] == s[i+1] ? 2 : 1; 每个格子依赖于左下,下,左边的格子。于是dp的顺序就是从下到上,从左到右。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public int longestPalindromeSubseq (String s) { int n = s.length(); char [] c = s.toCharArray(); int [][] dp = new int [n][n]; for (int l = n-1 ;l>=0 ;l--){ dp[l][l] = 1 ; if (l+1 <n){ dp[l][l+1 ] = c[l] == c[l+1 ] ? 2 : 1 ; } for (int r = l+2 ;r<n;r++){ if (c[r] == c[l]){ dp[l][r] = dp[l+1 ][r-1 ]+2 ; }else { dp[l][r] = Math.max(dp[l+1 ][r],dp[l][r-1 ]); } } } return dp[0 ][n-1 ]; }
不同的子序列 leetcode 115
思路:dp[i][j] 表示s1的前i个字符,s2的前j个字符,有多少个不同的子序列。 那么当s1[i]==s2[j] 时,dp[i][j] = dp[i-1][j-1]+dp[i-1][j] 否则dp[i][j] = dp[i-1][j] 然后初始化dp[i][0] = 1;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public int numDistinct (String s, String t) { int m = s.length(), n = t.length(); int [][] dp = new int [m + 1 ][n + 1 ]; for (int i = 0 ; i <= m; i++) dp[i][0 ] = 1 ; for (int i = 0 ; i < m; i++) { for (int j = 0 ; j <= Math.min(i,n-1 ); j++) { if (s.charAt(i) == t.charAt(j)) { dp[i+1 ][j+1 ] = dp[i][j] + dp[i][j+1 ]; } else { dp[i+1 ][j+1 ] = dp[i][j+1 ]; } } } return dp[m][n]; } public int numDistinct (String s, String t) { int m = s.length(), n = t.length(); int [] dp = new int [n + 1 ]; for (int i = 1 ; i <= m; i++) { dp[0 ] = 1 ; for (int j = Math.min(i,n); j >=1 ; j--) { if (s.charAt(i-1 ) == t.charAt(j-1 )) { dp[j] = dp[j-1 ] + dp[j]; } } } return dp[n]; }
编辑距离 leetcode 72 在此基础上新增了每种操作的代价。
思路:dp[i][j]含义为需要多少代价才能使得s1前i个字符和s2前j个字符相等。当s1[i] == s2[j]时,dp[i][j] = dp[i-1][j-1] 此时不用任何操作。如果不相等,就可以进行三种操作。
插入s1[i], 但需要s1前i个字符和s2前j-1个字符相等。就是dp[i][j-1]。最后加上插入代价 + a 删除s1[i], 但需要s1前i-1个字符和s2前j个字符相等。就是dp[i-1][j]。最后加上删除代价 + b 替换s1[i], 但需要s1前i-1个字符和s2前j-1个字符相等。就是dp[i-1][j-1]。最后加上替换代价 + c 初始化时dp[0][j] = j a(因为从空字符串到s2只能插入); dp[i][0] = i b(从s1到空字符串只能删除);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public static int editDistance2 (String str1, String str2, int a, int b, int c) { char [] s1 = str1.toCharArray(); char [] s2 = str2.toCharArray(); int n = s1.length; int m = s2.length; int [][] dp = new int [n + 1 ][m + 1 ]; for (int i = 1 ; i <= n; i++) { dp[i][0 ] = i * b; } for (int j = 1 ; j <= m; j++) { dp[0 ][j] = j * a; } for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= m; j++) { if (s1[i - 1 ] == s2[j - 1 ]) { dp[i][j] = dp[i - 1 ][j - 1 ]; } else { dp[i][j] = Math.min(Math.min(dp[i - 1 ][j] + b, dp[i][j - 1 ] + a), dp[i - 1 ][j - 1 ] + c); } } } return dp[n][m]; } private int fff (char [] s1, char [] s2, int a, int b, int c) { int n = s1.length; int m = s2.length; int [] dp = new int [m+1 ]; for (int i=1 ;i<=m;i++){ dp[i] = i*a; } for (int i=1 ;i<=n;i++){ int leftup = dp[0 ] ,backup; dp[0 ] = i*b; for (int j=1 ;j<=m;j++){ backup = dp[j]; if (s1[i-1 ] == s2[j-1 ]){ dp[j] = leftup; }else { dp[j] = Math.min(Math.min(dp[j-1 ]+a,dp[j]+b),leftup+c); } leftup = backup; } } return dp[m]; }
交错字符串 leetcode 97
思路: dp[i][j]表示s1的前i个字符和s2的前j个字符是否可以交错组成s3的前i+j个字符。然后判断
如果s1[i-1] == s3[i+j-1],说明是s1提供了最后一个字符,那么此时我就保证s1的前i-1个字符和s2的前j个字符可以交错组成s3的前i+j-1个字符。即dp[i-1][j]; 如果s2[j-1] == s3[i+j-1],说明是s2提供了最后一个字符,那么此时我就保证s1的前i个字符和s2的前j-1个字符可以交错组成s3的前i+j-1个字符。即dp[i][j-1]; 初始化: dp[i][0] 表示 s2不参与拼接,就是纯靠s1去拼接s3的前i个字符。 dp[0][j] 表示 s1不参与拼接,就是纯靠s2去拼接s3的前j个字符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public static boolean isInterleave1 (String str1, String str2, String str3) { if (str1.length() + str2.length() != str3.length()) { return false ; } char [] s1 = str1.toCharArray(); char [] s2 = str2.toCharArray(); char [] s3 = str3.toCharArray(); int n = s1.length; int m = s2.length; boolean [][] dp = new boolean [n + 1 ][m + 1 ]; dp[0 ][0 ] = true ; for (int i = 1 ; i <= n; i++) { if (s1[i - 1 ] != s3[i - 1 ]) { break ; } dp[i][0 ] = true ; } for (int j = 1 ; j <= m; j++) { if (s2[j - 1 ] != s3[j - 1 ]) { break ; } dp[0 ][j] = true ; } for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= m; j++) { dp[i][j] = (s1[i - 1 ] == s3[i + j - 1 ] && dp[i - 1 ][j]) || (s2[j - 1 ] == s3[i + j - 1 ] && dp[i][j - 1 ]); } } return dp[n][m]; }
子数组和相关问题 子数组最大和 leetcode 53
思路:dp[i]表示以i为结尾的子数组的和最大是多少。那么dp[i] = Math.max(dp[i-1]+nums[i],nums[i]);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static int maxSubArray1 (int [] nums) { int n = nums.length; int [] dp = new int [n]; dp[0 ] = nums[0 ]; int ans = nums[0 ]; for (int i = 1 ; i < n; i++) { dp[i] = Math.max(nums[i], dp[i - 1 ] + nums[i]); ans = Math.max(ans, dp[i]); } return ans; } public static int maxSubArray2 (int [] nums) { int n = nums.length; int ans = nums[0 ]; for (int i = 1 , pre = nums[0 ]; i < n; i++) { pre = Math.max(nums[i], pre + nums[i]); ans = Math.max(ans, pre); } return ans; }
循环数组中子数组的最大和 leetcode 918
思路:求子数组最大和跟最小和即可。用sum - minSum 就是分开的子数组的最大和。 但需要特判一下是不是全是负数的情况.
1 2 3 4 5 6 7 8 9 10 11 public int maxSubarraySumCircular (int [] nums) { int maxSum = nums[0 ] ,minSum = nums[0 ],preMax=nums[0 ],preMin=nums[0 ],sum=nums[0 ]; for (int i = 1 ;i< nums.length;i++){ sum+=nums[i]; preMax = Math.max(preMax+nums[i],nums[i]); maxSum = Math.max(maxSum,preMax); preMin = Math.min(preMin+nums[i],nums[i]); minSum = Math.min(minSum,preMin); } return sum == minSum ? maxSum : (Math.max(maxSum, sum - minSum)); }
打家劫舍2 leetcode 213
思路:分开讨论即可,要么不偷第一家,在2-n之间偷,要么偷第一家,在1-n-1之间偷。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public int rob2 (int [] nums) { int n = nums.length; if (n==1 ){ return nums[0 ]; } int [] dp1 = new int [n+1 ]; int [] dp2 = new int [n+1 ]; dp1[0 ] = 0 ; dp1[1 ] = 0 ; for (int i = 2 ;i<=n;i++){ dp1[2 ] = Math.max(dp1[i-1 ],dp1[i-2 ]+nums[i-1 ]); } dp2[0 ] = 0 ; dp2[1 ] = nums[0 ]; for (int i = 2 ;i<n;i++){ dp2[i] = Math.max(dp2[i-1 ],dp2[i-2 ]+nums[i-1 ]); } return Math.max(dp1[n],dp2[n-1 ]); }
打家劫舍4 leetcode 2560
思路:二分+dp。 转化为给定能力,小偷最多偷多少家即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public int minCapability (int [] nums, int k) { int n = nums.length; int max = 0 ; for (int i=0 ;i<n;i++){ max = Math.max(nums[i],max); } int r = max; int l=0 ; int ans = r; while (l<=r){ int mid = l + ((r-l)>>1 ); if (f(nums,mid,k)){ ans = mid; r = mid -1 ; }else { l = mid + 1 ; } } return ans; } public boolean f (int [] nums,int cap,int k) { int n = nums.length; int [] dp = new int [n+1 ]; dp[0 ] = 0 ; dp[1 ] = cap >= nums[0 ] ? 1 : 0 ; for (int i=2 ;i<=n;i++){ if (nums[i-1 ] > cap){ dp[i] = dp[i-1 ]; }else { dp[i] = Math.max(dp[i-1 ],dp[i-2 ]+1 ); } if (dp[i] >= k){ return true ; } } return dp[n] >= k; }
乘积最大子数组 leetcode 152
思路:dp1[i] 表示以i为结尾的子数组乘积最大是多少。dp2[i] 表示以i为结尾的子数组乘积最小是多少。 每次正在一个位置比较最小值乘以自己,最大值乘以自己,跟自己的大小即可。并且更新dp1[i],dp2[i]。 代码直接给出空间压缩版本.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public int maxProduct (int [] nums) { int n = nums.length; int min = nums[0 ]; int max = nums[0 ]; int ans = nums[0 ]; int temp; for (int i = 1 ;i<n;i++){ temp = max; temp = Math.max(Math.max(max*nums[i],min*nums[i]),nums[i]); min = Math.min(Math.min(min*nums[i],max*nums[i]),nums[i]); max = temp; ans = Math.max(max,ans); } return ans; }
被7整除的最大子序列和 思路:dp[i][j] 表示以第i个数为结尾,模数为j的子序列和最大是多少。那么dp[i][j] = dp[i-1][j] 或者 dp[i-1][(j+7-nums[i-1]%7)%7] + nums[i]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static int maxSum2 (int [] nums) { int n = nums.length; int [][] dp = new int [n + 1 ][7 ]; dp[0 ][0 ] = 0 ; for (int j = 1 ; j < 7 ; j++) { dp[0 ][j] = -1 ; } for (int i = 1 , x, cur, need; i <= n; i++) { x = nums[i - 1 ]; cur = nums[i - 1 ] % 7 ; for (int j = 0 ; j < 7 ; j++) { dp[i][j] = dp[i - 1 ][j]; need = cur <= j ? (j - cur) : (j - cur + 7 ); if (dp[i - 1 ][need] != -1 ) { dp[i][j] = Math.max(dp[i][j], dp[i - 1 ][need] + x); } } } return dp[n][0 ]; }
可以翻转1次的情况下子数组最大累加和 给定一个数组nums,
现在允许你随意选择数组连续一段进行翻转,也就是子数组逆序的调整
比如翻转[1,2,3,4,5,6]的[2~4]范围,得到的是[1,2,5,4,3,6]
返回必须随意翻转1次之后,子数组的最大累加和
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public static int maxSumReverse2 (int [] nums) { int n = nums.length; int [] start = new int [n]; start[n - 1 ] = nums[n - 1 ]; for (int i = n - 2 ; i >= 0 ; i--) { start[i] = Math.max(nums[i], nums[i] + start[i + 1 ]); } int ans = start[0 ]; int end = nums[0 ]; int maxEnd = nums[0 ]; for (int i = 1 ; i < n; i++) { ans = Math.max(ans, maxEnd + start[i]); end = Math.max(nums[i], end + nums[i]); maxEnd = Math.max(maxEnd, end); } ans = Math.max(ans, maxEnd); return ans; }
最长递增子序列相关题目 最长递增子序列 leetcode 300
思路:最基础的复杂度O(n^2),优化版本
维护一个ends[]数组,表示ends[i]表示长度为i+1的递增子序列的最小末尾元素。那么以后我要新增一个元素,那么我需要二分找到大于等于这个元素的最左位置,然后更新这个位置的元素为这个元素。如果没找到,那么就插入到数组末尾。并且len++。最后返回len即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public static int lengthOfLIS (int [] nums) { int n = nums.length; int [] ends = new int [n]; int len = 0 ; for (int i = 0 , find; i < n; i++) { find = bs1(ends, len, nums[i]); if (find == -1 ) { ends[len++] = nums[i]; } else { ends[find] = nums[i]; } } return len; } public static int bs1 (int [] ends, int len, int num) { int l = 0 , r = len - 1 , m, ans = -1 ; while (l <= r) { m = (l + r) / 2 ; if (ends[m] >= num) { ans = m; r = m - 1 ; } else { l = m + 1 ; } } return ans; }
俄罗斯套娃信封 leetcode 354
思路:先将宽高进行排序,按宽度升序,高度降序。然后对于高度求最长递增子序列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public int maxEnvelopes (int [][] envelopes) { Arrays.sort(envelopes,(a,b) -> a[0 ] != b[0 ] ? a[0 ] - b[0 ] : b[1 ] - a[1 ]); int n = envelopes.length; int [] ends = new int [n]; int len = 0 ; for (int i=0 ;i<n;i++){ int find = bs1(ends,len,envelopes[i][1 ]); if (find == -1 ){ ends[len++] = envelopes[i][1 ]; }else { ends[find] = envelopes[i][1 ]; } } return len; } private int bs1 (int [] ends, int len, int target) { int l=0 ,r=len-1 ,ans=-1 ; while (l<=r){ int mid = l + ((r-l)>>1 ); if (ends[mid] >= target){ ans = mid; r = mid-1 ; }else { l = mid+1 ; } } return ans; }
最长对数链 leetcode 646
思路:每个数对按第一个数降序排列。然后求最长递增子序列。但是注意,这里在二分查找的时候,查找跟放入的不是一个数。按开始位置进行查找,但是放入自己的结束位置,所以不能直接放入,应该判断跟原位置的大小(因为这里只能知道开始的值是小于等于找到的位置的,结束值跟这个位置的大小关系不确定)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 private int bs1 (int [] ends, int len, int target) { int l=0 ,r=len-1 ,ans=-1 ; while (l<=r){ int mid = l + ((r-l)>>1 ); if (ends[mid] >= target){ ans = mid; r = mid-1 ; }else { l = mid+1 ; } } return ans; } public int findLongestChain (int [][] pairs) { Arrays.sort(pairs,(a,b)-> a[0 ] - b[0 ]); int n = pairs.length; int [] ends = new int [n]; int len = 0 ; for (int i=0 ;i<n;i++){ int find = bs1(ends,len,pairs[i][0 ]); if (find == -1 ){ ends[len++] = pairs[i][1 ]; }else { ends[find] = Math.min(pairs[i][1 ],ends[find]); } } return len; }