算法刷题笔记:一维DP没那么难,状态想清楚就赢了一半

算法刷题笔记:一维DP没那么难,状态想清楚就赢了一半

哈喽大家好!今天我们聊聊一维动态规划,这可是算法里的重头戏。

很多同学听到DP就头疼,其实动态规划的核心就一句话:记住你算过的东西,别重复算。听起来简单,但怎么"记住"、"记住什么",才是精髓。

今天我带你用这些题,从入门到进阶,慢慢体会DP的美妙。准备好了吗?出发!


1. 爬楼梯

题目:假设你正在爬楼梯,需要 n 阶才能到达楼顶。每次你可以爬 1 或 2 个台阶。问有多少种不同的方法可以爬到楼顶?

思路过程

第一步:暴力枚举? 假设 n=5,我们可以枚举所有走法:

  • 1+1+1+1+1
  • 1+1+1+2
  • 1+2+2
  • 2+1+2
  • 2+2+1

等等,这也太多了吧!而且n越大,枚举量指数级爆炸。

第二步:换个角度想 走到第5阶的最后一步,可能是从第4阶走1步上来的,也可能是从第3阶走2步上来的。

也就是说:

  • 走到第5阶的方法数 = 走到第4阶的方法数 + 走到第3阶的方法数

这不就是斐波那契数列吗!

第三步:定义状态 dp[i] = 走到第 i 阶的方法数

第四步:找转移方程 dp[i] = dp[i-1] + dp[i-2]

解释:走到第i阶,要么从i-1走1步,要么从i-2走2步。

初始条件

  • dp[1] = 1(只有一种走法:走1步)
  • dp[2] = 2(两种走法:1+1 或者 直接走2步)

代码

java 复制代码
/**
 * 爬楼梯
 * 时间复杂度:O(n)
 * 空间复杂度:O(1) - 只用两个变量滚动
 */
public int climbStairs(int n) {
    if (n <= 2) {
        return n;
    }
    
    int prev1 = 2;  // dp[i-1]
    int prev2 = 1;  // dp[i-2]
    
    for (int i = 3; i <= n; i++) {
        int cur = prev1 + prev2;  // dp[i] = dp[i-1] + dp[i-2]
        prev2 = prev1;            // 更新 dp[i-2]
        prev1 = cur;              // 更新 dp[i-1]
    }
    
    return prev1;
}

复杂度分析

  • 时间复杂度:O(n),循环n次
  • 空间复杂度:O(1),只用了两个变量(可以优化到不用数组)

一句话总结

爬楼梯本质是斐波那契数列,关键发现:走到第i阶 = 走到i-1阶 + 走到i-2阶。


2. 杨辉三角

题目:给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。

思路过程

第一步:观察规律

markdown 复制代码
     1
    1 1
   1 2 1
  1 3 3 1
 1 4 6 4 1

第二步:找规律 每个数是它左上方右上方两个数的和:

  • 第3行第2个数字2 = 1 + 1(来自第2行的两个1)
  • 第4行第2个数字3 = 1 + 2(来自第3行的1和2)

第三步:定义状态 dp[i][j] = 杨辉三角第 i 行第 j 列的值(i从0开始计数)

第四步:转移方程

  • 每行第一个和最后一个都是1:dp[i][0] = dp[i][i] = 1
  • 中间数字:dp[i][j] = dp[i-1][j-1] + dp[i-1][j]

代码

java 复制代码
/**
 * 杨辉三角
 * 时间复杂度:O(n²)
 * 空间复杂度:O(n²)
 */
public List<List<Integer>> generate(int numRows) {
    List<List<Integer>> triangle = new ArrayList<>();
    
    for (int i = 0; i < numRows; i++) {
        List<Integer> row = new ArrayList<>();
        
        for (int j = 0; j <= i; j++) {
            // 每行第一个和最后一个都是1
            if (j == 0 || j == i) {
                row.add(1);
            } else {
                // 中间数字 = 左上 + 右上
                int val = triangle.get(i - 1).get(j - 1) 
                        + triangle.get(i - 1).get(j);
                row.add(val);
            }
        }
        triangle.add(row);
    }
    
    return triangle;
}

复杂度分析

  • 时间复杂度:O(n²),生成n行的三角形
  • 空间复杂度:O(n²),存储完整三角形

一句话总结

杨辉三角的精髓:每个数等于上方两数之和,用二维DP逐行计算即可。


