动态规划好难!题单:https://leetcode.cn/discuss/post/3581838/fen-xiang-gun-ti-dan-dong-tai-gui-hua-ru-007o/

1. 入门DP

1.1. 爬楼梯

70. 爬楼梯

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public int climbStairs(int n) {
// dp[i]表示爬了i阶楼梯,初始在第0阶,要爬到第n阶,实际上是个n+1长度的
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 2] + dp[i - 1];
}
return dp[n];
}
}

746. 使用最小花费爬楼梯

3693. 爬楼梯 II

377. 组合总和 Ⅳ

题目:给你一个由 不同 整数组成的数组 nums,和一个目标整数 target。请你从 nums 中找出并返回总和为 target 的元素排列的个数。

注意,题目说的是求元素排列的个数,因此是一种爬楼梯的题目。

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 combinationSum4(int[] nums, int target) {
int n = nums.length;
// dp[i]表示和为i的排列个数,答案为dp[target]
int[] dp = new int[target + 1];
Arrays.fill(dp, 0);

// 总和为零,没有元素,此时算1,这可以从两方面考虑:
// 1. 直观上没有元素也算一种排列方式
// 2. 递推过程中没有出现常数,如果初始也为零,则永远为零
dp[0] = 1;
for (int i = 1; i <= target; i++) {
int sum = 0;
for (int j = 0; j < n; j++) {
if (i < nums[j]) { continue; }
sum += dp[i - nums[j]];
dp[i] = sum;
}
}
return dp[target];
}
}

2466. 统计构造好字符串的方案数

1.2. 打家劫舍

198. 打家劫舍

213. 打家劫舍 II

2320. 统计放置房子的方式数

740. 删除并获得点数

1.3. 最大子数组和

53. 最大子数组和

这个题还可以用前缀和+买卖股票的思想来做。因为子数组就等于两个前缀和的差,因此就转化为前缀和的股票价格,选两天买和卖。

2606. 找到最大开销的子字符串

考虑一下空字符串的价值为0,也可能为最大值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
private final Map<Character, Integer> mp = new HashMap<>();
public int maximumCostSubstring(String s, String chars, int[] vals) {
for (int i = 0; i < vals.length; i++) {
mp.put(chars.charAt(i), vals[i]);
}
int n = s.length();
int[] dp = new int[n + 1];
dp[0] = this.getValue(s.charAt(0));
int ans = Math.max(dp[0], 0); // 空字符串只需在此处考虑
for (int i = 1; i < n; i++) {
int val = this.getValue(s.charAt(i));
dp[i] = Math.max(val, dp[i - 1] + val);
ans = Math.max(dp[i], ans);
}
return ans;
}

private int getValue(char c) {
Integer val = mp.get(c);
return val == null ? (c - 'a' + 1) : val;
}
}

1749. 任意子数组和的绝对值的最大值

152. 乘积最大子数组

2. 网格图DP

待补充

3. 背包问题

背包问题不难,难的是边界的考虑。

注意外层循环和内层循环分别是什么。爬楼梯问题求的是“排列个数”,因此外层循环必须是容量,内层循环则在之前容量的基础上再选一个物品;01背包问题求的是“组合个数”,且每个物品只能选1次,放了防止物品多选,外层循环必须是物品个数。完全背包问题两种都可,因为物品可以重复选。

3.1. 01背包

416. 分割等和子集

494. 目标和

3.2. 完全背包

322. 零钱兑换

首先递归方程和**原数据 coins[]**的下标表示,用灵神的方法就是 dp[i+1][j] 表示 coins 取闭区间 [0:i] 和容量为 j 的情况,i 就是从 for(i=0;i<n;i++) 开始遍历的,相当于在 i 的维度上整体右移了一格。
这样就有两个方向的边界需要考虑:

  1. 物品个数的维度 idp[0][j] 实际上在递推中是不被计算的,但在第一轮 dp[i+1][j]=f(d[i][j]) 中会被用到,所以是需要初始化边界的
  2. 容量的维度 jdp[i][0] 在递推中也是不被计算的,但也可能会被用到,例如说某个硬币面值刚好等于总金额 dp[][j-coins[i]], j=coins[i]

