Leetcode 刷题记录 19 —— 动态规划

本系列为笔者的 Leetcode 刷题记录,顺序为 Hot 100 题官方顺序,根据标签命名,记录笔者总结的做题思路,附部分代码解释和疑问解答,01~07为C++语言,08及以后为Java语言。

01 爬楼梯

java 复制代码
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 杨辉三角

java 复制代码
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 打家劫舍

java 复制代码
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 完全平方数

java 复制代码
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 零钱兑换

java 复制代码
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,遍历icoinamount,执行:

复制代码
dp[i] = min(dp[i], dp[i - coin] + 1);

如果dp[i - coin]不可达(即是初始化的无穷大),则dp[i]不会被更新。

06 字符串拼接

java 复制代码
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 最长递增子序列

java 复制代码
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++)为什么不是ji0j--,不然感觉好奇怪,感觉不是"连续"的递增子序列?

题目求的是"最长递增子序列(LIS)",这里的子序列(subsequence)不是必须连续的。也就是说,元素的索引不必连续,只要保持递增顺序即可。

08 乘积最大子序列

java 复制代码
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 分割等和子集

java 复制代码
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=1
            for(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 最长有效括号

方法一:动态规划(简单)

java 复制代码
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;
    }
}

方法二:栈

java 复制代码
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;
    }
}
相关推荐
依然易冷10 分钟前
【APR-自动代码修复】论文分享:PyTy
算法
ggdpzhk29 分钟前
输入两个正整数,计算最大公约数和最小公倍数
java·算法
weixin_307779132 小时前
波动方程兼容性条件分析
算法
-qOVOp-2 小时前
408第二季 - 组成原理 - 流水线
数据结构·算法
Xの哲學3 小时前
hostapd状态机解析
linux·网络·算法·wireless
kukubuzai3 小时前
搜索二叉数(c++)
算法
快手技术3 小时前
效果 & 成本双突破!快手提出端到端生成式推荐系统 OneRec!
算法
HNU混子4 小时前
leetcode-3443. K次修改后的最大曼哈顿距离
算法·leetcode·动态规划
奔跑吧邓邓子4 小时前
解锁决策树:数据挖掘的智慧引擎
人工智能·算法·决策树·机器学习·数据挖掘