3. 打家劫舍

题目:你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有现金,但相邻的房屋装有防盗系统,如果两间相邻的房屋在同一晚上被闯入,会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下,一晚上能够偷窃到的最高金额。

思路过程

第一步:简化问题 想象你从第一间房子开始走,遇到每间房子都要决定:偷还是不偷?

  • 如果偷了第i间,那第i-1间就不能偷
  • 如果不偷第i间,那就可以自由决定前面怎么偷

第二步:定义状态 dp[i] = 偷到第 i 间房子时,能偷到的最大金额(包括第i间

等等,这里有个问题要考虑清楚:

  • dp[i]考虑前i间房子能偷到的最大金额(不一定要偷第i间)

那我换个说法: dp[i] = 考虑第0到第i间房子,能偷到的最大金额

第三步:找转移方程 对于第i间房子(i >= 1),只有两种选择:

  1. 不偷第i间dp[i] = dp[i-1]
  2. 偷第i间 :那第i-1间不能偷,所以最大金额是 dp[i-2] + nums[i]

取最大值:dp[i] = max(dp[i-1], dp[i-2] + nums[i])

第四步:初始条件

  • dp[0] = nums[0](只有第一间,偷它)
  • dp[1] = max(nums[0], nums[1])(两间挑贵的偷)

代码

java 复制代码
/**
 * 打家劫舍
 * 时间复杂度:O(n)
 * 空间复杂度:O(n),可优化到O(1)
 */
public int rob(int[] nums) {
    if (nums == null || nums.length == 0) {
        return 0;
    }
    if (nums.length == 1) {
        return nums[0];
    }
    
    int n = nums.length;
    int[] dp = new int[n];
    dp[0] = nums[0];
    dp[1] = Math.max(nums[0], nums[1]);
    
    for (int i = 2; i < n; i++) {
        // 两种选择:不偷这间(继承dp[i-1])或偷这间(dp[i-2] + nums[i])
        dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
    }
    
    return dp[n - 1];
}

// 空间优化版本
public int rob_optimized(int[] nums) {
    if (nums == null || nums.length == 0) return 0;
    if (nums.length == 1) return nums[0];
    
    int prev2 = nums[0];                      // dp[i-2]
    int prev1 = Math.max(nums[0], nums[1]);    // dp[i-1]
    
    for (int i = 2; i < nums.length; i++) {
        int cur = Math.max(prev1, prev2 + nums[i]);
        prev2 = prev1;
        prev1 = cur;
    }
    
    return prev1;
}

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n),可优化到 O(1)

一句话总结

经典的"选或不选"DP:当前房子不偷则继承前一个状态,偷了就要跳过前一个房子。


4. 完全平方数

题目 :给定正整数 n,找到若干个完全平方数(如 1, 4, 9, 16 ...)使得它们的和等于 n。你需要让组成 n 的完全平方数的数量最少。返回最少需要几个完全平方数。

思路过程

第一步:问题转换 这道题可以这样想:凑出数字n,每次可以选择一个完全平方数,问最少选几次。

这不就是经典的完全背包问题吗?

第二步:定义状态 dp[i] = 凑出数字 i 需要的最少完全平方数个数

第三步:找转移方程 对于数字 i,假设我们选择了一个完全平方数 j*j(j从1到√i):

  • 在选了 jj 的情况下,还需要凑出 i - jj
  • 所以总个数 = 1 + dpi - j\*j

遍历所有可能的 j,取最小值: dp[i] = min(dp[i], 1 + dp[i - j*j])

第四步:初始条件 dp[0] = 0(数字0需要0个完全平方数)

代码

java 复制代码
/**
 * 完全平方数
 * 时间复杂度:O(n * √n)
 * 空间复杂度:O(n)
 */
public int numSquares(int n) {
    int[] dp = new int[n + 1];
    
    // 初始化:假设都需要n个(最大值)
    Arrays.fill(dp, n);
    dp[0] = 0;
    
    for (int i = 1; i <= n; i++) {
        // 遍历所有可能的完全平方数
        for (int j = 1; j * j <= i; j++) {
            // dp[i] = min(dp[i], 1 + dp[i - j*j])
            dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
        }
    }
    
    return dp[n];
}

复杂度分析

  • 时间复杂度:O(n * √n),外层n次,内层最多√n次
  • 空间复杂度:O(n)