那么怎么初始化边界呢?得看题目要我们求什么,这个题是求最少的硬币个数,所以上述两个边界问题可以这么考虑:

  1. dp[i][0](i>=0) 表示用 i 个硬币凑出 0,这必然有且仅有一种方案,就是啥也不选,但是此处并非初始化为 1,因为要求的是硬币个数,所以应当初始化为 0;该维度的初始化可以放在每次 j 的循环之前,但是 dp[0][0] 并不会被循环遍历到,所以还是要手动初始化至少 1 个值
  2. dp[0][j](j>0) 表示用 0 个硬币凑出 j>0 的硬币个数,很显然这是不可能做到的,是非法的,我们不希望这类各自被后续递推时用到。再看递推方程是取 Math.min() 的,因此我们只需将其初始化为 1 个很大的值,让合法值与非法值相比时,永远取合法值即可;当然也可能是两个非法值取 Math.min(),这样操作后仍为非法值,这是预期之内的;非法值的选择可以用最大值 amount+1,因为不可能用 amount+1 个硬币来凑出 amount
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public int coinChange(int[] coins, int amount) {
int n = coins.length;
// dp[i + 1][j]表示用闭区间[0,i]的硬币能否凑出j
int[][] dp = new int[n + 1][amount + 1];
// dp[0][]是边界,表示不用硬币凑出amount,需要无穷大个,用amount+1表示无穷大
Arrays.fill(dp[0], amount + 1);
dp[0][0] = 0; // 需要扣掉dp[0][0],这个操作无法再循环中完成,因为i+1至少是1
for (int i = 0; i < n; i++) {
dp[i + 1][0] = 0; // i维度的初始化可以放在循环之前
for (int j = 1; j <= amount; j++) {
// 1. 不选coins[i]
dp[i + 1][j] = dp[i][j];
// 2. 选coins[i]
if (coins[i] <= j) {
dp[i + 1][j] = Math.min(dp[i + 1][j], 1 + dp[i + 1][j - coins[i]]);
}
}
}
// 如果>amount,说明非法
return dp[n][amount] > amount ? -1 : dp[n][amount];
}
}

空间优化:我感觉空间优化还是得先写出二维的之后,从代码的角度进行优化。代码中每次转移是依赖于左下角 dp[i + 1][j - coins[i]] 的,对于依赖左下角和右上角的状态转移方程,我们用正序;依赖右下角或左上角的则用倒序,例如 01 背包。
如果要从完全背包的角度看,那就是每个硬币能取无数次,状态 dp[i + 1][j] 需要转移到仍能取本次硬币的 dp[i + 1][j - coins[i]],而非到无法取本次硬币的 dp[i][j - coins[i]],这就是 01 背包的方法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public int coinChange(int[] coins, int amount) {
int n = coins.length;
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
for (int i = 0; i < n; i++) {
dp[0] = 0;
for (int j = coins[i]; j <= amount; j++) {
dp[j] = Math.min(dp[j], 1 + dp[j - coins[i]]);
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}

518. 零钱兑换 II

这个题和上一题的区别完全只是换了个东西求,之前是求硬币的个数,现在是求方案数,所以说两类边界值的初始化值都变了。对于非法值要初始化为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
27
28
29
30
31
32
33
34
35
36
37
38
class Solution {
public int change(int amount, int[] coins) {
int n = coins.length;
// dp[i + 1][j] 表示用[0,i]的硬币凑出j的方案数
int[][] dp = new int[n + 1][amount + 1];
// dp[0][j]表示不用硬币凑出j的方案数,如果j不是0,是不可能办到的
Arrays.fill(dp[0], 0); //非法值不能是尽量大了,而是0了,因为求总和数量
dp[0][0] = 1; // 不用硬币凑出0的方案数
for (int i = 0; i < n; i++) {
dp[i + 1][0] = 1; // 不用硬币凑出0,必然是1种
for (int j = 1; j <= amount; j++) {
dp[i + 1][j] = dp[i][j];
if (coins[i] <= j) {
dp[i + 1][j] += dp[i + 1][j - coins[i]];
}
}
}
return dp[n][amount];
}
}

class Solution {
// 这个题是组合数,而不是硬币数
public int change(int amount, int[] coins) {
int n = coins.length;
// dp[i + 1][j] 表示用[0,i]的硬币凑出j的方案数
int[] dp = new int[amount + 1];
// dp[0][j]表示不用硬币凑出j的方案数,如果j不是0,是不可能办到的
Arrays.fill(dp, 0); //非法值不能是尽量大了,而是0了,因为求总和数量
for (int i = 0; i < n; i++) {
dp[0] = 1; // 不用硬币凑出0,必然是1种
for (int j = coins[i]; j <= amount; j++) {
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
}

279. 完全平方数

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 numSquares(int n) {
int target = n;
n = (int) Math.sqrt(target) + 1;
// dp[i][j]表示用前[0,i]凑出j的最小数量,答案是dp[n][target]
int[][] dp = new int[n + 1][target + 1];

// 1. dp[][]并非对任意i,j都有解,例如对i=0,j>0无解
// Arrays.fill(dp[0], Integer.MAX_VALUE);
// dp[0][0] = 0; // 0个数就能组成0
// 2. 或者你也可以把dp[1][j]全部先算出来,后面不使用dp[0][j]
for (int j = 0; j <= target; j++) {
dp[1][j] = j;
}
for (int i = 2; i <= n; i++) {
// j = 0时,dp[i][j] 必然为 dp[i - 1][j],也就是0
for (int j = 0; j <= target; j++) {

dp[i][j] = dp[i - 1][j]; // 不选i
if (i * i <= j) { // 选i,如果用到dp[][0],也会自动+1的
dp[i][j] = Math.min(dp[i][j], dp[i][j - i * i] + 1);
}
}
}

return dp[n][target];
}
}

空间优化,这种无需额外变量的空间优化只要考虑清楚是正序还是逆序就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public int numSquares(int n) {
int target = n;
n = (int) Math.sqrt(target) + 1;
int[] dp = new int[target + 1];
for (int j = 0; j <= target; j++) { dp[j] = j; }
for (int i = 2; i <= n; i++) {
for (int j = i * i; j <= target; j++) {
dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
}
}
return dp[target];
}
}

4. 线性DP

4.1 最长公共子序列(LCS)

1143. 最长公共子序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
char[] c1 = text1.toCharArray(), c2 = text2.toCharArray();
int n = c1.length, m = c2.length;
// dp[i][j]表示c1[:i]和c2[:j]这两个字符串的最长公共子序列
int[][] dp = new int[n + 1][m + 1];
// 边界是dp[i][0]和dp[0][j]表示和空字符串的最长公共子序列,显然为0
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (c1[i] == c2[j]) {
dp[i + 1][j + 1] = dp[i][j] + 1;
} else {
dp[i + 1][j + 1] = Math.max(dp[i + 1][j], dp[i][j + 1]);
}
}
}
return dp[n][m];
}
}

