递归 VS 动态规划,这里通过数三角形问题来引入递归与动态规划的区别
递归 + 记忆化搜索 —> 动态规划。
分治法与递归实现的动态规划的区别:有没有重复计算。
使用动态规划的几种极可能情况:
1.求最大值和最小值
2.判断是否可行
3.统计方案个数
很大可能不使用动态规划的情况:
1.求出所有具体方案(搜索)
2.输入时一个集合而不是一个序列,也就是交换顺序对结果没有影响
3.暴力已经是多项式级别的
DP擅长的是把暴力 2^n ->n^2 n! ->n^3
动态规划四要素:
1.状态
2.初始化
3.方程
4.答案
一般面试中常见动态规划问题可以分为:
坐标型DP
序列型DP
双序列型DP
划分型DP
背包型DP
区间型DP
接下来分别看一看各个类型的DP
数字三角形
给定一个数字三角形,找到从顶部到底部的最小路径和。每一步可以移动到下面一行的相邻数字上。这里记录n为数字三角形的行数。
方法一:traverse。相当于找出所有的路径,而路径数一共有2^n 条。所以很自然,这种方法超时了。
1 private int best = Integer.MAX_VALUE; 2 /** 3 * @param triangle: a list of lists of integers. 4 * @return: An integer, minimum path sum. 5 */ 6 public int minimumTotal(int[][] triangle) { 7 //traverse 8 if (triangle == null || triangle.length == 0) { 9 return -1; 10 } 11 traverse(triangle, 0, 0, triangle[0][0]); 12 return best; 13 } 14 //从[0,0]走到[x,y]路径只和为sum 15 public void traverse(int[][] triangle, int x, int y, int sum) { 16 if (x == triangle.length - 1) { 17 best = Math.min(best, sum); 18 return; 19 } 20 traverse(triangle, x + 1, y, sum + triangle[x + 1][y]); 21 traverse(triangle, x + 1, y + 1, sum + triangle[x + 1][y + 1]); 22 }
方法二:Divider & Conquer。复杂度仍然是2^n,每次走都有两个选择,并没有任何阻拦,所以仍然超时。
1 public int minimumTotal(int[][] triangle) { 2 //traverse 3 if (triangle == null || triangle.length == 0) { 4 return -1; 5 } 6 return dividerConquer(triangle, 0, 0); 7 8 } 9 //从[x,y]出发,走到最底层,最短路径是多少 10 public int dividerConquer(int[][] triangle, int x, int y) { 11 if (x == triangle.length) { 12 return 0; 13 } 14 int left = dividerConquer(triangle, x + 1, y); 15 int right = dividerConquer(triangle, x + 1, y + 1); 16 return Math.min(left, right) + triangle[x][y]; 17 }
方法三:观察Divider & Conquer方法,发现存在许多重复计算。所以这里存在很大的优化空间。假如我们把计算过的结果记忆,那么在此遍历到这个点的时候就不在往下分治。
记忆化搜索,使用hash[i][j]来记录(i,j)到最底层的最短路径,在此遍历到的时候直接返回即可。由于每个节点就计算一次,所以这里复杂度变为 n^2。
1 public int minimumTotal(int[][] triangle) { 2 //traverse 3 if (triangle == null || triangle.length == 0) { 4 return -1; 5 } 6 Integer[][] hash = new Integer[triangle.length][triangle.length]; 7 for (int i = 0; i < hash.length; i++) { 8 for (int j = 0; j <= i; j++) { 9 hash[i][j] = Integer.MAX_VALUE; 10 } 11 } 12 return dividerConquer(triangle, 0, 0, hash); 13 14 } 15 //从[x,y]出发,走到最底层,最短路径是多少 16 public int dividerConquer(int[][] triangle, int x, int y,Integer[][] hash) { 17 if (x == triangle.length) { 18 return 0; 19 } 20 if (hash[x][y] != Integer.MAX_VALUE) { 21 return hash[x][y]; 22 } 23 int left = dividerConquer(triangle, x + 1, y, hash); 24 int right = dividerConquer(triangle, x + 1, y + 1, hash); 25 hash[x][y] = Math.min(left, right) + triangle[x][y]; 26 return hash[x][y]; 27 }
方法四:多重循环实现DP。自上而下。需要特别注意的是,需要单独初始化边界。最左边只能从上边来。最右边只能从左上来。
1 public int minimumTotal(int[][] triangle) { 2 //traverse 3 if (triangle == null || triangle.length == 0) { 4 return -1; 5 } 6 //dp[i][j]表示从(0,0)出发,到达(i,j)的最短路径 7 int len = triangle.length; 8 int[][] dp = new int[len][len]; 9 dp[0][0] = triangle[0][0]; 10 for (int i = 1; i < len; i++) { 11 dp[i][0] = dp[i-1][0] + triangle[i][0]; 12 dp[i][i] = dp[i -1][i - 1] + triangle[i][i]; 13 } 14 for (int i = 1; i < len ; i++) { 15 for (int j = 1; j < i; j++) { 16 dp[i][j] = Math.min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle[i][j]; 17 } 18 } 19 int result = Integer.MAX_VALUE; 20 for (int j = 0; j < len ;j++) { 21 result = Math.min(result, dp[len - 1][j]); 22 } 23 return result; 24 }
方法五:多重循环DP。自下而上
1 public int minimumTotal(int[][] triangle) { 2 //traverse 3 if (triangle == null || triangle.length == 0) { 4 return -1; 5 } 6 //dp[i][j]表示(i,j)到最底层的最短路径 7 int len = triangle.length; 8 int [][] dp = new int[len][len]; 9 for (int j = 0; j < len; j++) { 10 dp[len - 1][j] = triangle[len - 1][j]; 11 } 12 for (int i = len - 2; i >=0; i--) { 13 for (int j = 0; j <= i; j++) { 14 dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle[i][j]; 15 } 16 } 17 return dp[0][0]; 18 }
坐标型动态规划
最小路径和
给定一个只含非负整数的m*n网格,找到一条从左上角到右下角的可以使数字和最小的路径。
1 public int minPathSum(int[][] grid) { 2 // write your code here 3 if (grid == null || grid.length == 0) { 4 return -1; 5 } 6 int m = grid.length; 7 int n = grid[0].length; 8 //dp[i][j]表示从grid[0][0]走到(i,j)的最短路径 9 int[][] dp = new int[m][n]; 10 dp[0][0] = grid[0][0]; 11 for (int i = 1; i < m; i++) { 12 dp[i][0] = dp[i-1][0] + grid[i][0]; 13 } 14 for (int j = 1; j < n; j++) { 15 dp[0][j] = dp[0][j - 1] + grid[0][j]; 16 } 17 for (int i = 1; i < m; i++) { 18 for (int j = 1; j < n; j++) { 19 dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]; 20 } 21 } 22 return dp[m - 1][n - 1]; 23 }
不同的路径
有一个机器人的位于一个M×N个网格左上角(下图中标记为'Start')。
1 public int uniquePaths(int m, int n) { 2 // write your code here 3 //dp[i][j]表示从(0,0)到(i,j)一共有多少种不同的路径 4 int[][] dp = new int[m][n]; 5 for (int i = 0; i < m; i++) { 6 dp[i][0] = 1; 7 } 8 for (int j = 1; j < n; j++) { 9 dp[0][j] = 1; 10 } 11 for (int i = 1; i < m; i++) { 12 for (int j = 1; j < n; j++) { 13 dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; 14 } 15 } 16 return dp[m - 1][n - 1]; 17 }
不同的路径 II
现在考虑网格中有障碍物,那样将会有多少条不同的路径?
网格中的障碍和空位置分别用 1 和 0 来表示。
1 public int uniquePathsWithObstacles(int[][] obstacleGrid) { 2 // write your code here 3 if (obstacleGrid == null || obstacleGrid.length == 0) { 4 return 0; 5 } 6 int m = obstacleGrid.length; 7 int n = obstacleGrid[0].length; 8 int[][] dp = new int[m][n]; 9 for (int i = 0; i < m; i++) { 10 if (obstacleGrid[i][0] == 1) { 11 break; 12 } 13 else { 14 dp[i][0] = 1; 15 } 16 } 17 for (int j = 0; j < n; j++) { 18 if (obstacleGrid[0][j] == 1) { 19 break; 20 } 21 else { 22 dp[0][j] = 1; 23 } 24 } 25 for (int i = 1; i < m; i++) { 26 for (int j = 1; j < n; j++) { 27 if (obstacleGrid[i][j] == 1) { 28 dp[i][j] = 0; 29 } 30 else { 31 dp[i][j] = dp[i][j - 1] + dp[i - 1][j]; 32 } 33 } 34 } 35 return dp[m - 1][n - 1]; 36 }
爬楼梯
假设你正在爬楼梯,需要n步你才能到达顶部。但每次你只能爬一步或者两步,你能有多少种不同的方法爬到楼顶部?
1 public int climbStairs(int n) { 2 // write your code here 3 if (n < 0) { 4 return 0; 5 } 6 if (n == 0) { 7 return 1; 8 } 9 //dp[i]表示到达第n层一共有多少种不同方法 10 int[] dp = new int[n + 1]; 11 dp[0] = 1; 12 dp[1] = 1; 13 for (int i = 2; i <= n; i++) { 14 dp[i] = dp[i - 1] + dp[i - 2]; 15 } 16 return dp[n]; 17 }
滚动数组优化:
1 public int climbStairs(int n) { 2 if (n <= 0) { 3 return 0; 4 } 5 if (n <= 2) { 6 return n; 7 } 8 int[] dp = new int[2]; 9 dp[0] = 1; 10 dp[1] = 2; 11 for (int i = 2; i < n; i++) { 12 dp[i % 2] = dp[(i - 1) % 2] + dp[(i - 2) % 2]; 13 } 14 return dp[(n - 1) % 2]; 15 }
爬楼梯II
一个小孩爬一个 n 层台阶的楼梯。他可以每次跳 1 步, 2 步 或者 3 步。实现一个方法来统计总共有多少种不同的方式爬到最顶层的台阶。
public int climbStairs2(int n) { // Write your code here if (n == 0) { return 1; } if (n == 1) { return 1; } if (n == 2) { return 2; } if (n == 3) { return 4; } int[] dp = new int[n + 1]; dp[1] = 1; dp[2] = 2; dp[3] = 4; for (int i = 4; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3]; } return dp[n]; }
跳跃游戏
给出一个非负整数数组,你最初定位在数组的第一个位置。
数组中的每个元素代表你在那个位置可以跳跃的最大长度。
判断你是否能到达数组的最后一个位置。
方法一:DP
1 public boolean canJump(int[] A) { 2 // wirte your code here 3 //DP 4 if (A == null || A.length == 0) { 5 return false; 6 } 7 //dp[i]表示能否到达i位置 8 boolean[] dp = new boolean[A.length]; 9 dp[0] = true; 10 for (int i = 1; i < A.length; i++) { 11 for (int j = 0; j < i; j++) { 12 if (dp[j] && j + A[j] >= i) { 13 dp[i] = true; 14 break; 15 } 16 } 17 } 18 return dp[A.length - 1]; 19 }
方法二:贪心
1 public boolean canJump(int[] A) { 2 // wirte your code here 3 //贪心 4 if (A == null || A.length == 0) { 5 return false; 6 } 7 int max = 0;//从开始能到达的最远位置 8 for (int i = 0; i < A.length; i++) { 9 if (i > max) { 10 return false; 11 } 12 max = Math.max(max, i + A[i]); 13 if (max >= A.length - 1) { 14 return true; 15 } 16 } 17 return false; 18 }
跳跃游戏 II
给出一个非负整数数组,你最初定位在数组的第一个位置。
数组中的每个元素代表你在那个位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
方法一:DP
1 public int jump(int[] A) { 2 // write your code here 3 if (A == null || A.length == 0) { 4 return 0; 5 } 6 //dp[i]表示从开始位置到达i位置的最少跳跃次数 7 int[] dp = new int[A.length]; 8 for (int i = 1; i < A.length; i++) { 9 dp[i] = Integer.MAX_VALUE; 10 } 11 dp[0] = 0; 12 for (int i = 1; i < A.length; i++) { 13 for (int j = 0; j < i; j++) { 14 if (dp[j] != Integer.MAX_VALUE && j + A[j] >= i) { 15 dp[i] = Math.min(dp[i], dp[j] + 1); 16 } 17 } 18 } 19 return dp[A.length - 1]; 20 }
方法二:贪心
public int jump(int[] nums) { if (nums == null || nums.length == 0) { return 0; } int edge = 0; // 当前步数可以到达哪儿 int nextReach = nums[0]; // 当前步数加1最多可以的到达哪儿 int step = 0; int n = nums.length; for (int i = 1; i < n; i++) { if (i > edge) { step++; edge = nextReach; if (edge >= n - 1) { return step; } } nextReach = Math.max(nextReach, nums[i] + i); if (nextReach == i) { return Integer.MAX_VALUE; } } return step; }
最长上升子序列
给定一个整数序列,找到最长上升子序列(LIS),返回LIS的长度。最长上升子序列问题是在一个无序的给定序列中找到一个尽可能长的由低到高排列的子序列,这种子序列不一定是连续的或者唯一的。
注意可以是任意位置开始,任意位置结束
方法一:DP O(n^2)
1 public int longestIncreasingSubsequence(int[] nums) { 2 // write your code here 3 if (nums == null || nums.length == 0) { 4 return 0; 5 } 6 //注意可以从任意位置开始任意位置结束 7 8 //dp[i]表示必须以i位置结尾的最长上升子序列的长度 9 10 int[] dp = new int[nums.length]; 11 int max = 1; 12 for (int i = 0; i < nums.length; i++) { 13 dp[i] = 1; 14 } 15 for (int i = 1; i < nums.length; i++) { 16 for (int j = 0; j < i; j++) { 17 if (nums[j] < nums[i]) { 18 dp[i] = Math.max(dp[j] + 1, dp[i]); 19 } 20 } 21 max = Math.max(max, dp[i]); 22 } 23 return max; 24 }
方法二:BS O(nlogn)
public int lengthOfLIS(int[] nums) { // write your code here if (nums == null || nums.length == 0) { return 0; } //h[i]表示遍历到当前时刻为止,长度为i+1的子序列的最小末尾 int[] h = new int[nums.length]; h[0] = nums[0]; int max = 0; for (int i = 1; i < nums.length; i++) { if (nums[i] > h[max]) { h[++max] = nums[i]; } else { int index = findFirstBiggerOrEqual(h, 0, max, nums[i]); h[index] = nums[i]; } } return max + 1; } public int findFirstBiggerOrEqual(int[] nums, int start, int end, int target) { while (start + 1 < end) { int mid = (start + end) / 2; if (nums[mid] == target) { end = mid; } else if (nums[mid] < target) { start = mid; } else { end = mid; } } if (nums[start] >= target) { return start; } else { return end; } }
Dungeon Game
勇士救公主,给了给了一个二维数组,数组中元素可正可负可0,正表示补充血量负表示损失血量,勇士血量必须大于0才能存活。现在勇士从左上角出发,前往右下角拯救公主。勇士选择只往左和往下移动。求勇士开始必须携带多少血量。
明显使用动态规划。dp[i][j]为从[i,j]到达右下角需要携带的能量。(加入采用自顶向下的动态规划,dp[i][j]表示从左上角到达[i,j],出发时必须携带多少血量的话,还需要记录到达一个位置之后还存有多少血量,所以选择自底向上的动态规划)
1 def calculateMinimumHP(self, dungeon): 2 """ 3 :type dungeon: List[List[int]] 4 :rtype: int 5 """ 6 row = len(dungeon) 7 col = len(dungeon[0]) 8 """ 9 从右下角往左上角开始遍历 10 遍历过的dungeon[i][j]表示从当前位置到达右下角需要具有的最小生命(-1) 11 """ 12 dungeon[row - 1][col - 1] = -dungeon[row - 1][col - 1] if dungeon[row - 1][col - 1] < 0 else 0 13 for i in range(row - 2, -1, -1): 14 temp = dungeon[i + 1][col - 1] - dungeon[i][col - 1] 15 dungeon[i][col - 1] = temp if temp > 0 else 0 16 for j in range(col - 2, -1, -1): 17 temp = dungeon[row - 1][j + 1] - dungeon[row - 1][j] 18 dungeon[row - 1][j] = temp if temp > 0 else 0 19 i = row - 2 20 j = col - 2 21 for i in range(row - 2, -1, -1): 22 for j in range(col - 2, -1, -1): 23 temp = dungeon[i][j + 1] - dungeon[i][j] 24 right = temp if temp > 0 else 0 25 temp = dungeon[i + 1][j] - dungeon[i][j] 26 down = temp if temp > 0 else 0 27 dungeon[i][j] = min(right, down) 28 return dungeon[0][0] + 1
Different Ways to Add Parentheses
Given a string of numbers and operators, return all possible results from computing all the different possible ways to group numbers and operators. The valid operators are +
, -
and *
"2-1-1" 首先是想到分治的思想,以每个符号分为前半和后半,两半组合起来作为一个结果,但是这容易带来重复的问题,而且具有很多的重复计算。考虑到重复计算,想到动态规划
首先把数字和符号分开存储在一个list中,然后dp[i][j]表示从第i个数到第j个数的所有加括号计算结果。首选dp[0][0],dp[1][1],...,dp[N-1][N-1](N为数字的个数)可以很容易得到,然后依次求解间距为1,2,...,N-1。求解每一个的时候比如dp[i][i + d]让分割点分别为i, i +1, i + d - 1,每个分割点的左右两边组合起来。
def diffWaysToCompute(self, input): """ :type input: str :rtype: List[int] """ options = [] i = 0 length = len(input) while i < length: j = i while i < length and input[i].isnumeric(): i += 1 options.append(input[j:i]) if i != length: options.append(input[i]) i += 1 N = (len(options) + 1) / 2 #dp[i][j] i to j num dp = [[[]for i in range(N)]for j in range(N)] for i in range(N): dp[i][i].append(int(options[2*i])) for d in range(1,N): for i in range(0,N - d): for j in range(i, i + d): left = dp[i][j] right = dp[j + 1][i + d] flag = options[2 * j + 1] l = 0 r = 0 for l in left: for r in right: if flag == '+': dp[i][i + d].append(l + r) elif flag == '-': dp[i][i + d].append(l - r) else: dp[i][i + d].append(l * r) return dp[0][N - 1]
Longest Valid Parentheses
Given a string containing just the characters '('
and ')'
, find the length of the longest valid (well-formed) parentheses substring.
dp[i]表示必须以i结尾的最长的有效括号对的长度。注意边界问题
1 def longestValidParentheses(self, s): 2 """ 3 :type s: str 4 :rtype: int 5 """ 6 length = len(s) 7 dp = [0] * length 8 res = 0 9 for i in range(1, length): 10 if s[i] == ')': 11 if s[i - 1] == '(': 12 dp[i] = (dp[i - 2] if i - 2 >= 0 else 0) + 2 13 elif i - dp[i - 1] - 1 >= 0 and s[i - dp[i - 1] - 1] == '(': 14 dp[i] = dp[i - 1] + 2 + (dp[i - dp[i - 1] - 2] if i - dp[i - 1] - 2 >= 0 else 0) 15 res = max(res, dp[i]) 16 return res
Maximal Rectangle***
Given a 2D binary matrix filled with 0's and 1's, find the largest rectangle containing only 1's and return its area.
方法一:动态规划
dp[i][j]记录了以(i,j)作为右下角的矩形最大长宽和最长的可以延伸的长宽。之前只记录最大矩形面积,这是有问题的,因为你可能某一行特别特别长或者某一列特别特别高这种情况。可以实现,但是代码太冗长了
1 public class Solution { 2 public int maximalRectangle(char[][] matrix) { 3 if (matrix == null || matrix.length == 0) { 4 return 0; 5 } 6 int m = matrix.length; 7 int n = matrix[0].length; 8 Node[][] dp = new Node[m][n]; 9 int res = 0; 10 if (matrix[0][0] == '1') { 11 dp[0][0] = new Node(1,1,1,1); 12 res = 1; 13 } 14 else { 15 dp[0][0] = new Node(); 16 } 17 int tmp; 18 for (int j = 1; j < n; j++) { 19 if (matrix[0][j] == '1') { 20 tmp = dp[0][j - 1].len1 + 1; 21 dp[0][j] = new Node(tmp, tmp, 1, 1); 22 res = Math.max(res, tmp); 23 } 24 else { 25 dp[0][j] = new Node(); 26 } 27 } 28 for (int i = 1; i < m; i++) { 29 if(matrix[i][0] == '1') { 30 tmp = dp[i - 1][0].width1 + 1; 31 dp[i][0] = new Node(1, 1, tmp, tmp); 32 res = Math.max(res, tmp); 33 } 34 else { 35 dp[i][0] = new Node(); 36 } 37 } 38 for (int i = 1; i < m; i++) { 39 for (int j = 1; j < n; j++) { 40 if (matrix[i][j] == '1') { 41 if (matrix[i - 1][j - 1] == '0') { 42 if (dp[i][j - 1].len1 > dp[i - 1][j].width1) { 43 tmp = dp[i][j - 1].len1 + 1; 44 dp[i][j] = new Node(tmp, tmp , dp[i - 1][j].width1 + 1, 1); 45 } 46 else { 47 tmp = dp[i - 1][j].width1 + 1; 48 dp[i][j] = new Node(dp[i][j - 1].len1 + 1,1, tmp, tmp); 49 } 50 } 51 else if (matrix[i - 1][j] == '0') { 52 tmp = dp[i][j - 1].len1 + 1; 53 dp[i][j] = new Node(tmp, tmp, 1, 1); 54 } 55 else if (matrix[i][j - 1] == '0') { 56 tmp = dp[i - 1][j].width1 + 1; 57 dp[i][j] = new Node(1, 1, tmp, tmp); 58 } 59 else { 60 int area1 = dp[i][j - 1].len1 + 1; 61 int area2 = dp[i - 1][j].width1 + 1; 62 int L = Math.min(dp[i - 1][j - 1].len2, dp[i][j - 1].len1) + 1; 63 int W = Math.min(dp[i - 1][j - 1].width2, dp[i - 1][j].width1) + 1; 64 int area3 = L * W; 65 if (area1 > area2 && area1 > area3) { 66 dp[i][j] = new Node(area1, area1, area2, 1); 67 } 68 else if(area2 > area1 && area2 > area3) { 69 dp[i][j] = new Node(area1, 1, area2, area2); 70 } 71 else { 72 dp[i][j] = new Node(dp[i][j - 1].len1 + 1, L, dp[i - 1][j].width1 + 1, W); 73 } 74 } 75 } 76 else { 77 dp[i][j] = new Node(); 78 } 79 res = Math.max(res, dp[i][j].len2 * dp[i][j].width2); 80 } 81 } 82 return res; 83 } 84 } 85 class Node { 86 int len1;//实际可延伸长 87 int len2;//矩形长 88 int width1;//实际可延伸宽 89 int width2;//矩形宽 90 public Node(){} 91 public Node(int len1, int len2, int width1, int width2) { 92 this.len1 = len1; 93 this.len2 = len2; 94 this.width1 = width1; 95 this.width2 = width2; 96 } 97 }
方法二:
使用三个数组 left[] right[] height[]。逐行遍历,height[i]表示i这个位置往上的连续的“1”的个数(包括当前行),left[i]表示在当前这个高度下,i的最左边的位置(包括),right[j]表示在当前这个高度下,i的最右边的位置(不包括)。那么i位置的一个可能结果为(right[i] - left[i]) * height[i]。当然了,还可以根据m和n的大小进行代码优化。
1 public int maximalRectangle(char[][] matrix) { 2 if (matrix == null || matrix.length == 0) { 3 return 0; 4 } 5 int m = matrix.length; 6 int n = matrix[0].length; 7 int[] left = new int[n]; 8 int[] right = new int[n]; 9 int[] height = new int[n]; 10 int res = 0; 11 Arrays.fill(right,n); 12 for (int i = 0; i < m ; i++) { 13 int curLeft = 0; 14 int curRight = n; 15 for (int j = 0; j < n; j++) { 16 if (matrix[i][j] == '1') { 17 height[j]++; 18 left[j] = Math.max(left[j], curLeft); 19 } 20 else { 21 height[j] = 0; 22 left[j] = 0; 23 curLeft = j + 1; 24 } 25 } 26 for (int j = n - 1; j >= 0; j--) { 27 if (matrix[i][j] == '1') { 28 right[j] = Math.min(right[j], curRight); 29 res = Math.max(res,(right[j] - left[j]) * height[j]); 30 } 31 else { 32 right[j] = n; 33 curRight = j; 34 } 35 } 36 } 37 return res; 38 }
序列型动态规划
如果不是与坐标有关的动态规划问题,一般有N个数/字符,就开N+1个空间,为了把第一个空也包含进来,第一个单独初始化
单词切分
方法一:如果直接使用位置来划分的话,那么求解每个位置都要看所有他前面的位置,而字符串的对比复杂度为O(L)(最大单词长度),因此最终的复杂度为O(LN^2),超时了。
1 public boolean wordBreak(String s, Set<String> dict) { 2 // write your code here 3 if (s == null || dict == null) { 4 return false; 5 } 6 if (s.length() == 0 && dict.size() == 0) { 7 return true; 8 } 9 //dp[i]表示前i个字符是否能够被完美切分 10 boolean[] dp = new boolean[s.length() + 1]; 11 dp[0] = true; 12 for (int i = 1; i < dp.length; i++) { 13 for (int j = 0; j < i; j++) { 14 if (dp[j] && dict.contains(s.substring(j,i))) { 15 dp[i] = true; 16 break; 17 } 18 } 19 } 20 return dp[s.length()]; 21 }
方法二:可是首先统计出最大单词长度,在求解dp[i]的时候,分割位置只有导致右半部分小于等于最大单词长度时才有可能匹配,所以让j从maxWordLen - i 开始分割,这样复杂度为O(NL^2)。下表问题需要好好分析一下。
1 public boolean wordBreak(String s, Set<String> dict) { 2 // write your code here 3 if (s == null || dict == null) { 4 return true; 5 } 6 int maxWordLen = 0; 7 for (String str:dict) { 8 maxWordLen = Math.max(maxWordLen, str.length()); 9 } 10 11 //dp[i]表示前i个字符是否能够被完美切分(从1计数) 12 boolean[] dp = new boolean[s.length() + 1]; 13 dp[0] = true; 14 for (int i = 1; i < dp.length; i++) { 15 int j = Math.max(i - maxWordLen, 0); 16 for (; j < i; j++) { 17 if (dp[j] && dict.contains(s.substring(j,i))) { 18 dp[i] = true; 19 break; 20 } 21 } 22 } 23 return dp[s.length()]; 24 }
Word Break II ****
Given a string s and a dictionary of words dict, add spaces in s to construct a sentence where each word is a valid dictionary word.
Return all such possible sentences.
tips: StringBuilder添加在前边 sb.insert(0, str);
StringBuilder删除 sb.delete(start, end); // 包含start, 不包含end
方法一:
受分割回文串的影响,这里思考首先求得[i, j]子串是否包含在字典中,然后进行DFS,但这里的问题是求[i, j]子串是否包含在字典中这个操作不能使用DP,只能通过N^2的复杂度直接计算出来,然后之后再进行深搜,直接这样做导致超时了。所以这里又借鉴一下单词切分一的思想。DFS的时候,从后边的单词开始分析,如果一个子串包含子字典中,并且前边的子串可以被完美切分,才继续往下搜索。这里判断是否能够完美切分就是使用上题中的动态规划来获得。
1 public List<String> wordBreak(String s, Set<String> wordDict) { 2 List<String> results = new ArrayList<String>(); 3 if (s == null || s.length() == 0 || wordDict == null || wordDict.size() == 0) { 4 return results; 5 } 6 int n = s.length(); 7 String[][] strs = new String[n][n]; 8 boolean[][] inDict = new boolean[n][n]; 9 for (int i = 0; i < n; i++) { 10 for (int j = i; j < n; j++) { 11 strs[i][j] = s.substring(i, j + 1); 12 if (wordDict.contains(strs[i][j])) { 13 inDict[i][j] = true; 14 } 15 } 16 } 17 18 //dp[i]表示前i个字符是否能够被完美拆分 19 boolean[] dp = new boolean[n + 1]; 20 dp[0] = true; 21 int max_length = 0; 22 for (String str : wordDict) { 23 max_length = Math.max(max_length, str.length()); 24 } 25 for (int i = 1; i <= n; i++) { 26 for (int j = Math.max(0, i - max_length); j < i; j++) { 27 if (dp[j] && wordDict.contains(s.substring(j, i))) { 28 dp[i] = true; 29 break; 30 } 31 } 32 } 33 help(s, results, new StringBuilder(), inDict, strs, s.length() - 1, dp); 34 return results; 35 } 36 37 private void help(String s, List<String> results, StringBuilder sb, boolean[][] inDict, String[][] strs, int end, boolean[] dp) { 38 if (end == -1) { 39 results.add(sb.toString().trim()); 40 return; 41 } 42 for (int i = end; i >= 0; i--) { 43 if (inDict[i][end]) { 44 if (!dp[i]) { 45 continue; 46 } 47 sb.insert(0, " "); 48 sb.insert(0, strs[i][end]); 49 help(s, results, sb, inDict, strs, i - 1, dp); 50 sb.delete(0, strs[i][end].length() + 1); 51 // int length = sb.length(); 52 // sb.append(strs[i][end]).append(" "); 53 // help(s, results, sb, inDict, strs, i - 1, dp); 54 // sb.setLength(length); 55 } 56 } 57 }
方法二:
带记忆功能的DFS。由于方法一求解是否在字典中的那个二维数组比较麻烦,所以这里换一种想法。使用一个map记录一个出现过的字符串的可以被切分的所有方案。遍历数组,如果一个子串的数组中,那么递归的去遍历其后边的子串,如果一个字符串已经存在在map中,直接返回即可。
1 public List<String> wordBreak(String s, Set<String> wordDict) { 2 if (s == null || s.length() == 0 || wordDict == null || wordDict.size() == 0) { 3 return new ArrayList<String>(); 4 } 5 return help(s, wordDict, new HashMap<String, List<String>>()); 6 } 7 8 private List<String> help(String s, Set<String> wordDict, HashMap<String, List<String>> map) { 9 if (map.containsKey(s)) { 10 return map.get(s); 11 } 12 List<String> res = new ArrayList<String>(); 13 if (s.length() == 0) { 14 res.add(""); 15 return res; 16 } 17 for (int i = 1; i <= s.length(); i++) { 18 String temp = s.substring(0, i); 19 if (wordDict.contains(temp)) { 20 List<String> subList = help(s.substring(i), wordDict, map); 21 for (String sub : subList) { 22 res.add(temp + (sub.length() == 0 ? "" : " ") + sub); 23 } 24 } 25 } 26 map.put(s, res); 27 return res; 28 }
方法三:
基本原理同方法二,只是方法二是去看s的子串是否在字典中,这里是s是否以字典中的单词作为开始,如果是,递归的遍历后边的子串,而且使用map来记录出现过的字符串的切分方案。如果字典较大的话这个方法不好。
1 public List<String> wordBreak(String s, Set<String> wordDict) { 2 if (s == null || s.length() == 0 || wordDict == null || wordDict.size() == 0) { 3 return new ArrayList<String>(0); 4 } 5 return DFS(s, wordDict, new HashMap<String, List<String>>()); 6 } 7 //DFS求解s的所有切分方案 8 private List<String> DFS(String s, Set<String> wordDict, HashMap<String, List<String>> map) { 9 if (map.containsKey(s)) { 10 return map.get(s); 11 } 12 List<String> res = new ArrayList<String>(); 13 if (s.length() == 0) { 14 res.add(""); 15 return res; 16 } 17 for (String str : wordDict) { 18 if (s.startsWith(str)) { 19 List<String> subList = DFS(s.substring(str.length()), wordDict, map); 20 for (String sub : subList) { 21 res.add(str + (sub.length() == 0 ? "" : " ") + sub); 22 } 23 } 24 } 25 map.put(s, res); 26 return res; 27 }
方法四:效率最高的方法。前面的算法一步一步推导而来。
方法二 + 是否能够完美切分。
1 public List<String> wordBreak(String s, Set<String> wordDict) { 2 if (s == null || s.length() == 0 || wordDict == null || wordDict.size() == 0) { 3 return new ArrayList<String>(); 4 } 5 int n = s.length(); 6 boolean[] dp = new boolean[n + 1]; 7 dp[0] = true; 8 int max_length = 0; 9 for (String str : wordDict) { 10 max_length = Math.max(max_length, str.length()); 11 } 12 for (int i = 1; i <= n; i++) { 13 for (int j = Math.max(0, i - max_length); j < i; j++) { 14 if (dp[j] && wordDict.contains(s.substring(j, i))) { 15 dp[i] = true; 16 break; 17 } 18 } 19 } 20 HashMap<String, List<String>> map = new HashMap<String, List<String>>(); 21 return DFS(s, wordDict, map, dp); 22 } 23 24 private List<String> DFS(String s, Set<String> wordDict, HashMap<String, List<String>> map, boolean[] dp) { 25 if (map.containsKey(s)) { 26 return map.get(s); 27 } 28 List<String> res = new ArrayList<String>(); 29 if (s.length() == 0) { 30 return res; 31 } 32 if (wordDict.contains(s)) { 33 res.add(s); 34 } 35 int end = s.length(); 36 for (int i = end - 1; i >= 0; i--) { 37 String cur = s.substring(i, end); 38 if (wordDict.contains(cur) && dp[i]) { 39 List<String> list = DFS(s.substring(0, i), wordDict, map, dp); 40 for (String str : list) { 41 res.add(str + " " + cur); 42 } 43 } 44 } 45 map.put(s, res); 46 return res; 47 }
分割回文串 II
方法一:DP
1 public int minCut(String s) { 2 // write your code here 3 if (s == null || s.length() == 0) { 4 return 0; 5 } 6 //dp[i]表示前i个字符符合要求的最小分割次数 7 int len = s.length(); 8 int[] dp = new int[len + 1]; 9 dp[0] = -1; 10 for (int i = 1; i <= len; i++) { 11 dp[i] = Integer.MAX_VALUE; 12 for (int j = 0; j < i; j++) { 13 if (isPalidrome(s.substring(j,i))) { 14 dp[i] = Math.min(dp[i], dp[j] + 1); 15 } 16 } 17 } 18 return dp[len]; 19 } 20 public boolean isPalidrome(String str) { 21 int left = 0; 22 int right = str.length() - 1; 23 while(left < right) { 24 if (str.charAt(left++) != str.charAt(right--)) { 25 return false; 26 } 27 } 28 return true; 29 }
方法二:由于方法一每次多得按照标准方法判断一个字符串是不是回文串,这里使用DP事先就把一个字符串是不是回文串这样的一个二维数组准备好
1 public int minCut(String s) { 2 // write your code here 3 if (s == null || s.length() == 0) { 4 return 0; 5 } 6 boolean[][] isPalidrome = getIsPalidrome(s); 7 //dp[i]表示前i个字符符合要求的最小分割次数 8 int len = s.length(); 9 int[] dp = new int[len + 1]; 10 dp[0] = -1; 11 for (int i = 1; i <= len; i++) { 12 dp[i] = Integer.MAX_VALUE; 13 for (int j = 0; j < i; j++) { 14 if (isPalidrome[j][i - 1]) { 15 dp[i] = Math.min(dp[i], dp[j] + 1); 16 } 17 } 18 } 19 return dp[len]; 20 } 21 public boolean[][] getIsPalidrome(String s){ 22 int len = s.length(); 23 boolean[][] isPalidrome = new boolean[len][len]; 24 for (int i = 0; i < len; i++) { 25 isPalidrome[i][i] = true; 26 } 27 for (int i = 0; i < len - 1; i++) { 28 isPalidrome[i][i+1] = (s.charAt(i) == s.charAt(i + 1)); 29 } 30 for (int length = 2; length < len; length++) { 31 for (int start = 0; start + length < len; start++) { 32 isPalidrome[start][start + length] = isPalidrome[start + 1][start + length -1] 33 && s.charAt(start) == s.charAt(start + length); 34 } 35 } 36 return isPalidrome; 37 }
Burst Balloons************
Given n
balloons, indexed from 0
to n-1
. Each balloon is painted with a number on it represented by array nums
. You are asked to burst all the balloons. If the you burst balloon i
you will get nums[left] * nums[i] * nums[right]
coins. Here left
and right
are adjacent indices of i
. After the burst, the left
and right
then becomes adjacent.
Find the maximum coins you can collect by bursting the balloons wisely.
自己只想出了回溯的解法。很明显具有许多的重复计算,但阻碍我进行DP的是我选择一个戳之后,左边和右边部分并不能单独求解,是相互有影响的。
所以这里是否给出的解法是自底向上的DP。思考i位置的元素为最后一个戳破的气球,那么带来的收益为nums[left] * nums[i] * nums[right]。此时的左右两边就相互没有影响了,左边可以递归的调用,此时左半部分的右边界变为i。右半部分递归调用,左边界变为i。计算3456,分别计算3 4 5 6为最后一个戳破的,取结果的最大。计算的时候记录一个memo避免重复计算。
DP也就一样了,这类题目共有的,以距离为一个递增量,先计算距离短的,然后长的。
方法一:backTracking 复杂度O(n!)超时了
1 public int maxCoins(int[] nums) { 2 if (nums == null || nums.length == 0) { 3 return 0; 4 } 5 if (nums.length == 1) { 6 return nums[0]; 7 } 8 Node[] nodes = new Node[nums.length]; 9 for (int i = 0; i < nums.length; i++) { 10 nodes[i] = new Node(nums[i]); 11 } 12 for (int i = 1; i < nums.length - 1; i++) { 13 nodes[i].left = nodes[i - 1]; 14 nodes[i].right = nodes[i + 1]; 15 } 16 Node head = new Node(1); 17 Node end = new Node(1); 18 head.right = nodes[0]; 19 nodes[0].left = head; 20 nodes[0].right = nodes[1]; 21 nodes[nums.length - 1].left = nodes[nums.length - 2]; 22 nodes[nums.length - 1].right = end; 23 end.left = nodes[nums.length - 1]; 24 return dfs(head, nums.length); 25 } 26 public int dfs(Node head, int num) { 27 if (num == 0) { 28 return 0; 29 } 30 int res = 0; 31 int cur = 0; 32 Node curNode = head.right; 33 while (curNode.right != null) { 34 cur = curNode.left.val * curNode.val * curNode.right.val; 35 curNode.left.right = curNode.right; 36 curNode.right.left = curNode.left; 37 res = Math.max(res, cur + dfs(head, num - 1)); 38 curNode.left.right = curNode; 39 curNode.right.left = curNode; 40 curNode = curNode.right; 41 } 42 return res; 43 } 44 } 45 class Node { 46 int val; 47 Node left; 48 Node right; 49 public Node(int val) { 50 this.val = val; 51 }
方法二:D & C + memory
1 public int maxCoins(int[] inums) { 2 int n = inums.length; 3 int[] nums = new int[n + 2]; 4 for (int i = 1; i <= n; i++) { 5 nums[i] = inums[i - 1]; 6 } 7 nums[0] = 1; 8 nums[n + 1] = 1; 9 n += 2; 10 int[][] memo = new int[n][n]; 11 return burst(memo, nums, 0, n - 1); 12 13 } 14 public int burst(int[][] memo, int[] nums, int left, int right) { 15 if (left + 1 == right) { 16 return 0; 17 } 18 if (memo[left][right] != 0) { 19 return memo[left][right]; 20 } 21 int ans = 0; 22 for (int i = left + 1; i < right; i++) { 23 ans = Math.max(ans, nums[left] * nums[right] * nums[i] 24 + burst(memo, nums, left, i) + burst(memo, nums, i, right)); 25 } 26 memo[left][right] = ans; 27 return ans; 28 29 }
方法三:DP
1 public int maxCoins(int[] inums) { 2 int n = inums.length; 3 int[] nums = new int[n + 2]; 4 for (int i = 1; i <= n; i++) { 5 nums[i] = inums[i - 1]; 6 } 7 nums[0] = 1; 8 nums[n + 1] = 1; 9 n += 2; 10 //dp[i][j] 表示 i dao j的最大(bu bao kuo i j) 11 int[][] dp = new int[n][n]; 12 for (int k = 2; k < n; k++) { 13 for (int left = 0; left < n - k; left++) { 14 int right = left + k; 15 for (int j = left + 1; j < right; j++) { 16 dp[left][right] = Math.max(dp[left][right], nums[left] * nums[j] * nums[right] 17 + dp[left][j] + dp[j][right]); 18 } 19 } 20 } 21 return dp[0][n - 1]; 22 }
双序列型动态规划
dp[i][j] 第一个字符串的前i个字符和第二个字符串的前j个字符
研究s(i-1)和p(j-1)的关系
初始化第一行、第一列
dp[m][n]为答案
最长公共子序列
1 public int longestCommonSubsequence(String A, String B) { 2 // write your code here 3 if (A == null || A.length() == 0 || B == null || B.length() == 0) { 4 return 0; 5 } 6 int m = A.length(); 7 int n = B.length(); 8 //dp[i][j]表示A的前i个字符和B的前j个字符的最长公共子串长度 9 int[][] dp = new int[m + 1][n + 1]; 10 for (int i = 1; i<= m; i++) { 11 for (int j = 1; j <= n; j++) { 12 if (A.charAt(i - 1) == B.charAt(j - 1)) { 13 dp[i][j] = dp[i - 1][j - 1] + 1; 14 } 15 else { 16 dp[i][j] = Math.max(dp[i][j - 1],dp[i - 1][j]); 17 } 18 } 19 } 20 return dp[m][n]; 21 }
最长公共子串
1 public int longestCommonSubstring(String A, String B) { 2 // write your code here 3 if (A == null || A.length() == 0 || B == null || B.length() == 0) { 4 return 0; 5 } 6 int m = A.length(); 7 int n = B.length(); 8 //dp[i][j]表示A的必须以i-1结尾B以j-1结尾的LCS 9 int max = 0; 10 int[][] dp = new int[m + 1][n + 1]; 11 for (int i = 1; i<= m; i++) { 12 13 for (int j = 1; j <= n; j++) { 14 if (A.charAt(i - 1) == B.charAt(j - 1)) { 15 dp[i][j] = dp[i - 1][j - 1] + 1; 16 max = Math.max(max, dp[i][j]); 17 } 18 } 19 } 20 return max; 21 }
编辑距离
给出两个单词word1和word2,计算出将word1 转换为word2的最少操作次数。
你总共三种操作方法:
- 插入一个字符
- 删除一个字符
- 替换一个字符
1 public int minDistance(String word1, String word2) { 2 // write your code here 3 if (word1 == null || word2 == null) { 4 return 0; 5 } 6 int m = word1.length(); 7 int n = word2.length(); 8 //dp[i][j]表示word1的前i个字符转化为word2的前j个字符的最小转化次数 9 int[][] dp = new int[m + 1][n + 1]; 10 for (int i = 0; i <= m; i++) { 11 dp[i][0] = i; 12 } 13 for (int j = 1; j <= n; j++) { 14 dp[0][j] = j; 15 } 16 for (int i = 1; i<=m; i++) { 17 for (int j = 1; j<=n; j++) { 18 if (word1.charAt(i - 1) == word2.charAt(j - 1)) {//注意真正拿出值来比较的时候比位置小1 19 dp[i][j] = dp[i - 1][j - 1]; 20 } 21 else { 22 dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i][j - 1],dp[i - 1][j])) + 1;//Attention 23 } 24 } 25 } 26 return dp[m][n]; 27 }
不同的子序列
给出字符串S和字符串T,计算S的不同的子序列中T出现的个数。
求S中包含多少个T
思路:求方案总数,与顺序有关,暴力2^n,所以自然想到使用动态规划。
1 public int numDistinct(String S, String T) { 2 // write your code here 3 if (S == null || T == null) { 4 return 0; 5 } 6 int m = S.length(); 7 int n = T.length(); 8 //dp[i][j]表示S的前i个字符串包含多少个T的前j个字符串 9 int[][] dp = new int[m + 1][n + 1]; 10 //本题的初始化需要特别注意一下 11 for (int i = 0; i<=m; i++) { 12 dp[i][0] = 1; 13 } 14 for (int i = 1; i <= m; i++) { 15 for (int j = 1; j <= n; j++) { 16 if (S.charAt(i - 1) == T.charAt(j - 1)) { 17 dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 18 } 19 else { 20 dp[i][j] = dp[i-1][j]; 21 } 22 } 23 } 24 return dp[m][n]; 25 }
Is Subsequence **** 四种方法都需要掌握
判断s是不是t的子序列。t可能很长。
方法一:直接使用动态规划,由于t太长而发生内存溢出。
1 public boolean isSubsequence(String s, String t) { 2 if (s == null || t == null) 3 return false; 4 int m = s.length(); 5 int n = t.length(); 6 //dp[i][j]表示s的前i个子串是否是n的前j个子串的子序列 7 boolean[][] dp = new boolean[m + 1][n + 1]; 8 for (int j = 0; j <= n; j++) { 9 dp[0][j] = true; 10 } 11 for (int i = 1; i <= m; i++) { 12 for (int j = 1; j <= n; j++) { 13 if (s.charAt(i - 1) == t.charAt(j - 1)) { 14 dp[i][j] = dp[i - 1][j - 1]; 15 } 16 else { 17 dp[i][j] = dp[i][j - 1]; 18 } 19 } 20 } 21 return dp[m][n]; 22 }
方法二:两个指针charAt()
1 public boolean isSubsequence(String s, String t) { 2 if (s == null || t == null) { 3 return false; 4 } 5 if (s.length() == 0) { 6 return true; 7 } 8 int i = 0; 9 int j = 0; 10 while (i < t.length()) { 11 if (s.charAt(j) == t.charAt(i)) { 12 j++; 13 if (j == s.length()) { 14 return true; 15 } 16 } 17 i++; 18 } 19 return false; 20 }
方法三:两个指针indexOf()
1 public boolean isSubsequence(String s, String t) { 2 if (s == null || t == null) { 3 return false; 4 } 5 if (s.length() == 0) { 6 return true; 7 } 8 int prev = 0; 9 for (int i = 0; i < s.length(); i++) { 10 char tmpChar = s.charAt(i); 11 prev = t.indexOf(tmpChar,prev);//important 12 if (prev == -1) { 13 return false; 14 } 15 prev++; 16 } 17 return true; 18 }
方法三比方法二快的原因是indexOf比如现在找不到,那么方法一一直需要到t遍历一遍之后才可以确定,而方法二不需要遍历一遍。
方法四:BinarySearch 适用于t不变,需要多次查询的情况。
Collections.binarySearch() 返回与插入点有关,如果包含键,则返回插入点,由0开始计数。否则,则返回负的插入点,由1开始计数。
使用一个数组记录t中各个字符出现的位置,由于可能有多个位置,所以数组的元素为一个list,list元素递增。
然后遍历s中的元素,prev记录下标已经走到的位置,如果某个元素所在下标都小于下标到达的位置,那么返回false,遍历结束后返回true.
1 public boolean isSubsequence(String s, String t) { 2 List<Integer>[] list = new List[26]; 3 for (int i = 0; i < t.length(); i++) { 4 int index = t.charAt(i) - 'a'; 5 if (list[index] == null) { 6 list[index] = new ArrayList<Integer>(); 7 } 8 list[index].add(i); 9 } 10 int prev = 0; 11 for (int i = 0; i < s.length(); i++) { 12 int index = s.charAt(i) - 'a'; 13 if (list[index] == null) { 14 return false; 15 } 16 int j = Collections.binarySearch(list[index], prev); 17 if (j < 0) { 18 j = -j - 1; 19 } 20 if (j == list[index].size()) { 21 return false; 22 } 23 prev = list[index].get(j) + 1; 24 } 25 return true; 26 }
交叉字符串
给出三个字符串:s1、s2、s3,判断s3是否由s1和s2交叉构成。
思路:有顺序,判断可行性,暴力2^n,基本可以确定为DP。那么按照之前的思路是不是需要开一个三维的DP呢?注意到如果满足条件,S3的子序列的长度一定是S1和S2子序列的长度的和,所以只需要开一个二维的DP就可以了。
1 public boolean isInterleave(String s1, String s2, String s3) { 2 // write your code here 3 if (s1 == null || s2 == null || s3 == null) { 4 return false; 5 } 6 int m = s1.length(); 7 int n = s2.length(); 8 int k = s3.length(); 9 if (m + n != k) { 10 return false; 11 } 12 //dp[i][j]表示是s1的前i个字符与s2的前j个字符是否能够交叉构成s3的前i+j个字符。 13 boolean[][] dp = new boolean[m + 1][n + 1]; 14 dp[0][0] = true; 15 for (int i = 1; i <= m; i++) { 16 if (s1.charAt(i - 1) == s3.charAt(i - 1)) { 17 dp[i][0] = true; 18 } 19 else { 20 break; 21 } 22 } 23 for (int j = 1; j <= n; j++) { 24 if (s2.charAt(j - 1) == s3.charAt(j - 1)) { 25 dp[0][j] =true; 26 } 27 else { 28 break; 29 } 30 } 31 for (int i = 1; i <= m; i++) { 32 for (int j = 1; j <=n; j++) { 33 char c1 = s1.charAt(i - 1); 34 char c2 = s2.charAt(j - 1); 35 char c3 = s3.charAt(i + j - 1); 36 if (c1 == c3 && c2 == c3) { 37 dp[i][j] = dp[i - 1][j] || dp[i][j - 1]; 38 } 39 else if (c1 == c3) { 40 dp[i][j] = dp[i - 1][j]; 41 } 42 else if (c2 == c3) { 43 dp[i][j] = dp[i][j - 1]; 44 } 45 else { 46 dp[i][j] = false; 47 } 48 } 49 } 50 return dp[m][n]; 51 }
解码方法
有一个消息包含A-Z
通过以下规则编码
'A' -> 1
'B' -> 2
...
'Z' -> 26
现在给你一个加密过后的消息,问有几种解码的方式
注意:要特别注意遇到0的情况,而且可能是当前为0,也有可能是前一个为0
1 public int numDecodings(String s) { 2 // Write your code here 3 if (s == null || s.length() == 0) { 4 return 0; 5 } 6 //dp[i] 表示前i个字符一共有多少种解码方式 7 int[] dp = new int[s.length() + 1]; 8 dp[0] = 1; 9 dp[1] = s.charAt(0) == '0' ? 0 : 1; 10 for (int i = 2; i <= s.length(); i++) { 11 char c1 = s.charAt(i - 1); 12 char c2 = s.charAt(i - 2); 13 if (c1 == '0') { 14 if (c2 - '0' != 1 && c2 - '0' != 2) { 15 return 0; 16 } 17 else { 18 dp[i] = dp[i - 2]; 19 } 20 } 21 else { 22 if (c2 == '0') { 23 dp[i] = dp[i - 1]; 24 } 25 else { 26 int val = (c2 - '0') * 10 + (c1 - '0'); 27 if (val > 26) { 28 dp[i] = dp[i - 1]; 29 } 30 else { 31 dp[i] = dp[i - 2] + dp[i - 1]; 32 } 33 } 34 } 35 } 36 return dp[s.length()]; 37 }
最小调整代价***
给一个整数数组,调整每个数的大小,使得相邻的两个数的差小于等于一个给定的整数target,调整每个数的代价为调整前后的差的绝对值,求调整代价之和最小是多少。
思路:dp[i][j]表示前i个字符,第i个字符取j的最小调整代价。
1 public int MinAdjustmentCost(ArrayList<Integer> A, int target) { 2 // write your code here 3 //dp[i][j]表示到前i个字符为止,第i个字符变为j的最小调整代价 4 int n = A.size(); 5 int[][] dp = new int[n + 1][101]; 6 for (int i = 0; i <= n; i++) { 7 for (int j = 0; j <= 100; j++) { 8 dp[i][j] = Integer.MAX_VALUE; 9 } 10 } 11 for (int j = 0; j <= 100; j++) { 12 dp[0][j] = 0; 13 } 14 15 for (int i = 1; i <= n; i++) { 16 int curVal = A.get(i - 1); 17 for (int k = 0; k <= 100; k++) {//当前位置取k 18 for (int j = 0; j <= 100; j++) {//前一个位置取j 19 if (dp[i - 1][j] == Integer.MAX_VALUE) { 20 continue; 21 } 22 if (Math.abs(k - j) <= target) { 23 dp[i][k] = Math.min(dp[i][k], dp[i - 1][j] + Math.abs(curVal - k)); 24 } 25 } 26 } 27 } 28 int result = Integer.MAX_VALUE; 29 for (int j = 0; j<=100; j++) { 30 result = Math.min(result,dp[n][j]); 31 } 32 return result; 33 }
通配符匹配
判断两个可能包含通配符“?”和“*”的字符串是否匹配。匹配规则如下:
'?' 可以匹配任何单个字符。 '*' 可以匹配任意字符串(包括空字符串)。 方法一:DP.两个串完全匹配才算匹配成功。
1 public boolean isMatch(String p, String s) { 2 // write your code here 3 if (s == null || p == null) { 4 return false; 5 } 6 int m = s.length(); 7 int n = p.length(); 8 //dp[i][j]表示s的前i个字符时候能够和p的前j个字符匹配 9 boolean[][] dp = new boolean[m + 1][n + 1]; 10 dp[0][0] = true; 11 for (int i = 1; i <= m; i++) { 12 if (s.charAt(i - 1) == '*') { 13 dp[i][0] = true; 14 }else { 15 break; 16 } 17 } 18 for (int i = 1; i <= m; i++) { 19 for (int j = 1; j <= n; j++) { 20 if (s.charAt(i -1) == p.charAt(j -1) || s.charAt(i - 1) == '?') { 21 dp[i][j] = dp[i - 1][j - 1]; 22 } 23 else if (s.charAt(i - 1) == '*') { 24 dp[i][j] = dp[i - 1][j] || dp[i][j - 1]; 25 } 26 } 27 } 28 return dp[m][n]; 29 }
方法二:控制s p的下标移动。变换*匹配的长度看是否能够匹配abcded a*d。可以这样做的原因是这里的*和前边的元素没有关系。
def isMatch(self, s, p): """ :type s: str :type p: str :rtype: bool """ sIndex = 0 pIndex = 0 sLen = len(s) pLen = len(p) starIndex = -1 match = 0 while sIndex < sLen: if pIndex < pLen and (s[sIndex] == p[pIndex] or p[pIndex] == '?'): pIndex += 1 sIndex += 1 elif pIndex < pLen and p[pIndex] == '*': starIndex = pIndex pIndex += 1 match = sIndex elif starIndex != -1: match += 1 sIndex = match pIndex = starIndex + 1 else: return False while pIndex < pLen and p[pIndex] == '*': pIndex += 1 return pIndex == pLen
正则表达式匹配
'.' Matches any single character. '*' Matches zero or more of the preceding element.
1 public boolean isMatch(String s, String p) { 2 if(s==null||p==null) 3 return false; 4 int m = s.length(); 5 int n = p.length(); 6 boolean[][] dp = new boolean[m + 1][n + 1]; 7 dp[0][0] = true; 8 for (int j = 2; j <= n; j++) { 9 if (p.charAt(j - 1) == '*' && dp[0][j - 2]) { 10 dp[0][j] = true; 11 } 12 } 13 for (int i = 1; i <= m; i++) { 14 for (int j = 1; j <= n; j++) { 15 if (p.charAt(j - 1) == s.charAt(i - 1) || p.charAt(j - 1) == '.') { 16 dp[i][j] = dp[i - 1][j - 1]; 17 } 18 else if (p.charAt(j - 1) == '*') { 19 if (j == 1) { 20 dp[i][j] = false;//第一个为“*”,肯定为false;19-24行代码是为了后边的下标减二操作 21 } 22 else { //分开的原因是a d*,不分开这就会因为dp[i - 1][j]而变为True,根本原因是“*”指代了多个东西 23 if (p.charAt(j - 2) != '.' && p.charAt(j - 2) != s.charAt(i - 1)) { 24 25 dp[i][j] = dp[i][j - 2]; 26 } 27 else { 28 dp[i][j] = dp[i][j - 2] || dp[i][j - 1] || dp[i - 1][j]; 29 } 30 } 31 } 32 } 33 } 34 return dp[m][n]; 35 }
k数和
给定n个不同的正整数,整数k(k < = n)以及一个目标数字。
在这n个数里面找出K个数,使得这K个数的和等于目标数字,求问有多少种方案?
1 public int kSum(int A[], int k, int target) { 2 // write your code here 3 if (A == null) { 4 return 0; 5 } 6 int m = A.length; 7 //dp[i][j][t] 表示从前m个数中挑选j个数,和为t的方案总数 8 int[][][] dp = new int[m + 1][k + 1][target + 1]; 9 for (int i = 0; i <= m; i++) { 10 dp[i][0][0] = 1; 11 } 12 for (int i = 1; i <= m; i++) { 13 for (int j = 1; j <= k && j <= i; j++) { 14 for (int t = 1; t <= target; t++) { 15 if (A[i - 1] <= t) { 16 dp[i][j][t] = dp[i - 1][j - 1][t - A[i - 1]];//选当前数 17 } 18 dp[i][j][t] += dp[i - 1][j][t];//不选当前数 19 } 20 } 21 } 22 return dp[m][k][target]; 23 }
凑 N 分钱的方案数**
给你无限多个的 25 分,10 分,5 分和 1 分的硬币。问如果要凑出 n
分钱有多少种不同的方式?
使用coin[j]或者不使用coin[j]
public int waysNCents(int n) { // Write your code here if (n < 1) { return 1; } int[] coin = {1, 5, 10, 25}; int[][] dp = new int[n + 1][4]; for (int i = 0; i <= n; i++) { dp[i][0] = 1; } for (int j = 0; j < 4; j++) { dp[0][j] = 1; } for (int i = 1; i <= n; i++) { for (int j = 1; j < 4; j++) { if (i < coin[j]) { dp[i][j] = dp[i][j - 1]; } else { dp[i][j] = dp[i][j - 1] + dp[i - coin[j]][j]; } } } return dp[n][3]; }
Scramble String***
方法一:递归
1 public boolean isScramble(String s1, String s2) { 2 if(s1.equals(s2)) 3 return true; 4 int[] chars=new int[26]; 5 for(int i=0;i<s1.length();i++){ 6 chars[s1.charAt(i)-'a']++; 7 chars[s2.charAt(i)-'a']--; 8 } 9 for(int i=0;i<26;i++){ 10 if(chars[i]!=0) 11 return false; 12 } 13 for(int i=1;i<s1.length();i++){//注意这里是1开始,分割点不能是0 14 if(isScramble(s1.substring(0,i),s2.substring(0,i)) 15 &&isScramble(s1.substring(i),s2.substring(i))) 16 return true; 17 if(isScramble(s1.substring(0,i),s2.substring(s1.length()-i)) 18 && isScramble(s1.substring(i),s2.substring(0,s1.length()-i))) 19 return true; 20 } 21 return false; 22 }
方法二:DP.
1 public boolean isScramble(String s1, String s2) { 2 if (s1.length() != s2.length()) { 3 return false; 4 } 5 int n = s1.length(); 6 //dp[i][j][k]表示s1以i开始的K个与s2以j开始的k个是否匹配 7 boolean[][][] dp = new boolean[n][n][n + 1]; 8 for (int k = 1; k <= n; k++) { 9 for (int i = 0; i + k <= n; i++) { 10 for (int j = 0; j + k <= n; j++) { 11 if (k == 1) { 12 dp[i][j][k] = s1.charAt(i) == s2.charAt(j); 13 } 14 else { 15 for (int p = 1; p < k && !dp[i][j][k]; p++) { 16 dp[i][j][k] = dp[i][j][p] && dp[i + p][j + p][k - p] 17 ||(dp[i][j + k - p][p] && dp[i + p][j][k - p]); 18 } 19 } 20 } 21 } 22 } 23 return dp[0][0][n]; 24 }
dp[i][j][k]表示s1以i开始的K个与s2以j开始的k个是否匹配
Drop Eggs II
There is a building of n
floors. If an egg drops from the k
th floor or above, it will break. If it's dropped from any floor below, it will not break.
You're given m
eggs, Find k while minimize the number of drops for the worst case. Return the number of drops in the worst case.
1 public int dropEggs2(int m, int n) { 2 // Write your code here 3 if (m == 1) { 4 return n; 5 } 6 // dp[i][j]表示 i 个蛋 j 层 最少需要扔多少次 7 int[][] dp = new int[m + 1][n + 1]; 8 for (int j = 1; j <= n; j++) { 9 dp[1][j] = j; 10 } 11 for (int i = 2; i <= m; i++) { 12 for (int j = 1; j <= n; j++) { 13 dp[i][j] = Integer.MAX_VALUE; 14 for (int k = 1; k <= j; k++) { 15 dp[i][j] = Math.min(dp[i][j], 1 + Math.max(dp[i - 1][k - 1], dp[i][j - k])); 16 } 17 } 18 } 19 return dp[m][n]; 20 }