一句话总结

把问题转化为完全背包:数字i可以由jj和i-jj组成,dpi取所有可能中的最小值。


5. 零钱兑换

题目 :给你一个整数数组 coins 表示不同面额的硬币,和一个整数 amount 表示总金额。计算并返回凑成总金额所需的最少硬币个数 。如果没有任何硬币组合能组成总金额,返回 -1。每种硬币的数量是无限的。

思路过程

上一题的思路完全适用!

第一步:确认背包类型 和完全平方数一样,硬币数量无限,是完全背包问题。

第二步:定义状态 dp[i] = 凑出金额 i 需要的最少硬币个数

第三步:转移方程 对于金额 i,遍历所有硬币面额: dp[i] = min(dp[i], 1 + dp[i - coin])

第四步:初始条件 dp[0] = 0(金额0需要0个硬币) 其他初始化为一个很大的数,表示"凑不出来"

代码

java 复制代码
/**
 * 零钱兑换
 * 时间复杂度:O(amount * len(coins))
 * 空间复杂度:O(amount)
 */
public int coinChange(int[] coins, int amount) {
    if (amount == 0) return 0;
    
    int[] dp = new int[amount + 1];
    
    // 初始化:金额i默认凑不出来,设为一个大数
    Arrays.fill(dp, amount + 1);
    dp[0] = 0;  // 金额0需要0个硬币
    
    for (int i = 1; i <= amount; i++) {
        for (int coin : coins) {
            if (i - coin >= 0) {
                // 选了这枚硬币后,还剩 i-coin
                dp[i] = Math.min(dp[i], dp[i - coin] + 1);
            }
        }
    }
    
    // 检查是否真的能凑出来
    return dp[amount] > amount ? -1 : dp[amount];
}

复杂度分析

  • 时间复杂度:O(amount * len(coins))
  • 空间复杂度:O(amount)

一句话总结

和完全平方数一模一样的套路:遍历每种硬币,更新dpi = min(dpi, dpi-coin + 1)。


6. 单词拆分

题目:给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判断 s 能否被空格拆分成一个或多个在字典中出现的单词。说明:拆分时可以重复使用字典中的单词。

思路过程

第一步:理解题意 比如 s = "leetcode",wordDict = "leet", "code"

  • "leetcode" 可以拆分成 "leet" + "code"
  • 所以返回 true

第二步:定义状态 dp[i] = 字符串 s 的前 i 个字符(s0:i)能否被拆分成字典中的单词

第三步:找转移方程 对于位置 i,遍历所有可能的分割点 j:

  • 如果 dp[j] = true(前j个字符能拆分)
  • s[j:i] 在字典中(substring从j到i-1)
  • 那么 dp[i] = true

第四步:优化 判断字符串是否在字典中,用哈希集合O(1)查找。

代码

java 复制代码
/**
 * 单词拆分
 * 时间复杂度:O(n² * m),n是字符串长度,m是字典单词平均长度(substring的O(m))
 * 空间复杂度:O(n + m),dp数组 + 哈希集合
 */
public boolean wordBreak(String s, List<String> wordDict) {
    // 哈希集合:O(1) 查找
    Set<String> dict = new HashSet<>(wordDict);
    
    int n = s.length();
    boolean[] dp = new boolean[n + 1];
    dp[0] = true;  // 空字符串可以被拆分
    
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j < i; j++) {
            // 如果前j个字符能拆分,且 s[j:i] 在字典中
            if (dp[j] && dict.contains(s.substring(j, i))) {
                dp[i] = true;
                break;  // 找到一个就够了
            }
        }
    }
    
    return dp[n];
}

复杂度分析

  • 时间复杂度:O(n²),虽然有substring操作,但字符串不可变所以实际操作会稍慢
  • 空间复杂度:O(n + m),dp数组 + 哈希集合

一句话总结

把字符串切成两半,前半部分能拆分 + 后半部分是单词 = 前i个字符能拆分。


7. 最长递增子序列

题目 :给你一个整数数组 nums,找到其中最长严格递增子序列的长度。子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。

思路过程

第一步:理解"子序列" 子序列和子数组不同,子数组要求连续,子序列只需要保持相对顺序。 比如 3, 5, 7, 1 的子序列可以是 3, 5, 73, 75, 7

第二步:定义状态 dp[i] = 以 numsi 结尾的最长递增子序列长度