空间优化:需要用到 2 个额外变量来存储即将被覆盖的 dp[j+1];对比完全背包,其实完全背包只有 i 这一个递推的维度,只需要 i+1 的上一个 dp[i][j],而这个题还需要 j+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
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
char[] c1, c2; // c2尽可能短
if (text1.length() > text2.length()) {
c1 = text1.toCharArray(); c2 = text2.toCharArray();
} else {
c2 = text1.toCharArray(); c1 = text2.toCharArray();
}
int n = c1.length, m = c2.length;
// dp[i][j]表示c1[:i]和c2[:j]这两个字符串的最长公共子序列
int[] dp = new int[m + 1];
// 边界是dp[i][0]和dp[0][j]表示和空字符串的最长公共子序列,显然为0
int upLeft;
for (int i = 0; i < n; i++) {
upLeft = dp[0]; // 这里要先存上一轮的再初始化为0,虽然不初始化也没啥事,因为默认值就是0,但下一题编辑距离就需要考虑清楚了
dp[0] = 0;
for (int j = 0; j < m; j++) {
// 现在是dp[j+1],下一轮是dp[j],马上要被覆盖了
int tmp = dp[j + 1];
if (c1[i] == c2[j]) {
dp[j + 1] = upLeft + 1;
} else {
dp[j + 1] = Math.max(dp[j], dp[j + 1]);
}
upLeft = tmp; // 这一轮被覆盖的dp[j + 1]下一轮还要用
}
}
return dp[m];
}
}

72. 编辑距离

非常典的一个题,这里的边界初始化只能用循环,注意n和m都要取等号

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 minDistance(String word1, String word2) {
// 插入和删除的操作是重复的,如果不考虑转化的方向的话
char[] w1 = word1.toCharArray(), w2 = word2.toCharArray();
int n = w1.length, m = w2.length;
// dp[i+1][j+1]表示w1[:i]和w2[:j]的编辑距离
int[][] dp = new int[n + 1][m + 1];
// 边界:dp[i][0]和dp[0][j],注意此处i和j是要到n和m的
for (int i = 0; i <= n; i++) { dp[i][0] = i; }
for (int j = 0; j <= m; j++) { dp[0][j] = j; }

for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (w1[i] == w2[j]) {
dp[i + 1][j + 1] = dp[i][j];
} else {
// 要么由dp[i][j]通过修改操作得到,要么通过dp[i + 1][j]或dp[i][j + 1]的删除操作
dp[i + 1][j + 1] = 1 + Math.min(dp[i][j], Math.min(dp[i + 1][j], dp[i][j + 1]));
}
}
}
return dp[n][m];
}
}

