动态规划:区间DP问题零神基础精讲
Posted Miraclo_acc
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划:区间DP问题零神基础精讲相关的知识,希望对你有一定的参考价值。
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/)(有点懵🎃)
- 其他
区间DP
区间DP定义、三部曲、模板
区间dp
,顾名思义,在区间上dp
,大多数题目的状态都是由区间(类似于dp[l][r]
这种形式)构成的,就是我们可以把大区间转化成小区间来处理,然后对小区间处理后再回溯的求出大区间的值,因为大区间的最优必须要保证小区间也是最优。
区间DP是线性DP的扩展,分阶段地划分问题,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来有很大的关系。
区间DP的特点:
(1)合并:即将两个或多个部分进行整合,当然也可以反过来。
(2)特征:能将问题分解为能两两合并的形式。
(3)求解:将整个问题取最优值,枚举合并点,将问题分解为左右两个部分,最后合并的两个部分的最优值得到原问题的最优值。
- 简而言之:通过合并小区间的最优解进而得出整个大区间上的最优解。
区间DP三部曲:
-
定义状态:
dp[i, j]
为区间[i, j]
的最优解 -
**定义状态转移方程:**常见的写法如下
dp(i,j) = max/mindp(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]
的最优解
- 初始化:
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
为分界点。区间长度从2
到n
,跟上一种写法相同。这种写法的正确性可能不如上一种那么直观,它从小到大枚举出所有区间,在求解大区间时,状态转移方程中利用的状态都是小区间的状态,必定在它之前被求解,所以也是正确的。
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];
// 倒序枚举i
for(int i = n-1; i >= 0; i--)
f[i][i] = 1; // 初始化条件:只有一个字母
// 正序枚举j
for(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:
输入:values = [1,2,3]
输出:6
解释:多边形已经三角化,唯一三角形的分数为 6。
示例 2:
输入:values = [3,7,4,5]
输出:144
解释:有两种三角剖分,可能得分分别为:3*7*5 + 4*5*7 = 245,或 3*4*5 + 3*4*7 = 144。最低分数为 144。
示例 3:
输入: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++) //枚举顶点k
res = 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
到n
之间选择一个数字。 - 你来猜我选了哪个数字。
- 如果你猜到正确的数字,就会 赢得游戏 。
- 如果你猜错了,那么我会告诉你,我选的数字比你的 更大或者更小 ,并且你需要继续猜数。
- 每当你猜了数字
x
并且猜错了的时候,你需要支付金额为x
的现金。如果你花光了钱,就会 输掉游戏 。
给你一个特定的数字 n
,返回能够 确保你获胜 的最小现金数,不管我选择那个数字 。
示例 1:
问题求解:
n = 100,典型的O(n ^ 3)的动规问题。一般来说这种O(n ^ 3)的问题可以考虑使用区间dp来解决。
区间dp是典型的三层结构,最外围枚举区间长度,中间层枚举起点,最里层枚举截断点,因此区间dp的时间复杂度往往为O(n ^ 3)。
public int minimumMoves(int[] arr) { int n = arr.length; int[][] dp = new int[n + 1][n + 1]; for (int i = 0; i < n; i++) dp[i][i] = 1; for (int len = 2; len <= n; len++) { for (int i = 0; i <= n - len; i++) { int j = i + len - 1; dp[i][j] = 1 + dp[i + 1][j]; if (arr[i] == arr[i + 1]) dp[i][j] = Math.min(dp[i][j], 1 + dp[i + 2][j]); for (int k = i + 2; k <= j; k++) { if (arr[k] == arr[i]) { dp[i][j] = Math.min(dp[i][j], dp[i + 1][k - 1] + dp[k + 1][j]); } } } } return dp[0][n - 1]; }
以上是关于动态规划:区间DP问题零神基础精讲的主要内容,如果未能解决你的问题,请参考以下文章
《算法零基础100例》(第100例) 动态规划 - 区间DP