为什么要"以numsi结尾"?因为我们需要用一个明确的结尾来定义子序列,这样转移才有方向。

第三步:找转移方程 对于每个 i,遍历它之前的所有 j(j < i):

  • 如果 nums[j] < nums[i](可以接在后面)
  • 那么 dp[i] = max(dp[i], dp[j] + 1)

第四步:优化------二分查找 上面的方法时间复杂度是 O(n²)。有没有办法优化?

我们可以用贪心 + 二分

  • 维护一个有序数组 tailk,表示长度为 k+1 的递增子序列的最小结尾元素
  • 遍历每个元素,用二分找到它应该插入的位置
  • 最终数组长度就是答案

这个优化版本时间复杂度 O(n log n),但比较难理解,先掌握 O(n²) 的基础版本!

代码

java 复制代码
/**
 * 最长递增子序列 - 基础DP版本
 * 时间复杂度:O(n²)
 * 空间复杂度:O(n)
 */
public int lengthOfLIS_basic(int[] nums) {
    if (nums == null || nums.length == 0) return 0;
    
    int n = nums.length;
    int[] dp = new int[n];
    
    // 初始化:每个元素自己就是一个长度为1的子序列
    Arrays.fill(dp, 1);
    int maxLen = 1;
    
    for (int i = 1; i < n; i++) {
        for (int j = 0; j < i; j++) {
            // 如果 nums[j] < nums[i],可以把 nums[i] 接在后面
            if (nums[j] < nums[i]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
        maxLen = Math.max(maxLen, dp[i]);
    }
    
    return maxLen;
}

/**
 * 最长递增子序列 - 二分优化版本
 * 时间复杂度:O(n log n)
 * 空间复杂度:O(n)
 */
public int lengthOfLIS(int[] nums) {
    if (nums == null || nums.length == 0) return 0;
    
    // tail[i] 存储长度为 i+1 的递增子序列的最小结尾元素
    int[] tail = new int[nums.length];
    int size = 0;  // 当前有序数组的长度
    
    for (int num : nums) {
        // 用二分找到 num 应该插入的位置
        int left = 0, right = size;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (tail[mid] < num) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        
        // 插入或替换
        tail[left] = num;
        
        // 如果插入位置是末尾,说明找到了更长的子序列
        if (left == size) {
            size++;
        }
    }
    
    return size;
}

复杂度分析

  • 基础版本:时间 O(n²),空间 O(n)
  • 二分版本:时间 O(n log n),空间 O(n)

一句话总结

以每个元素结尾定义状态,遍历前面所有元素找能接在后面的最大值;二分优化用有序数组维护最小结尾。


8. 乘积最大子数组

题目 :给你一个整数数组 nums,请你找出数组中乘积最大的连续子数组(子数组最少包含一个元素),返回其最大乘积。

思路过程

第一步:直觉反应 最大值?那直接遍历找最大乘积不就行了?

不行! 因为乘法有负数:

  • -2, 3, -4 的最大乘积是 24(3 * -4 * 3 = -36?不对)
  • 实际上 -2, 3, -4 中,3, -4 乘积是 -12,3 是 3
  • 但如果全是负数呢?-1, -2, -3 乘积最大是 6(-1 * -2 * -3 = -6,不对)

等等,让我重新算:

  • -1, -2, -3:最大乘积是 6(-2 * -3 = 6)

关键发现两个负数相乘得正数

所以我们不仅要记住最大值 ,还要记住最小值

第二步:定义状态 maxDp[i] = 以 numsi 结尾的最大乘积 minDp[i] = 以 numsi 结尾的最小乘积

第三步:找转移方程 对于 numsi,有三种情况:

  1. 单独作为子数组:nums[i]
  2. 和前面的最大值相乘:nums[i] * maxDp[i-1]
  3. 和前面的最小值相乘:nums[i] * minDp[i-1]

取最大:maxDp[i] = max(nums[i], nums[i] * maxDp[i-1], nums[i] * minDp[i-1]) 取最小:minDp[i] = min(nums[i], nums[i] * maxDp[i-1], nums[i] * minDp[i-1])

第四步:答案 遍历所有位置,取 maxDpi 的最大值

代码

java 复制代码
/**
 * 乘积最大子数组
 * 时间复杂度:O(n)
 * 空间复杂度:O(1) - 只需两个变量
 */
public int maxProduct(int[] nums) {
    if (nums == null || nums.length == 0) return 0;
    
    int maxProd = nums[0];      // 全局最大乘积
    int curMax = nums[0];       // 以当前元素结尾的最大乘积
    int curMin = nums[0];       // 以当前元素结尾的最小乘积
    
    for (int i = 1; i < nums.length; i++) {
        // 乘以当前元素可能改变正负,所以要同时保存之前的max和min
        int tempMax = curMax;
        int tempMin = curMin;
        
        // 三种情况取最大
        curMax = Math.max(nums[i], 
                    Math.max(tempMax * nums[i], tempMin * nums[i]));
        // 三种情况取最小
        curMin = Math.min(nums[i], 
                    Math.min(tempMax * nums[i], tempMin * nums[i]));
        
        // 更新答案
        maxProd = Math.max(maxProd, curMax);
    }
    
    return maxProd;
}

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

一句话总结

因为负数乘负数得正数,所以要同时维护最大和最小乘积,用当前元素分别与两者相乘后取最大/最小。


9. 分割等和子集

题目 :给定一个只包含正整数的非空数组 nums。判断是否可以将这个数组分成两个子集,使得两个子集的元素和相等。

思路过程

第一步:转换问题

  • 两个子集元素和相等 → 总和是偶数
  • 找子集使得元素和 = 总和/2 → 背包问题

本质上:能不能从数组中挑一些数字,它们的和恰好等于 target = sum/2?

第二步:定义状态 dp[i] = 是否能凑出和为 i(true/false)

第三步:转移方程 对于每个数字 num,遍历和(倒序!重要!):

  • 如果 dp[j - num] = true,那 dp[j] 也能凑出来
  • dp[j] = dp[j] || dp[j - num]

为什么要倒序? 因为正序会导致同一个数字被重复使用多次。比如 num=1,target=3:

  • 正序:dp1 = true → dp2 = true → dp3 = true(用了三次1)
  • 倒序:dp3 检查时,dp2 还是旧值,不会重复用

第四步:初始条件 dp[0] = true(和为0可以通过不选任何元素凑出)

代码

java 复制代码
/**
 * 分割等和子集
 * 时间复杂度:O(n * target)
 * 空间复杂度:O(target)
 */
public boolean canPartition(int[] nums) {
    int sum = 0;
    for (int num : nums) {
        sum += num;
    }
    
    // 总和为奇数,无法平分
    if (sum % 2 != 0) return false;
    
    int target = sum / 2;
    
    // dp[j] = 是否能凑出和为j
    boolean[] dp = new boolean[target + 1];
    dp[0] = true;  // 和为0,总能凑出来(什么都不选)
    
    for (int num : nums) {
        // 倒序遍历!防止同一个数字被重复使用
        for (int j = target; j >= num; j--) {
            dp[j] = dp[j] || dp[j - num];
        }
    }
    
    return dp[target];
}

复杂度分析

  • 时间复杂度:O(n * target)
  • 空间复杂度:O(target)

一句话总结

0-1背包的经典应用:能不能从数组中"选"一些数字凑出 target,注意背包问题要倒序遍历。


10. 最长有效括号

题目 :给你一个只包含 '(' 和 ')' 的字符串,找出其中最长有效括号子串的长度。

思路过程

第一步:理解有效括号

  • "()" 是有效的
  • "(())" 是有效的
  • "()()" 是有效的
  • ")(" 是无效的

第二步:定义状态 dp[i] = 以 si 结尾的最长有效括号长度

为什么要"以si结尾"?因为只有结尾确定了,我们才能判断有效括号在哪里结束。

第三步:找转移方程 分两种情况:

情况1:当前是 ')',前一个是 '('

scss 复制代码
...()
  • 直接拼接:dp[i] = dp[i-2] + 2
  • dpi-2 是前面已经有效的部分

情况2:当前是 ')',前一个是 ')'

复制代码
...))

