LeetCode 面试经典 150 题-动态规划
动态规划
- 定义状态:dp[i]表示当前能够偷盗的最大金额。
- 状态转移:dp[i] = max(dp[i-1], dp[i-2] + nums[i])。
- 初始条件:dp[0] = nums[0]。
- 返回结果:dp[n-1]。
- 空间优化:可以用两个变量代替dp[i-1]和dp[i-2]。
1 2 3 4 5 6 7 8 9 10 11
| class Solution { public int rob(int[] nums) { int f0 = 0, f1 = nums[0]; for (int i = 1; i < nums.length; i++) { int f2 = Math.max(f0 + nums[i], f1); f0 = f1; f1 = f2; } return f1; } }
|
动态规划
和上一题的区别是第一间房屋和最后一间房屋不能同时打劫。如果不偷窃最后一间房屋,则偷窃房屋的下标范围是 [0,n−2];如果不偷窃第一间房屋,则偷窃房屋的下标范围是 [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 24 25 26
| class Solution { public int rob(int[] nums) { int n = nums.length; if (n == 1) { return nums[0]; } if (n == 2) { return Math.max(nums[0], nums[1]); } return Math.max(robRange(nums, 0, n-2), robRange(nums, 1, n-1)); }
public int robRange(int[] nums, int start, int end) { int f0 = nums[start], f1 = Math.max(nums[start], nums[start+1]); for (int i = start + 2; i <= end; i++) { int f2 = Math.max(f0 + nums[i], f1); f0 = f1; f1 = f2; } return f1; } }
|
动态规划
- 定义状态:f表示选中当前节点的最大金额,g表示不选中当前节点的最大金额。
- 状态转移:如果选中当前节点,则不能选左右节点,f = g(l) + g(r)。如果不选中当前节点,则可选可不选左右节点,g = max(f(l), g(l)) + max(f(r), g(r))。
- 初始条件:都是0.
- 返回结果:max(f, g)。
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
|
class Solution { public int rob(TreeNode root) { int[] ans = dfs(root); return Math.max(ans[0], ans[1]); }
public int[] dfs(TreeNode node) { if (node == null) { return new int[]{0,0}; } int[] left = dfs(node.left); int[] right = dfs(node.right); int selected = node.val + left[1] + right[1]; int nonSelected = Math.max(left[0], left[1]) + Math.max(right[0], right[1]); return new int[]{selected, nonSelected}; } }
|
- 时间复杂度:O(n),相当于对树进行了一次后序遍历。
- 空间复杂度:O(n),栈空间消耗。
动态规划
- 定义状态:dp[i]表示前i个字符能否被拆分成单词。
- 状态转移:如果s[0:j]能被拆分,即dp[j]==true,且s[j:i]在单词列表中,则dp[i]为true,否则为false。
- 初始条件:长度为0的字符串可以拆分为单词,dp[0] = true。
- 返回结果:dp[n]。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Solution { public boolean wordBreak(String s, List<String> wordDict) { int n = s.length(); boolean[] dp = new boolean[n + 1]; dp[0] = true; HashSet<String> set = new HashSet<>(wordDict); for (int i = 1; i <= n; i++) { for (int j = 0; j < i; j++) { if (dp[j] == true && set.contains(s.substring(j, i))) { dp[i] = true; } } } return dp[n]; } }
|
- 时间复杂度:$O(n ^ 2)$,一共有 O(n) 个状态需要计算,每次计算需要枚举 O(n) 个分割点,哈希表判断一个字符串是否出现在给定的字符串列表需要 O(1) 的时间。
- 空间复杂度:O(n),dp数组所占空间。
动态规划
- 定义状态:dp[i]表示凑成i元所需要的最少硬币数。
- 状态转移:枚举硬币面值,dp[i] = min(dp[i], dp[i-num]+1)。
- 初始条件:dp[i] = amount+1,dp[0] = 0,凑成0需要0个硬币。
- 返回结果:如果dp[amount]等于amount+1,说明不符合条件,返回-1,否则返回dp[amount]。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Solution { public int coinChange(int[] coins, int amount) { int[] dp = new int[amount + 1]; int max = amount + 1; Arrays.fill(dp, max); dp[0] = 0; for (int coin : coins) { for (int i = coin; i <= amount; i++) { dp[i] = Math.min(dp[i], dp[i-coin] + 1); } } return dp[amount] == max ? -1 : dp[amount]; } }
|
- 时间复杂度:$O(n \times amount)$
- 空间复杂度:O(amount)
动态规划
- 定义状态:dp[i]表示以nums[i]为结尾的最长递增子序列。
- 状态转移:枚举j,如果nums[i] > nums[j],更新状态dp[i] = max(dp[i], dp[j] + 1)。
- 初始条件:dp[i] = 1.
- 返回结果:维护一个最大值变量,记录过程中的最长递增子序列长度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class Solution { public int lengthOfLIS(int[] nums) { int n = nums.length; int[] dp = new int[n]; Arrays.fill(dp, 1); int ans = 1; for (int i = 1; i < n; i++) { for (int j = 0; j < i; j++) { if (nums[i] > nums[j]) { dp[i] = Math.max(dp[i], dp[j] + 1); ans = Math.max(ans, dp[i]); } } } return ans; } }
|
- 时间复杂度:$O(n^2)$
- 空间复杂度:O(n)
动态规划
- 定义状态:dp[j]表示到达当前行第i列的最小路径和。
- 状态转移:第i行有i+1个数字,如果j > 0,dp[j] = min(dp[j-1], dp[j]) + triangle[i][j];否则dp[j] = dp[j] + triangle[i][j]。
- 初始条件:dp[0] = triangle[0][0]。
- 返回结果:最后一行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
| class Solution { public int minimumTotal(List<List<Integer>> triangle) { int n = triangle.size(); int[] dp = new int[n]; Arrays.fill(dp, Integer.MAX_VALUE); dp[0] = triangle.get(0).get(0); for (int i = 1; i < n; i++) { for (int j = i; j > 0; j--) { dp[j] = Math.min(dp[j-1], dp[j]) + triangle.get(i).get(j); } dp[0] += triangle.get(i).get(0); } int ans = Integer.MAX_VALUE; for (int i = 0; i < n; i++) { ans = Math.min(ans, dp[i]); }
return ans; } }
|
- 时间复杂度:$O(n^2)$
- 空间复杂度:O(n)
动态规划
- 定义状态:dp[j]表示走到当前行第j列的最小路径和。
- 状态转移:dp[0] = dp[0] + grid[i][0],dp[j] = min(dp[j-1], dp[j]) + grid[i][j]。
- 初始条件:第一行只能是向右走得到的,即dp[j] = dp[j-1] + grid[0][j]。
- 返回结果:dp[n-1]。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Solution { public int minPathSum(int[][] grid) { int m = grid.length; int n = grid[0].length; int[] dp = new int[n]; dp[0] = grid[0][0]; for (int j = 1; j < n; j++) { dp[j] = dp[j-1] + grid[0][j]; } for (int i = 1; i < m; i++) { dp[0] += grid[i][0]; for (int j = 1; j < n; j++) { dp[j] = Math.min(dp[j-1], dp[j]) + grid[i][j]; } } return dp[n - 1]; } }
|
动态规划
- 定义状态:dp[j]表示走到当前行第j列的路径数。
- 状态转移:如果grid[i][j] == 1,则无法到达当前位置,dp[j] = 0.否则,dp[0] += 1,dp[j] += dp[j-1]。
- 初始条件:第一行只能是向右走得到的,即dp[j] = 1,但如果存在一个位置有障碍物,其后的都为0。
- 返回结果:dp[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 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| class Solution { public int uniquePathsWithObstacles(int[][] obstacleGrid) { int m = obstacleGrid.length; int n = obstacleGrid[0].length; int[] dp = new int[n]; dp[0] = (obstacleGrid[0][0] + 1) % 2; for (int i = 1; i < n; i++) { if (obstacleGrid[0][i] == 1) { dp[i] = 0; } else { dp[i] = dp[i-1]; } } for (int i = 1; i < m; i++) { if (obstacleGrid[i][0] == 1) { dp[0] = 0; } for (int j = 1; j < n; j++) { if (obstacleGrid[i][j] == 1) { dp[j] = 0; } else { dp[j] = dp[j-1] + dp[j]; } } } return dp[n-1]; } }
|
动态规划
- 定义状态:dp[i][j]表示s[i:j]是否是回文的。
- 状态转移:$s[i] = s[j], dp[i][j] = \begin{cases} true & j-i \leq 2 \cr dp[i+1][j - 1] & else \end{cases}$
- 初始条件:dp[i][j] = false.
- 返回结果:维护一个最大长度变量和起始点。
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
| class Solution { public String longestPalindrome(String s) { int n = s.length(); int maxStart = 0, maxLen = 1; boolean[][] dp = new boolean[n][n]; for (int j = 1; j < n; j++) { for (int i = 0; i < j; i++) { if (s.charAt(i) == s.charAt(j)){ dp[i][j] = (j - i <= 2) || dp[i+1][j-1]; if (dp[i][j] && j - i + 1 > maxLen) { maxLen = j - i + 1; maxStart = i; } } } } return s.substring(maxStart, maxStart + maxLen); } }
|
- 时间复杂度:$O(n^2)$
- 空间复杂度:$O(n^2)$
中心扩散法
从每一个位置出发,向两边扩散即可。遇到不是回文的时候结束。
- 首先往左寻找与当前位置相同的字符,直到遇到不相等为止。
- 然后往右寻找与当前位置相同的字符,直到遇到不相等为止。
- 最后左右双向扩散,直到左和右不相等。
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
| class Solution { public String longestPalindrome(String s) { int n = s.length(); int maxStart = 0, maxLen = 1; for (int i = 0; i < n; i++) { int left = i - 1, right = i + 1; while (left >= 0 && s.charAt(left) == s.charAt(i)) { left--; } while (right < n && s.charAt(right) == s.charAt(i)) { right++; } while (left >= 0 && right < n && s.charAt(left) == s.charAt(right)) { left--; right++; } if (right - left - 1 > maxLen) { maxLen = right - left - 1; maxStart = left + 1; } } return s.substring(maxStart, maxStart + maxLen); } }
|
- 时间复杂度:$O(n^2)$
- 空间复杂度:O(1)
Manacher 算法
很难理解,之后看看之前做的。
动态规划
- 定义状态:dp[i][j]表示s1的前i个字符和s2的前j个字符能否交错组成s3的前i+j个字符。
- 状态转移:如果s1[i] == s3[i+j],则dp[i][j] = dp[i-1][j];如果s2[j] == s3[i+j],则dp[i][j] = dp[i][j-1]。
- 初始条件:dp[0][0] = true。
- 返回结果:dp[m][n]。
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
| class Solution { public boolean isInterleave(String s1, String s2, String s3) { int m = s1.length(); int n = s2.length(); int t = s3.length(); if (m + n != t) { return false; } boolean[][] dp = new boolean[m + 1][n + 1]; dp[0][0] = true; for (int i = 0; i <= m; i++){ for (int j = 0; j <= n; j++) { int p = i + j - 1; if (i > 0) { dp[i][j] = dp[i][j] || (dp[i-1][j] && s1.charAt(i-1) == s3.charAt(p)); } if (j > 0) { dp[i][j] = dp[i][j] || (dp[i][j-1] && s2.charAt(j-1) == s3.charAt(p)); } } } return dp[m][n]; } }
|
空间优化
使用滚动数组优化空间:
- 定义状态:dp[j]表示s1的前i个字符和s2的前j个字符能否交错组成s3的前i+j个字符。
- 状态转移:如果s1[i] == s3[i+j],则dp[j] = dp[j];如果s2[j] == s3[i+j],则dp[j] = dp[j-1]。
- 初始条件:dp[0] = true。
- 返回结果:dp[n]。
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
| class Solution { public boolean isInterleave(String s1, String s2, String s3) { int m = s1.length(); int n = s2.length(); int t = s3.length(); if (m + n != t) { return false; } boolean[] dp = new boolean[n + 1]; dp[0] = true; for (int i = 0; i <= m; i++){ for (int j = 0; j <= n; j++) { int p = i + j - 1; if (i > 0) { dp[j] &= (s1.charAt(i - 1) == s3.charAt(p)); } if (j > 0) { dp[j] |= (dp[j-1] && s2.charAt(j - 1) == s3.charAt(p)); } } } return dp[n]; } }
|
动态规划
- 定义状态:dp[i][j]表示将word1的前i个字符转换成word2的前j个字符所需要的最少操作数。
- 状态转移:如果word1[i-1] != word2[j-1]:
- 插入:dp[i-1][j] + 1
- 删除:dp[i][j-1] + 1
- 替换:dp[i-1][j-1] + 1
- 三种情况取最小值。
- 初始条件:dp[0][0]=0,dp[0][j] = j, dp[i][0] = i。
- 返回结果:dp[m][n]。
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
| class Solution { public int minDistance(String word1, String word2) { int m = word1.length(); int n = word2.length(); int[][] dp = new int[m + 1][n + 1]; for (int i = 0; i <= m; i++) { dp[i][0] = i; } for (int j = 0; j <= n; j++) { dp[0][j] = j; } for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (word1.charAt(i-1) != word2.charAt(j-1)) { dp[i][j] = Math.min(Math.min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]) + 1; } else { dp[i][j] = dp[i-1][j-1]; } } } return dp[m][n]; } }
|
动态规划
- 定义状态:buy1、sell1、buy2、sell2,表示每次操作后得到的利润。
- 状态转移
- buy1 = max(buy1, -prices[i]);
- sell1 = max(sell1, buy1 + prices[i]);
- buy2 = max(buy2, sell1 - prices[i]);
- sell2 = max(sell2, buy2 + prices[i]);
- 初始条件:
- buy1 = -prices[0]
- sell1 = 0 表示同一天买入又卖出
- buy2 = -prices[0] 表示同一天买入又卖出又买入
- sell2 = 0 表示同一天买入又卖出又买入又卖出
- 返回结果:max(0, sell1, sell2),如果最优的情况对应的是恰好一笔交易,那么它也会因为我们在转移时允许在同一天买入并且卖出这一宽松的条件,从 sell1转移至sell2,因此最终返回sell2即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Solution { public int maxProfit(int[] prices) { int n = prices.length; int buy1 = -prices[0], sell1 = 0; int buy2 = -prices[0], sell2 = 0; for (int i = 1; i < n; i++) { buy1 = Math.max(buy1, -prices[i]); sell1 = Math.max(sell1, buy1 + prices[i]); buy2 = Math.max(buy2, sell1 - prices[i]); sell2 = Math.max(sell2, buy2 + prices[i]); } return sell2; } }
|
动态规划
- 定义状态:dp[j][0]表示第j次交易后不持有股票的最大利润,dp[j][1]表示第j次交易后持有股票的最大利润。
- 状态转移:注意,j从后向前遍历,可以避免覆盖上一轮的状态。
- dp[j][0] = max(dp[j-1][0], dp[j-1][1] + prices[i])
- dp[j][1] = max(dp[j-1][1], dp[j-1][0] - prices[i])
- 初始条件:dp[j][0] = 0,dp[j][1] =负无穷.
- 返回结果:dp[k][0]。
取k值部分是因为,一共有n个价格,最多交易次数就是n/2,因此k>n/2没有意义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Solution { public int maxProfit(int k, int[] prices) { int n = prices.length; k = Math.min(k, n / 2); int[][] dp = new int[k + 1][2]; for (int j = 0; j <= k; j++) { dp[j][1] = Integer.MIN_VALUE; } for (int i = 0; i < n; i++) { for (int j = k; j > 0; j--) { dp[j][0] = Math.max(dp[j][0], dp[j][1] + prices[i]); dp[j][1] = Math.max(dp[j][1], dp[j - 1][0] - prices[i]); } } return dp[k][0]; } }
|
- 时间复杂度:$O(n \cdot min(n,k))$
- 空间复杂度:$O(min(n,k))$
动态规划
- 划分阶段:按照正方形的右下角坐标进行阶段划分。
- 定义状态:dp[i][j]表示以(i,j)为右下角的且值包含1的正方形的最大边长。
- 状态转移方程:如果matrix[i][j] == 0,则dp[i][j] = 0;否则dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1。
- 初始条件:dp[i][j] = 0。边界条件:i == 0 || j == 0。即当matrix[i][j] = 0时,无需进行操作,因为初始化为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
| class Solution { public int maximalSquare(char[][] matrix) { int m = matrix.length; int n = matrix[0].length; int[][] dp = new int[m][n]; int ans = 0; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (matrix[i][j] == '1') { if (i == 0 || j == 0) { dp[i][j] = 1; } else { dp[i][j] = Math.min(Math.min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]) + 1; } ans = Math.max(ans, dp[i][j]); } } } return ans * ans; } }
|
总结
面试150题除了前缀树基本都做完了,和hot100有大量重合的题目。之后就不再刷leetcode了,开始找真题刷,然后每天复习之前做过的这些经典题目。