空间优化:也是要用到额外的两个变量

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 minDistance(String word1, String word2) {
// 插入和删除的操作是重复的,如果不考虑转化的方向的话
char[] w1 = word1.toCharArray(), w2 = word2.toCharArray();
int n = w1.length, m = w2.length;
// dp[i+1][j+1]表示w1[:i]和w2[:j]的编辑距离
int[] dp = new int[m + 1];
// 边界:dp[i][0]和dp[0][j],注意此处i和j是要到n和m的
for (int j = 0; j <= m; j++) { dp[j] = j; }

for (int i = 0; i < n; i++) {
int upLeft = dp[0]; // 先存储上一轮的,再初始化这一轮的
dp[0] = i + 1; // 换成1维后,不要忘记初始化边界值
for (int j = 0; j < m; j++) {
int tmp = dp[j + 1];
if (w1[i] == w2[j]) {
dp[j + 1] = upLeft;
} else {
dp[j + 1] = 1 + Math.min(upLeft, Math.min(dp[j], dp[j + 1]));
}
upLeft = tmp;
}
}
return dp[m];
}
}

583. 两个字符串的删除操作

和编辑距离没区别,状态转移方程更简单一些。

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
class Solution {
public int minDistance(String word1, String word2) {
char[] cw1 = word1.toCharArray();
char[] cw2 = word2.toCharArray();

int n1 = cw1.length, n2 = cw2.length;

// dp[i + 1][j + 1] 表示cw1[0...i]到cw2[0...j]的最小步数
int[][] dp = new int[n1 + 1][n2 + 1];

for (int i = 0; i <= n1; i++) {
dp[i][0] = i;
}
for (int j = 0; j <= n2; j++) {
dp[0][j] = j;
}

for (int i = 0; i < n1; i++) {
for (int j = 0; j < n2; j++) {
if (cw1[i] == cw2[j]) {
dp[i + 1][j + 1] = dp[i][j]; // 相等的话直接不用删
} else {
// 往某个方向偏一下,删1次
dp[i + 1][j + 1] = Math.min(dp[i][j + 1], dp[i + 1][j]) + 1;
}
}
}

return dp[n1][n2];
}
}

10. 正则表达式匹配

注意 . 和其他字符的匹配情况都被封装在 match 函数之中了,以及带 * 的可以匹配空字符,分 p[j]* 和不为 * 两种情况考虑。比较难的是等于 * 的情况,分匹配零次和匹配多次考虑,针对匹配多次的情况,递归到 s 的前一个位置。
这里用的是 0-based 算法,或许对于本题而言,1-based 更适合,下次重写的话就用 1-based

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
39
40
41
42
class Solution {
public boolean isMatch(String s, String p) {
char[] sc = s.toCharArray(), pc = p.toCharArray();
int n = sc.length, m = pc.length;
boolean[][] dp = new boolean[n + 1][m + 1];
dp[0][0] = true; // 空字符串可以匹配
// 如果是形如“a*、a*b*、a*b*c*”的模式串,可以匹配空串
for (int j = 1; j < m; j++) {
if (pc[j] == '*') {
dp[0][j + 1] = dp[0][j - 1];
}
}

for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (pc[j] == '*') {
// 如果是*,则可能匹配零次或多次
// 1. 匹配零次
dp[i + 1][j + 1] |= dp[i + 1][j - 1]; // i和j都是从小的值推过来的
// 2. 匹配多次
if (!dp[i + 1][j + 1] && match(sc[i], pc[j - 1])) {
dp[i + 1][j + 1] |= dp[i][j + 1]; // i在上一轮算过了
}
} else {
if (match(sc[i], pc[j])) {
dp[i + 1][j + 1] = dp[i][j];
} else {
dp[i + 1][j + 1] = false;
}
}
}
}
return dp[n][m];
}

boolean match(char a, char b) {
if (b == '.') {
return true;
}
return a == b;
}
}

4.2 最长递增子序列(LIS)

300. 最长递增子序列

这个题有两种做法,前者直接使用动态规划,复杂度为 O(n^2),后者贪心+二分,可以将时间复杂度优化到 O(nlogn)

方法一:直接 DP,还是非常容易理解的,用 dp[i] 表示以 nums[i] 结尾的最长递增子序列,然后取 dp[] 中最大的元素,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp, 1); // 最小为1
int ans = 1;
for (int i = 1; i < n; i++) {
int max = 0;
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
max = Math.max(dp[j], max);
}
}
dp[i] = max + 1;
ans = Math.max(ans, dp[i]);
}
return ans;
}