这时候要往前找配对的 '('。如果 si-dp\[i-1-1] 是 '(',就能配对:

css 复制代码
... ( dp[i-1] )   )    ← i
      ↑
      这个位置的'('和s[i]配对
  • 配对后:dp[i] = dp[i-1] + 2
  • 还要加上前面的有效部分:+ dp[i - dp[i-1] - 2]

第四步:处理越界 记得检查 i-2 和 i-dpi-1-2 是否 >= 0

代码

java 复制代码
/**
 * 最长有效括号
 * 时间复杂度:O(n)
 * 空间复杂度:O(n)
 */
public int longestValidParentheses(String s) {
    if (s == null || s.length() == 0) return 0;
    
    int n = s.length();
    int[] dp = new int[n];
    
    int maxLen = 0;
    
    for (int i = 1; i < n; i++) {
        if (s.charAt(i) == ')') {
            // 情况1:...()
            if (s.charAt(i - 1) == '(') {
                dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
            } 
            // 情况2:...))
            else if (i - dp[i - 1] - 1 >= 0 
                     && s.charAt(i - dp[i - 1] - 1) == '(') {
                dp[i] = dp[i - 1] + 2 
                      + (i - dp[i - 1] - 2 >= 0 ? dp[i - dp[i - 1] - 2] : 0);
            }
        }
        maxLen = Math.max(maxLen, dp[i]);
    }
    
    return maxLen;
}

