动态规划:区间DP问题【零神基础精讲】

news/2025/1/18 3:15:43/

0x3f:https://www.bilibili.com/video/BV1Gs4y1E7EU/

chenf99:由易到难,一步步说明思路和细节:https://leetcode.cn/problems/minimum-cost-to-merge-stones/solution/yi-dong-you-yi-dao-nan-yi-bu-bu-shuo-ming-si-lu-he/

文章目录

  • 区间DP
    • 区间DP定义、三部曲、模板
    • [516. 最长回文子序列](https://leetcode.cn/problems/longest-palindromic-subsequence/)【题型1】
      • 记忆化搜索=>动态规划
    • [1039. 多边形三角剖分的最低得分](https://leetcode.cn/problems/minimum-score-triangulation-of-polygon/)【题型2】
      • 记忆化搜索=>动态规划
      • 模板二写法
    • [375. 猜数字大小 II](https://leetcode.cn/problems/guess-number-higher-or-lower-ii/)
      • 记忆化搜索=>动态规划
    • [1312. 让字符串成为回文串的最少插入次数](https://leetcode.cn/problems/minimum-insertion-steps-to-make-a-string-palindrome/)(旧瓶装新酒)
      • 记忆化搜索=>动态规划
    • [1547. 切棍子的最小成本](https://leetcode.cn/problems/minimum-cost-to-cut-a-stick/)
    • [1000. 合并石头的最低成本](https://leetcode.cn/problems/minimum-cost-to-merge-stones/)(有点懵🎃)
  • 其他
    • [877. 石子游戏](https://leetcode.cn/problems/stone-game/)(区间DP,博弈)

区间DP

区间DP定义、三部曲、模板

区间dp,顾名思义,在区间上dp,大多数题目的状态都是由区间(类似于dp[l][r]这种形式)构成的,就是我们可以把大区间转化成小区间来处理,然后对小区间处理后再回溯的求出大区间的值,因为大区间的最优必须要保证小区间也是最优

区间DP是线性DP的扩展,分阶段地划分问题,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来有很大的关系。

区间DP的特点:

(1)合并:即将两个或多个部分进行整合,当然也可以反过来。

(2)特征:能将问题分解为能两两合并的形式。

(3)求解:将整个问题取最优值,枚举合并点,将问题分解为左右两个部分,最后合并的两个部分的最优值得到原问题的最优值。

  • 简而言之:通过合并小区间的最优解进而得出整个大区间上的最优解

区间DP三部曲:

  1. 定义状态dp[i, j]为区间[i, j]的最优解

  2. **定义状态转移方程:**常见的写法如下

dp(i,j) = max/min{dp(i,k) + dp(k+1,j)} + w(i,j)   (i <= k < j)

其中dp(i,j)表示在区间[i,j]上的最优值,w(i,j)表示在转移时需要额外付出的代价,选取[i, j]之间的一个分界点k,分别计算[i, k][k+1, j]的最优解

  1. 初始化:dp[i][i] = 常数。区间长度为1时的最优解应当是已知的。

区间DP模板部分:

假设要求的区间最优解为dp[1, n],区间dp问题有两种编码方法:

第一种:常规DP写法

for (int i = n; i >= 1; --i) {for (int j = i + 1; j <= n; ++j) {for (int k = i; k < j; ++k) {dp[i,j] = max/min(dp[i,j], dp[i,k] + dp[k+1, j] + cost)}}
}

这种写法就是常规的dp写法,枚举i为子区间左边界,枚举j为子区间有边界,枚举k为分界点。要注意由于要求的是dp[1,n],所以i必须从大往小遍历,j必须从小往大遍历。这样在状态转移方程中利用的就是已求解的dp状态。

第二种:将区间分割成一个个小区间,求解每个小区间上的最优解。

dp = new int[n+1][n+1]for (int len = 2; len <= n; len++) {         // 区间长度for (int i = 1; i + len - 1 <= n; i++) { // 枚举起点int j = i + len - 1;                 // 区间终点// 判断i j关系进行初始化for (int k = i; k < j; k++) {        // 枚举分割点,构造状态转移方程dp[i][j] = max/min(dp[i][j], dp[i][k] + dp[k + 1][j] + w[i][j]);}}
}
return dp[1][n] // 返回[1,n]整个区间的最优解

这种写法最常见,枚举len为区间长度,枚举i为区间左端点,由此可以计算出区间右端点j,枚举k为分界点。区间长度从2n,跟上一种写法相同。这种写法的正确性可能不如上一种那么直观,它从小到大枚举出所有区间,在求解大区间时,状态转移方程中利用的状态都是小区间的状态,必定在它之前被求解,所以也是正确的。

  • dp数组的维度和边界条件以及转移方程都是可变的,但是很多简单题都是这样可以做出来的,难题也都是情况更复杂了些,其最基本的思想还是不变的。

516. 最长回文子序列【题型1】

难度中等982

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:

输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。

示例 2:

输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。

提示:

  • 1 <= s.length <= 1000
  • s 仅由小写英文字母组成

【思路1:转换】求s和反转后s的LCS

class Solution {public int longestPalindromeSubseq(String s) {String t = new StringBuilder(s).reverse().toString();int n = s.length();int[][] dp = new int[n+1][n+1];for(int i = 0; i < n; i++){for(int j = 0; j < n; j++){if(s.charAt(i) == t.charAt(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][n];}
}

记忆化搜索=>动态规划

【思路2:选或者不选】从两侧向内缩小问题规模

  • 要么不选第一个字母,要么不选最后一个字母
dp[i][j]的含义是s[i..=j]的最长回文子串的长度, 最终答案就是dp[0][s.len() - 1], 0 <= i <= j < s.len()
基本状态: 当i==j时, 即只有一个字符, 设置回文长度为1
下面是普通状态转移方法(i < j):
情况1: s[i] == s[j]: 最左边和最右边的字符相同, 我们可以直接将中间部分的最长回文子串长度(dp[i+1][j-1])2作为当前部分的最长回文子串长度dp[i][j]=> dp[i][j] = dp[i+1][j-1] + 2;(s[i] == s[j])
情况2: s[i] != s[j]: 最左边和最右边的字符不同, 没别的好办法, 只能取dp[i][j-1]与dp[i+1][j]的较大值=> dp[i][j] = dp[i][j-1].max(dp[i+1][j]);(s[i] != s[j])

记忆化搜索

class Solution {char[] s;int[][] memo;public int longestPalindromeSubseq(String S) {s = S.toCharArray();int n = s.length;memo = new int[n][n];for(int i = 0; i < n; i++){Arrays.fill(memo[i], -1);}return dfs(0, n-1);}public int dfs(int i, int j){if(i > j) return 0; //空串if(i == j) return 1; // 只有一个字母if(memo[i][j] != -1) return memo[i][j];if(s[i] == s[j]) return memo[i][j] = dfs(i+1, j-1) + 2; // 都选return memo[i][j] = Math.max(dfs(i+1, j), dfs(i, j-1)); // 枚举哪个不选}
}

转成递推

因为计算f(i)需要f[i+1],因此i需要倒序枚举,f(j)需要计算f(j-1),所以j需要正序枚举

class Solution {public int longestPalindromeSubseq(String s) {int n = s.length();char[] c = s.toCharArray();int[][] f = new int[n+1][n+1];// 倒序枚举ifor(int i = n-1; i >= 0; i--){f[i][i] = 1; // 初始化条件:只有一个字母// 正序枚举jfor(int j = i+1; j < n; j++){if(c[i] == c[j]){f[i][j] = f[i+1][j-1] + 2;}else f[i][j] = Math.max(f[i+1][j], f[i][j-1]);}}return f[0][n-1];}
}

1039. 多边形三角剖分的最低得分【题型2】

难度中等131

你有一个凸的 n 边形,其每个顶点都有一个整数值。给定一个整数数组 values ,其中 values[i] 是第 i 个顶点的值(即 顺时针顺序 )。

假设将多边形 剖分n - 2 个三角形。对于每个三角形,该三角形的值是顶点标记的乘积,三角剖分的分数是进行三角剖分后所有 n - 2 个三角形的值之和。

返回 多边形进行三角剖分后可以得到的最低分

示例 1:

img

输入:values = [1,2,3]
输出:6
解释:多边形已经三角化,唯一三角形的分数为 6。

示例 2:

img

输入:values = [3,7,4,5]
输出:144
解释:有两种三角剖分,可能得分分别为:3*7*5 + 4*5*7 = 245,或 3*4*5 + 3*4*7 = 144。最低分数为 144。

示例 3:

img

输入:values = [1,3,1,4,1,5]
输出:13
解释:最低分数三角剖分的得分情况为 1*1*3 + 1*1*4 + 1*1*5 + 1*1*1 = 13。

提示:

  • n == values.length
  • 3 <= n <= 50
  • 1 <= values[i] <= 100

记忆化搜索=>动态规划

题解:

定义:dp[i][j]:表示从第i个到第j个角所形成的多边形的最小面积

状态转移方程:

dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] + A[i] * A[k] * A[j])

记忆化搜索

class Solution {// 定义:dp[i][j]:表示从第i个到第j个角所形成的多边形的最小面积// dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] + A[i] * A[k] * A[j])// 递归边界 dp[i][i+1] = 0   ; 递归入口:dfs(0, n-1)private int[] v;private int[][] memo;public int minScoreTriangulation(int[] values) {v = values;int n = v.length;memo = new int[n][n];for(int i = 0; i < n; i++){Arrays.fill(memo[i], -1); }return dfs(0, n-1);}public int dfs(int i, int j) {if(i + 1 == j) return 0; // 只有两个点,无法形成三角形if(memo[i][j] != -1) return memo[i][j];int res = Integer.MAX_VALUE;for(int k = i+1; k < j; k++) {//枚举顶点kres = Math.min(res, dfs(i,k) + dfs(k,j) + v[i] * v[j] * v[k]);}return memo[i][j] = res;}
}

转成递推

  • i<k, 因为f[i]f[k]转移过来,所以i要倒序枚举
  • j>k, 因为f[i][j]f[i][k]转移过来,所以j要正序枚举
  • 答案f[0][n-1]
class Solution {// 定义:dp[i][j]:表示从第i个到第j个角所形成的多边形的最小面积// dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] + A[i] * A[k] * A[j])// 递归边界 dp[i][i+1] = 0   ; 递归入口:dfs(0, n-1)public int minScoreTriangulation(int[] values) {int n = values.length;int[][] f = new int[n][n];for(int i = n-3; i >= 0; i--){//三角形至少三个顶点for(int j = i+2; j < n; j++){int res = Integer.MAX_VALUE;for(int k = i+1; k < j; k++){res = Math.min(res, f[i][k] + f[k][j] + values[i] * values[j] * values[k]);}f[i][j] = res;}}return f[0][n-1];}}

模板二写法

class Solution {// 定义:dp[i][j]:表示从第i个到第j个角所形成的多边形的最小面积// dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] + A[i] * A[k] * A[j])// 递归边界 dp[i][i+1] = 0   ; 递归入口:dfs(0, n-1)public int minScoreTriangulation(int[] values) {int n = values.length;int[][] f = new int[n][n];for(int i = 0; i < n; i++) Arrays.fill(f[i], Integer.MAX_VALUE);for(int len = 3; len <= n; len++){ // 枚举区间长度for(int i = 0; i + len - 1 < n; ++i){ // 枚举区间起点int j = i + len - 1; // 根据长度和区间起点获得区间终点for(int k = i+1; k < j; k++){if(k == i+1) f[i][k] = 0; // 三角形至少三个角if(k == j-1) f[k][j] = 0;f[i][j] = Math.min(f[i][j], f[i][k] + f[k][j] + values[i] * values[j] * values[k]);}}}return f[0][n-1];}}

375. 猜数字大小 II

难度中等533

我们正在玩一个猜数游戏,游戏规则如下:

  1. 我从 1n 之间选择一个数字。
  2. 你来猜我选了哪个数字。
  3. 如果你猜到正确的数字,就会 赢得游戏
  4. 如果你猜错了,那么我会告诉你,我选的数字比你的 更大或者更小 ,并且你需要继续猜数。
  5. 每当你猜了数字 x 并且猜错了的时候,你需要支付金额为 x 的现金。如果你花光了钱,就会 输掉游戏

给你一个特定的数字 n ,返回能够 确保你获胜 的最小现金数,不管我选择那个数字

示例 1:

img

输入:n = 10
输出:16
解释:制胜策略如下:
- 数字范围是 [1,10] 。你先猜测数字为 7 。- 如果这是我选中的数字,你的总费用为 $0 。否则,你需要支付 $7 。- 如果我的数字更大,则下一步需要猜测的数字范围是 [8,10] 。你可以猜测数字为 9 。- 如果这是我选中的数字,你的总费用为 $7 。否则,你需要支付 $9 。- 如果我的数字更大,那么这个数字一定是 10 。你猜测数字为 10 并赢得游戏,总费用为 $7 + $9 = $16 。- 如果我的数字更小,那么这个数字一定是 8 。你猜测数字为 8 并赢得游戏,总费用为 $7 + $9 = $16 。- 如果我的数字更小,则下一步需要猜测的数字范围是 [1,6] 。你可以猜测数字为 3 。- 如果这是我选中的数字,你的总费用为 $7 。否则,你需要支付 $3 。- 如果我的数字更大,则下一步需要猜测的数字范围是 [4,6] 。你可以猜测数字为 5 。- 如果这是我选中的数字,你的总费用为 $7 + $3 = $10 。否则,你需要支付 $5 。- 如果我的数字更大,那么这个数字一定是 6 。你猜测数字为 6 并赢得游戏,总费用为 $7 + $3 + $5 = $15 。- 如果我的数字更小,那么这个数字一定是 4 。你猜测数字为 4 并赢得游戏,总费用为 $7 + $3 + $5 = $15 。- 如果我的数字更小,则下一步需要猜测的数字范围是 [1,2] 。你可以猜测数字为 1 。- 如果这是我选中的数字,你的总费用为 $7 + $3 = $10 。否则,你需要支付 $1 。- 如果我的数字更大,那么这个数字一定是 2 。你猜测数字为 2 并赢得游戏,总费用为 $7 + $3 + $1 = $11 。
在最糟糕的情况下,你需要支付 $16 。因此,你只需要 $16 就可以确保自己赢得游戏。

示例 2:

输入:n = 1
输出:0
解释:只有一个可能的数字,所以你可以直接猜 1 并赢得游戏,无需支付任何费用。

示例 3:

输入:n = 2
输出:1
解释:有两个可能的数字 1 和 2 。
- 你可以先猜 1 。- 如果这是我选中的数字,你的总费用为 $0 。否则,你需要支付 $1 。- 如果我的数字更大,那么这个数字一定是 2 。你猜测数字为 2 并赢得游戏,总费用为 $1 。
最糟糕的情况下,你需要支付 $1 。

提示:

  • 1 <= n <= 200

记忆化搜索=>动态规划

class Solution {// 递归:设计递归函数为 int dfs(int l, int r) 传入参数 l 和 r 代表在范围 [l,r] 内进行猜数,// 返回值为在 [l,r] 内猜中数字至少需要多少钱。// 可以决策选哪个数但不能决策值落在哪边 // 就是要找你猜的数中的付出现金的最小值,但是你往左方向还是右方向是不能确定的,所以要选最大值int n;int[][] cache;public int getMoneyAmount(int n) {this.n = n;cache = new int[n+1][n+1];return dfs(1, n);}public int dfs(int l, int r){if(l >= r) return 0;if(cache[l][r] != 0) return cache[l][r];int res = Integer.MAX_VALUE;for(int x = l; x <= r; x++){// 当选择的数位 x 时,至少需要 cur 才能猜中数字int cur = Math.max(dfs(l,x-1), dfs(x+1, r)) + x;// 在所有我们可以决策的数值之间取最优res = Math.min(res, cur);}return cache[l][r] = res;}
}

转递推:

class Solution {// 在求解[l,r]的最小成本时,需要依赖[l,i-1]和[i+1,r]这样更小的区间public int getMoneyAmount(int n) {// 在[l,r]范围内进行猜数的最小成本int[][] f = new int[n+10][n+10];for(int len = 2; len <= n; len++){for(int l = 1; l+len-1 <= n; l++){int r = l+len-1;f[l][r] = Integer.MAX_VALUE;for(int x = l; x <= r; x++){int cur = Math.max(f[l][x-1], f[x+1][r]) + x;f[l][r] = Math.min(f[l][r], cur);}}}return f[1][n];}
}

1312. 让字符串成为回文串的最少插入次数(旧瓶装新酒)

难度困难178

给你一个字符串 s ,每一次操作你都可以在字符串的任意位置插入任意字符。

请你返回让 s 成为回文串的 最少操作次数

「回文串」是正读和反读都相同的字符串。

示例 1:

输入:s = "zzazz"
输出:0
解释:字符串 "zzazz" 已经是回文串了,所以不需要做任何插入操作。

示例 2:

输入:s = "mbadm"
输出:2
解释:字符串可变为 "mbdadbm" 或者 "mdbabdm" 。

示例 3:

输入:s = "leetcode"
输出:5
解释:插入 5 个字符后字符串变为 "leetcodocteel" 。

提示:

  • 1 <= s.length <= 500
  • s 中所有字符都是小写字母。

记忆化搜索=>动态规划

记忆化搜索

class Solution {char[] c;int[][] cache;public int minInsertions(String s) {c = s.toCharArray();int n = c.length;cache = new int[n][n];for(int i = 0; i < n; i++) Arrays.fill(cache[i], -1);return dfs(0, n-1);}public int dfs(int i, int j){if(i >= j) return 0; // 长度为1的串就是回文串if(cache[i][j] != -1) return cache[i][j];if(c[i] == c[j]) // 相同,不需要插入操作return cache[i][j] = dfs(i+1, j-1); // 插入 // dfs(i+1, j) : 在 j 处插入一个c[i],则i+1, j不变// dfs(i, j-1) :在 i 处插入一个c[j],则i不变,j+1return cache[i][j] = Math.min(dfs(i+1, j), dfs(i, j-1)) + 1;}
}

转递推

class Solution {public int minInsertions(String s) {char[] c = s.toCharArray();int n = c.length;int[][] dp = new int[n][n];for(int i = n-1; i >= 0; i--){dp[i][i] = 0;for(int j = i+1; j < n; j++){if(c[i] == c[j]) dp[i][j] = dp[i+1][j-1];elsedp[i][j] = Math.min(dp[i+1][j], dp[i][j-1]) + 1;}}return dp[0][n-1];}
}

1547. 切棍子的最小成本

难度困难77

有一根长度为 n 个单位的木棍,棍上从 0n 标记了若干位置。例如,长度为 6 的棍子可以标记如下:

img

给你一个整数数组 cuts ,其中 cuts[i] 表示你需要将棍子切开的位置。

你可以按顺序完成切割,也可以根据需要更改切割的顺序。

每次切割的成本都是当前要切割的棍子的长度,切棍子的总成本是历次切割成本的总和。对棍子进行切割将会把一根木棍分成两根较小的木棍(这两根木棍的长度和就是切割前木棍的长度)。请参阅第一个示例以获得更直观的解释。

返回切棍子的 最小总成本

示例 1:

img

输入:n = 7, cuts = [1,3,4,5]
输出:16
解释:按 [1, 3, 4, 5] 的顺序切割的情况如下所示:第一次切割长度为 7 的棍子,成本为 7 。第二次切割长度为 6 的棍子(即第一次切割得到的第二根棍子),第三次切割为长度 4 的棍子,最后切割长度为 3 的棍子。总成本为 7 + 6 + 4 + 3 = 20 。
而将切割顺序重新排列为 [3, 5, 1, 4] 后,总成本 = 16(如示例图中 7 + 4 + 3 + 2 = 16)。

示例 2:

输入:n = 9, cuts = [5,6,1,4,2]
输出:22
解释:如果按给定的顺序切割,则总成本为 25 。总成本 <= 25 的切割顺序很多,例如,[4, 6, 5, 2, 1] 的总成本 = 22,是所有可能方案中成本最小的。

提示:

  • 2 <= n <= 10^6
  • 1 <= cuts.length <= min(n - 1, 100)
  • 1 <= cuts[i] <= n - 1
  • cuts 数组中的所有整数都 互不相同

题解:https://leetcode.cn/problems/minimum-cost-to-cut-a-stick/solution/c-dong-tai-gui-hua-si-lu-zhuan-hua-by-ming-tian-ge/

这道题是经典的区间dp。类似石子合并问题、戳气球。

切分木棍也可以想象成每次合并相邻的木棍,使得总成本最小。

  • 状态dp[i][j]

    • 集合:所有把[i,j]的木棍合成一根的方案

    • 属性:求这些方案的最小成本。

  • 状态计算:

最后的合并位置是k

dp[i][j] = min(dp[i, k] + dp[k, j] + cost)

class Solution {public int minCost(int n, int[] cuts) {List<Integer> list = new ArrayList<>();list.add(0);list.add(n);for(int num : cuts) list.add(num);Collections.sort(list);int m = list.size();int[][] dp = new int[m][m];for(int len = 2; len < m; len++){ // 枚举区间for(int i = 0; i + len < m; i++){int j = i + len;dp[i][j] = Integer.MAX_VALUE;// 枚举每一个分割点for(int k = i + 1; k < j; k++){dp[i][j] = Math.min(dp[i][j], dp[i][k] + dp[k][j] + list.get(j) - list.get(i));}}}return dp[0][m-1];}
}

1000. 合并石头的最低成本(有点懵🎃)

难度困难214

N 堆石头排成一排,第 i 堆中有 stones[i] 块石头。

每次*移动(move)*需要将连续的 K 堆石头合并为一堆,而这个移动的成本为这 K 堆石头的总数。

找出把所有石头合并成一堆的最低成本。如果不可能,返回 -1

示例 1:

输入:stones = [3,2,4,1], K = 2
输出:20
解释:
从 [3, 2, 4, 1] 开始。
合并 [3, 2],成本为 5,剩下 [5, 4, 1]。
合并 [4, 1],成本为 5,剩下 [5, 5]。
合并 [5, 5],成本为 10,剩下 [10]。
总成本 20,这是可能的最小值。

示例 2:

输入:stones = [3,2,4,1], K = 3
输出:-1
解释:任何合并操作后,都会剩下 2 堆,我们无法再进行合并。所以这项任务是不可能完成的。.

示例 3:

输入:stones = [3,5,1,2,6], K = 3
输出:25
解释:
从 [3, 5, 1, 2, 6] 开始。
合并 [5, 1, 2],成本为 8,剩下 [3, 8, 6]。
合并 [3, 8, 6],成本为 17,剩下 [17]。
总成本 25,这是可能的最小值。

提示:

  • 1 <= stones.length <= 30
  • 2 <= K <= 30
  • 1 <= stones[i] <= 100

题解0x3f:https://leetcode.cn/problems/minimum-cost-to-merge-stones/solution/tu-jie-qu-jian-dpzhuang-tai-she-ji-yu-yo-ppv0/

class Solution {int[][][] memo;int[] s; // 前缀和int k;public int mergeStones(int[] stones, int k) {int n = stones.length;if((n-1) % (k-1) > 0) return -1; // 无法合并成一堆s = new int[n+1];for(int i = 0; i < n; i++){s[i+1] = s[i] + stones[i]; }  this.k = k;memo = new int[n][n][k+1];for(int i = 0; i < n; i++){for(int j = 0; j < n; j++){Arrays.fill(memo[i][j], -1);}}      return dfs(0, n-1, 1);}// 定义dfs(i, j , p) 为把stones[i]到stones[j]合并成p堆的最低成本// dfs(i, j, 1) = dfs(i, j , k) + sum(i, j)public int dfs(int i, int j, int p){if(memo[i][j][p] != -1) return memo[i][j][p];if(p == 1) { // 合并成一堆return memo[i][j][p] = i == j ? 0 : dfs(i, j , k) + s[j+1] - s[i]; }int res = Integer.MAX_VALUE;for (int m = i; m < j; m += k - 1) // 枚举哪些石头堆合并成第一堆res = Math.min(res, dfs(i, m, 1) + dfs(m + 1, j, p - 1));return memo[i][j][k] = res;}
}

记忆化搜索优化:

class Solution {int[][] memo;int[] s; // 前缀和int k;public int mergeStones(int[] stones, int k) {int n = stones.length;if((n-1) % (k-1) > 0) return -1; // 无法合并成一堆s = new int[n+1];for(int i = 0; i < n; i++){s[i+1] = s[i] + stones[i]; }  this.k = k;memo = new int[n][n];for(int i = 0; i < n; i++){Arrays.fill(memo[i], -1);}      return dfs(0, n-1);}// 优化:通过判断j-i是否为k-1的倍数,就能知道p=1还是>=2,p是多余的// dfs(i, j) 表示从i到j的石头堆合并到小于k堆的最小代价public int dfs(int i, int j){if(i == j) return 0; // 只有一堆石头,无需合并if(memo[i][j] != -1) return memo[i][j];int res = Integer.MAX_VALUE;for (int m = i; m < j; m += k - 1) // 枚举哪些石头堆合并成第一堆res = Math.min(res, dfs(i, m) + dfs(m + 1, j));if ((j-i) % (k-1) == 0) { // 可以合并成一堆res += s[j+1] - s[i];}return memo[i][j] = res;}
}

转递推:

class Solution {public int mergeStones(int[] stones, int k) {int n = stones.length;if ((n - 1) % (k - 1) > 0) // 无法合并成一堆return -1;int[] s = new int[n + 1];for (int i = 0; i < n; i++)s[i + 1] = s[i] + stones[i]; // 前缀和int[][] f = new int[n][n];for(int i = n-1; i >= 0; i--) {for(int j = i+1; j < n; j++){f[i][j] = Integer.MAX_VALUE;for(int m = i; m < j; m += k-1){f[i][j] = Math.min(f[i][j], f[i][m] + f[m+1][j]);}if((j-i) % (k-1) == 0) {f[i][j] += s[j+1] - s[i];}}}return f[0][n-1];}
}

其他

877. 石子游戏(区间DP,博弈)

难度中等486

Alice 和 Bob 用几堆石子在做游戏。一共有偶数堆石子,排成一行;每堆都有 整数颗石子,数目为 piles[i]

游戏以谁手中的石子最多来决出胜负。石子的 总数奇数 ,所以没有平局。

Alice 和 Bob 轮流进行,Alice 先开始 。 每回合,玩家从行的 开始结束 处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中 石子最多 的玩家 获胜

假设 Alice 和 Bob 都发挥出最佳水平,当 Alice 赢得比赛时返回 true ,当 Bob 赢得比赛时返回 false

示例 1:

输入:piles = [5,3,4,5]
输出:true
解释:
Alice 先开始,只能拿前 5 颗或后 5 颗石子 。
假设他取了前 5 颗,这一行就变成了 [3,4,5] 。
如果 Bob 拿走前 3 颗,那么剩下的是 [4,5],Alice 拿走后 5 颗赢得 10 分。
如果 Bob 拿走后 5 颗,那么剩下的是 [3,4],Alice 拿走后 4 颗赢得 9 分。
这表明,取前 5 颗石子对 Alice 来说是一个胜利的举动,所以返回 true 。

示例 2:

输入:piles = [3,7,2,3]
输出:true

提示:

  • 2 <= piles.length <= 500
  • piles.length偶数
  • 1 <= piles[i] <= 500
  • sum(piles[i])奇数

题解:https://leetcode.cn/problems/stone-game/solution/shi-zi-you-xi-dong-tai-gui-hua-qu-jian-d-5ra8/

class Solution {// dp[i][j] 【永远】是【先手】玩家赢后手的石头数public boolean stoneGame(int[] piles) {int n = piles.length;if(n == 0) return false;int[][] dp = new int[n][n];for(int i = 0; i < n; i++){dp[i][i] = piles[i];}for(int i = n-1; i >= 0; i--){for(int j = i+1; j < n; j++){// 先手玩家拿了开头的piles[i]// dp[i+1][j]表示的是后手玩家在这个区间内,比先手玩家多的最大石子个数// - dp[i+1][j] : 在这个区间内,先手玩家比后手玩家多的最大石子个数// 先手玩家拿了结尾的piles[j]// dp[i][j-1]表示的是后手玩家在这个区间内,比先手玩家多的最大石子个数// -dp[i][j-1] : 在这个区间内,先手玩家比后手玩家多的最大石子个数;// 在这两种情况中, 选择先手玩家和后手玩家选择石子堆后,石子个数差更大的一种情况dp[i][j] = Math.max(piles[i] - dp[i+1][j], piles[j] - dp[i][j-1]);}}return dp[0][n-1] >= 0;}
}
class Solution {// dp[i][j] 【永远】是【先手】玩家赢后手的石头数public boolean stoneGame(int[] piles) {int n = piles.length;if(n == 0) return false;int[][] dp = new int[n][n];for(int i = 0; i < n; i++){dp[i][i] = piles[i];}for(int i = n-1; i >= 0; i--){for(int j = i+1; j < n; j++){// 先手玩家拿了开头的piles[i]// dp[i+1][j]表示的是后手玩家在这个区间内,比先手玩家多的最大石子个数// - dp[i+1][j] : 在这个区间内,先手玩家比后手玩家多的最大石子个数// 先手玩家拿了结尾的piles[j]// dp[i][j-1]表示的是后手玩家在这个区间内,比先手玩家多的最大石子个数// -dp[i][j-1] : 在这个区间内,先手玩家比后手玩家多的最大石子个数;// 在这两种情况中, 选择先手玩家和后手玩家选择石子堆后,石子个数差更大的一种情况dp[i][j] = Math.max(piles[i] - dp[i+1][j], piles[j] - dp[i][j-1]);}}return dp[0][n-1] >= 0;}
}

http://www.ppmy.cn/news/37795.html

相关文章

让ChatGpt可以看视频,看文档,帮你总结,并提供示例的github项目(附体验地址)

github地址&#xff1a;https://github.com/madawei2699/myGPTReader 演示 Stay updated with the latest news summaries daily with chatGPT. Use chatGPT to read and provide a summary of any webpage include the video(YouTube). 总之这个玩意有很多&#xff0c;可以…

SpringMVC --- 获取请求参数、域对象共享数据、视图

一、SpringMVC获取请求参数 1.1、通过ServletAPI获取 将 HttpServletRequest 作为控制器方法的形参&#xff0c;此时 HttpServletRequest 类型的参数表示封装了当前请求的请求报文的对象 RequestMapping("/param/servletAPI")public String getParamByServletAPI(H…

ChatGPT AI国内免魔法使用

什么是ChatGPT? ChatGPT是一种基于深度学习技术的自然语言处理模型。它由OpenAI开发&#xff0c;采用了transformer架构&#xff0c;可以处理各种自然语言任务&#xff0c;如问答、文本生成、对话等。 ChatGPT的优点 ChatGPT的优点在于它具有强大的文本生成能力&#xff0c…

python入门(一)conda的使用,创建修改删除虚拟环境,以及常用命令,配置镜像

文章目录背景1.conda的下载地址:2.安装3.执行常用命令1&#xff09;查看版本2&#xff09;查看所有虚拟环境3&#xff09;创建虚拟环境4&#xff09;激活虚拟环境5&#xff09;关闭虚拟环境6&#xff09;删除虚拟环境7&#xff09;创建python2.7的虚拟环境8&#xff09;使用pyt…

学习通信原理之——频谱/功率谱/功率谱密度(MATLAB演示)

前言 最近在复习通信原理&#xff0c;每次到了功率谱这一块就感到困惑&#xff0c;每次都要去查&#xff0c;我觉得不能再这样循环下去了&#xff0c;这次一定要对这三个概念理解透彻&#xff0c;于是去网上找了资料去学习。 学习了b站视频&#xff1a;NO.31 十分钟搞定频谱/功…

java校园行为分析预警管理系统

目 录 摘 要 II ABSTRACT III 第一章 绪论 1 1.1研究背景 1 1.2选题目的 1 1.3本文研究内容 2 第二章 开发技术介绍 3 2.1开发工具介绍 3 2.2 JAVA技术介绍 3 2.3 MYSQL数据库介绍 4 第三章 系统需求分析 6 3.1可行性分析 6 3.1.1技术…

Python 自动化指南(繁琐工作自动化)第二版:二、流程控制

原文&#xff1a;https://automatetheboringstuff.com/2e/chapter2/ 所以&#xff0c;你知道单个指令的基本原理&#xff0c;程序就是一系列指令。但是编程的真正优势不仅仅是像周末跑腿一样一个接一个地运行指令。根据表达式的求值方式&#xff0c;程序可以决定跳过指令&#…

第18章_主从复制

第18章_主从复制 &#x1f3e0;个人主页&#xff1a;shark-Gao &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是shark-Gao&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f389;目前状况&#xff1a;23届毕业生&#xff0c;目前在某…