Leetcode 刷题记录 19 —— 动态规划
本系列为笔者的 Leetcode 刷题记录,顺序为 Hot 100 题官方顺序,根据标签命名,记录笔者总结的做题思路,附部分代码解释和疑问解答,01~07为C++语言,08及以后为Java语言。
01 爬楼梯
class Solution {public int climbStairs(int n) {int a = 0, b = 0, c = 1; //边界条件for(int i=0; i<n; i++){a = b;b = c;c = a + b; //转移方程}return c;}
}/**边界条件:a = 0, b = 0, c = 1;转移方程:c = a + b;*/
02 杨辉三角
class Solution {public List<List<Integer>> generate(int numRows) {List<List<Integer>> c = new ArrayList<>(numRows);c.add(List.of(1)); //边界条件//一行一行计算for(int i=1; i<numRows; i++){List<Integer> row = new ArrayList<>(i + 1);row.add(1);for(int j=1; j<i; j++){row.add(c.get(i - 1).get(j - 1) + c.get(i - 1).get(j)); //转移方程}row.add(1);c.add(row);}return c;}
}/**边界条件:c.add(List.of(1));转移方程:row.add(c.get(i - 1).get(j - 1) + c.get(i - 1).get(j)); */
① c.add(List.of(1));
啥意思?
List.of(1)
:这是Java 9引入的一个静态方法,用来快速创建一个不可变(immutable)的列表,这里创建了一个只包含元素1
的列表。
② List<List<Integer>> c = new ArrayList<>(numRows);
中(numRows)
啥意思?
这里的 numRows
作用是给 ArrayList
指定一个初始容量(initial capacity
),ArrayList
底层是用数组实现的,当你创建一个 ArrayList
时,如果指定了初始容量,ArrayList
会提前申请这么大容量的底层数组,避免插入元素时频繁扩容,提升效率。
03 打家劫舍
class Solution {public int rob(int[] nums) {//特殊情况判断if(nums.length == 0){return 0;}int n = nums.length;int[] dp = new int[n + 1];dp[0] = 0;dp[1] = nums[0]; //边界条件for(int i=2; i<=n; i++){dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i-1]); //转移方程}return dp[n];}
}/**边界条件:dp[0] = 0; dp[1] = nums[0];转移方程:dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i-1]);*/
04 完全平方数
class Solution {public int numSquares(int n) {int[] dp = new int[n + 1];for(int i=1; i<=n; i++){ //[1, n]dp[i] = i; //边界条件for(int j=1; j*j <= i; j++){ //[1, i]dp[i] = Math.min(dp[i], dp[i - j * j] + 1); //转移方程}}return dp[n];}
}/**边界条件:dp[i] = i;转移方程:dp[i] = Math.min(dp[i], dp[i - j * j] + 1);*/
05 零钱兑换
class Solution {public int coinChange(int[] coins, int amount) {int max = amount + 1;int[] dp = new int[amount + 1];Arrays.fill(dp, max);dp[0] = 0; //边界条件for(int i=1; i<=amount; i++){ //遍历dp数组for(int j=0; j<coins.length; j++){ //遍历coins数组if(coins[j] <= i){dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1); //转移方程}}}return dp[amount] > amount ? -1 : dp[amount];}
}/**边界条件:dp[0] = 0;转移方程:dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);*/
① Arrays.fill(dp, max);
啥意思?
dp
是一个数组,max
是一个变量,表示你想赋给数组每个元素的值,Arrays.fill
是 Java 标准库中的一个静态方法,用来快速初始化或重置数组的值。
② return dp[amount] > amount ? -1 : dp[amount];
为啥就这么确定能找出正确组合,万一是1、2、5
块的零钱,要求凑13
,凑出来5、5、5
块,返回-1
了怎么办?
dp[0] = 0
,其他dp[i]
初始化为一个较大值(通常是amount+1
或者类似的无穷大),对每一个硬币coin
,遍历i
从coin
到amount
,执行:
dp[i] = min(dp[i], dp[i - coin] + 1);
如果dp[i - coin]
不可达(即是初始化的无穷大),则dp[i]
不会被更新。
06 字符串拼接
class Solution {public boolean wordBreak(String s, List<String> wordDict) {Set<String> wordDictSet = new HashSet(wordDict);boolean[] dp = new boolean[s.length() + 1];dp[0] = true; //边界条件for(int i=1; i<=s.length(); i++){ //外层循环:i表示当前考虑字符串的前i个字符for(int j=0; j<i; j++){ //内层循环:j表示字符串拆分点if(dp[j] && wordDictSet.contains(s.substring(j, i))){ //转移方程dp[i] = true;break;}}}return dp[s.length()];}
}/**边界条件:dp[0] = true;转移方程:dp[j] && wordDictSet.contains(s.substring(j, i))*/
07 最长递增子序列
class Solution {public int lengthOfLIS(int[] nums) {if(nums.length == 0){return 0;}int ans = 1;int[] dp = new int[nums.length];dp[0] = 1;for(int i=1; i<nums.length; i++){dp[i] = 1;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;}
}/**边界条件:dp[0] = 1;转移方程:dp[i] = Math.max(dp[i], dp[j] + 1);*/
for(int j=0; j<i; j++)
为什么不是j
从i
到0
,j--
,不然感觉好奇怪,感觉不是“连续”的递增子序列?
题目求的是“最长递增子序列(LIS)”,这里的子序列(subsequence)不是必须连续的。也就是说,元素的索引不必连续,只要保持递增顺序即可。
08 乘积最大子序列
class Solution {public int maxProduct(int[] nums) {int max = Integer.MIN_VALUE;int imax = 1, imin = 1;for(int i=0; i<nums.length; i++){if(nums[i] < 0){int temp = imax;imax = imin;imin = temp;}imax = Math.max(nums[i], imax * nums[i]);imin = Math.min(nums[i], imin * nums[i]);max = Math.max(max, imax);}return max;}
}
09 分割等和子集
class Solution {public boolean canPartition(int[] nums) {int n = nums.length;if(n < 2){return false;}int sum = 0, maxNum = 0;for(int num : nums){sum += num;maxNum = Math.max(maxNum, num);}if(sum % 2 != 0){return false;}int target = sum / 2;if(maxNum > target){return false;}//dp[i][j]//i 遍历元素//j 遍历目标值boolean[][] dp = new boolean[n][target + 1];for(int i=0; i<n; i++){dp[i][0] = true;}dp[0][nums[0]] = true;for(int i=1; i<n; i++){ //出错:i=1for(int j=0; j<=target; j++){if(j >= nums[i]){dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];}else{dp[i][j] = dp[i-1][j];}}}return dp[n-1][target];}
}
10 最长有效括号
方法一:动态规划(简单)
class Solution {public int longestValidParentheses(String s) {int ans = 0;int[] dp = new int[s.length()];for(int i=1; i<s.length(); i++){if(s.charAt(i) == ')'){if(s.charAt(i-1) == '('){dp[i] = (i>=2 ? dp[i-2] : 0) + 2;}else if(i-dp[i-1] > 0 && s.charAt(i-dp[i-1]-1) == '('){dp[i] = dp[i-1] + (i-dp[i-1] >= 2 ? dp[i-dp[i-1]-2] : 0) + 2;}ans = Math.max(ans, dp[i]);}}return ans;}
}
方法二:栈
class Solution {public int longestValidParentheses(String s) {int ans = 0;Deque<Integer> stack = new LinkedList<>(); //记录括号下标stack.push(-1); //初始化for(int i=0; i<s.length(); i++){if(s.charAt(i) == '('){stack.push(i);}else{stack.pop();if(stack.isEmpty()){stack.push(i); //记录最后一个未被匹配的右括号下标}else{ans = Math.max(ans, i - stack.peek()); //可能是左括号、可能是右括号}}}return ans;}
}