/**
 * 方法2:栈(更直观)
 * 时间复杂度:O(n)
 * 空间复杂度:O(n)
 */
public int longestValidParentheses_stack(String s) {
    int maxLen = 0;
    Stack<Integer> stack = new Stack<>();
    stack.push(-1);  // 初始基准位置
    
    for (int i = 0; i < s.length(); i++) {
        if (s.charAt(i) == '(') {
            stack.push(i);
        } else {
            stack.pop();
            if (stack.isEmpty()) {
                // 栈空,说明这个')'配不上,push当前索引作为新基准
                stack.push(i);
            } else {
                // 当前索引 - 栈顶索引 = 有效括号长度
                maxLen = Math.max(maxLen, i - stack.peek());
            }
        }
    }
    
    return maxLen;
}

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

一句话总结

以每个字符结尾定义状态:'()'直接拼接,'))'则找前面配对的'('并加上更前面的有效部分。


总结

今天我们学了这些一维动态规划题目,来回顾一下核心套路:

题目 状态定义 转移关键
爬楼梯 dpi = 到第i阶的方法数 dpi = dpi-1 + dpi-2
杨辉三角 dpij = 第i行j列的值 dpij = dpi-1j-1 + dpi-1j
打家劫舍 dpi = 前i间房的最多偷窃 dpi = max(dpi-1, dpi-2+numsi)
完全平方数 dpi = 凑出i的最少个数 遍历所有平方数取最小
零钱兑换 dpi = 凑出i的最少硬币 遍历所有硬币取最小
单词拆分 dpi = 前i字符能否拆分 dpi = dpj && dict.contains(sj:i)
最长递增子序列 dpi = 以numsi结尾的最长 遍历前面找能接的
乘积最大子数组 维护max和min两个状态 负负得正,同时记录最大和最小
分割等和子集 dpj = 能否凑出和j 0-1背包,倒序遍历
最长有效括号 dpi = 以si结尾的最长 分'()'和'))'两种情况

DP的核心

  1. 定义清楚状态 - "以...结尾"是最常用的套路
  2. 找准转移方程 - 当前状态和之前状态的关系
  3. 初始化别忘 - dp0 或者边界条件
  4. 优化要想到 - 空间滚动、数组变变量、二分优化

恭喜你又完成了一篇!如果觉得有用,欢迎点个赞,我们下期见!

相关推荐
Yiyaoshujuku1 小时前
化合物数据集API接口(数据结构及样例)
java·网络·数据结构
QiLinkOS1 小时前
极客与商业思维的融合实践(1)
c语言·数据库·c++·人工智能·算法·开源协议
fu的博客1 小时前
【数据结构16】图:基于邻接矩阵、邻接表实现DFS/BFS
数据结构·算法
阿正的梦工坊1 小时前
【Rust】17-Send、Sync 与并发安全抽象
算法·安全·rust
IceBing1 小时前
还在一个个连接 Arthas?这个开源平台支持批量诊断 JVM
java
菩提树下的凡夫1 小时前
新版OpenCV5.0在ONNX模型的推理应用
opencv·算法
SL_staff2 小时前
《如何用规则引擎替代if-else?JVS-Rules可视化编排比硬编码强在哪里?》
java·低代码·架构
Sam_Deep_Thinking2 小时前
java中的class到底是个什么东西?
java·开发语言·面试
swordbob2 小时前
Spring 3 级缓存解决循环依赖
java·spring