方法二:贪心+二分,用 dp[i] 记录长度为 i 的 IS 的最小末尾值
注意Arrays.binarySearch的特性,它并不是找第一个大于等于num的元素,而仅仅是寻找等于;如果找不到等于的,就会返回负数。但是这个返回的负数取补码后的值,标记了第一个大于num的元素的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length, len = 0;
int[] dp = new int[n]; // dp[i]表示长度为i+1的IS的最小末尾值
for (int i = 0; i < n; i++) {
// 判断nums[i]是否处在某个dp[j-1]和dp[j]之间
// 如果是的话,则可以优化掉dp[j]为nums[i];相等的情况相当于啥也不做
int j = Arrays.binarySearch(dp, 0, len, nums[i]); // 在[0,len)范围搜索
if (j < 0) { j = ~j; } // 注意Arrays.binarySearch的特性
dp[j] = nums[i]; // dp[j]是第一个大于等于nums[i]的值;或者是数组的长度,即dp[0:len)全部小于nums[i]
if (j == len) { len++; }
}
return len;
}
}

3825. 按位与结果非零的最长上升子序列

这是 2026-01-31 的双周赛,思想就是 LIS,但强制要用 O(nlogn) 算法,因为数据量比较大,注意 Integer 自带的一些处于二进制的函数,例如 .toBinaryString() 是将一个数转化为二进制字符串。具体见我的周赛博客。

5. 划分DP

5.1 判定能否划分

一般定义 f[i] 表示长为 i 的前缀 a[:i] 能否划分,在进行状态转移时,枚举 i 左边的端点,例如左端点为 L,判断 a[L:i] 的划分是否满足要求。

139. 单词拆分

注意内层循环在进行状态转移时的下标。

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 boolean wordBreak(String s, List<String> wordDict) {
int n = s.length();
int maxLength = 0; // 每一轮算dp[i]的
for (String word : wordDict) {
maxLength = Math.max(maxLength, word.length());
}
Set<String> wordSet = new HashSet<>(wordDict);
boolean[] dp = new boolean[n + 1];
Arrays.fill(dp, false);
dp[0] = true;
for (int i = 0; i < n; i++) {
// j=i也是一种情况,后面只需要减maxLength-1次,因此下标是到i-maxLength+1
for (int j = i; j >= Math.max(0, i - maxLength + 1); j--) {
String subWord = s.substring(j, i + 1);
// dp[j + 1]表示包含下标j的s[j],此处s[j]被前面处理了,因此是s[j-1],即dp[j-1+1]
if (dp[j - 1 + 1] && wordSet.contains(subWord)) {
dp[i + 1] = true;
break; // 为true了就不需要再变了
}
}
}
return dp[n];
}
}

6. 状态机DP

122. 买卖股票的最佳时机 II

7. 其他线性DP

7.1 一维DP

发生在前缀/后缀之间的转移,例如从 f[i−1] 转移到 f[i],或者从 f[j] 转移到 f[i]

32. 最长有效括号

入门比较难的题,此处采用 0-based 方法,即 for 循环的下标是 [0,n),每轮循环算的是 dp[i + 1],而 sc[i] 则直接用下标 i 表示。

dp[i+1] 表示以 sc[i] 结尾的括号字符串的最长有效括号长度,在 sc[i] 为右括号时比较麻烦,需要考虑 ())) 的情况,后者更为麻烦。需要考虑诸如 ()(())(()) 的情况,具体可看注释。

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
class Solution {
public int longestValidParentheses(String s) {
char[] sc = s.toCharArray();
int n = sc.length;
if (n <= 1) {
return 0;
}
int ans = 0;
// 使用0-based方案
int[] dp = new int[n + 1];
dp[0] = 0; dp[1] = 0;

for (int i = 1; i < n; i++) {
if (sc[i] == '(') {
dp[i + 1] = 0;
} else {
if (sc[i - 1] == '(') {
dp[i + 1] = dp[i - 1] + 2;
} else {
// 注意下标 `i-dp[i]-1` 是 `(())` 的下标 0 位置
// 如果是 `()(())` 的话,得加上“下标 1 位置”,即最前面的 `()`,下标为 i-dp[i]-2,在 dp[] 中就是 i-dp[i]-1
if (i - dp[i] - 1 >= 0 && sc[i - dp[i] - 1] == '(') {
dp[i + 1] = dp[i] + 2 + dp[i - dp[i] - 1];
}
}
}
ans = Math.max(ans, dp[i + 1]);
}
return ans;
}
}

专题训练

跳跃游戏

毒蘑菇

第一问:
第二问:
第三问: