区间DP求解策略详解
区间DP求解策略详解
- 一、区间DP基础认知
- 1.1 问题特征与核心思想
- 1.2 典型应用场景
- 二、区间DP的基本框架
- 2.1 状态定义
- 2.2 递推关系
- 2.3 遍历顺序
- 三、经典案例详解
- 3.1 案例1:最长回文子序列
- 问题描述
- 区间DP设计
- 代码实现
- 3.2 案例2:矩阵链最优计算顺序
- 问题描述
- 区间DP设计
- 代码实现
- 3.3 案例3:戳气球
- 问题描述
- 区间DP设计
- 代码实现
- 四、区间DP的关键技巧与优化
- 4.1 状态定义的灵活性
- 4.2 遍历顺序的严格性
- 4.3 复杂度分析与优化
- 区间DP与其他DP的对比
- 总结
区间动态规划(Interval Dynamic Programming)是动态规划中一类重要的分支,专门用于解决区间上的最优问题。与线性DP(如0/1背包)按元素顺序推进不同,区间DP通过“划分区间、合并子问题”的思路,高效求解区间内的最优解,广泛应用于字符串处理、矩阵运算、区间调度等场景。
一、区间DP基础认知
1.1 问题特征与核心思想
区间DP适用于以下特征的问题:
- 问题与区间相关:最优解依赖于区间
[i,j]
的子区间[i,k]
和[k+1,j]
的解(如“区间内的最大价值”“最少操作次数”)。 - 区间可拆分:大区间的解可通过拆分后的小区间的解合并得到(分治思想+DP)。
- 无后效性:子区间的解一旦确定,不会受后续区间划分的影响。
核心思想:“先解决子区间的最优解,再合并得到大区间的最优解”。例如,求区间[i,j]
的最优解时,可枚举中间点k
(i≤k<j
),将[i,j]
拆分为[i,k]
和[k+1,j]
,用两个子区间的最优解推导[i,j]
的解。
1.2 典型应用场景
- 字符串问题:最长回文子序列、最长回文子串、字符串编辑距离(区间变种)。
- 矩阵运算:矩阵链乘法(最小计算代价)。
- 区间合并:戳气球(最大化得分)、合并石头(最小代价)。
- 区间调度:最优区间划分、区间染色问题。
二、区间DP的基本框架
2.1 状态定义
区间DP的状态通常以区间的左右端点为维度:
- 设
dp[i][j]
表示“区间[i,j]
(从索引i
到j
)的最优解”(如最大价值、最小代价等)。 - 目标是求解
dp[0][n-1]
(整个区间的最优解,n
为区间长度)。
2.2 递推关系
对于区间[i,j]
,枚举所有可能的拆分点k
(i≤k<j
),则:
dp[i][j] = max/min( dp[i][k] + dp[k+1][j] + cost(i,j,k) )
cost(i,j,k)
为合并[i,k]
和[k+1,j]
时产生的额外代价(如矩阵乘法的计算量、戳气球的得分等,部分问题中cost=0
)。
2.3 遍历顺序
区间DP的关键是按区间长度从小到大遍历:
- 先处理长度为1的区间(
j=i
)——基础子问题,直接初始化。 - 再处理长度为2的区间(
j=i+1
)——依赖长度为1的子区间。 - 依次推进,直到处理长度为
n
的区间(整个区间)。
这种顺序确保在计算dp[i][j]
时,所有子区间[i,k]
和[k+1,j]
的解已被计算。
三、经典案例详解
3.1 案例1:最长回文子序列
问题描述
给定一个字符串s
,找到其中最长的回文子序列的长度(子序列可不连续)。
- 示例:输入
s = "bbbab"
,输出4
(最长回文子序列为"bbbb"
)。
区间DP设计
- 状态定义:
dp[i][j]
表示s[i..j]
中最长回文子序列的长度。 - 递推关系:
- 若
s[i] == s[j]
:两端字符相同,可加入回文子序列 →dp[i][j] = dp[i+1][j-1] + 2
。 - 若
s[i] != s[j]
:两端字符不同,取去掉左端或右端后的最大值 →dp[i][j] = max(dp[i+1][j], dp[i][j-1])
。
- 若
- 边界条件:长度为1的区间(
i==j
),dp[i][i] = 1
(单个字符是回文)。
代码实现
public class LongestPalindromicSubsequence {public int longestPalindromeSubseq(String s) {int n = s.length();int[][] dp = new int[n][n];// 初始化:长度为1的区间for (int i = 0; i < n; i++) {dp[i][i] = 1;}// 按区间长度从小到大遍历(len=2到n)for (int len = 2; len <= n; len++) {// 枚举区间起点i,终点j = i + len - 1for (int i = 0; i + len - 1 < n; i++) {int j = i + len - 1;if (s.charAt(i) == s.charAt(j)) {// 两端字符相同if (len == 2) {dp[i][j] = 2; // 长度为2的特殊情况(i+1 > j-1)} else {dp[i][j] = dp[i + 1][j - 1] + 2;}} else {// 两端字符不同,取子区间最大值dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);}}}return dp[0][n - 1];}public static void main(String[] args) {LongestPalindromicSubsequence solution = new LongestPalindromicSubsequence();System.out.println(solution.longestPalindromeSubseq("bbbab")); // 输出4}
}
3.2 案例2:矩阵链最优计算顺序
问题描述
给定n
个矩阵A1, A2, ..., An
(维度分别为p0×p1, p1×p2, ..., pn-1×pn
),求最少的乘法次数,使矩阵链相乘的总计算量最小(矩阵A(m×n)
与B(n×k)
相乘的计算量为m×n×k
)。
- 示例:矩阵维度
[10, 20, 30, 40]
(3个矩阵:10×20,20×30,30×40),最优顺序为(A1×A2)×A3
,总计算量10×20×30 + 10×30×40 = 6000 + 12000 = 18000
。
区间DP设计
- 状态定义:
dp[i][j]
表示计算矩阵链Ai...Aj
所需的最少乘法次数。 - 递推关系:
枚举拆分点k
(i≤k<j
),则Ai...Aj = (Ai...Ak) × (Ak+1...Aj)
,总计算量为:
dp[i][j] = min(dp[i][k] + dp[k+1][j] + p[i]×p[k+1]×p[j+1])
(p[i]
为矩阵Ai
的行数,p[j+1]
为矩阵Aj
的列数) - 边界条件:
dp[i][i] = 0
(单个矩阵无需乘法)。
代码实现
public class MatrixChainMultiplication {public int minCalculationCost(int[] p) {int n = p.length - 1; // 矩阵数量 = 维度数组长度 - 1int[][] dp = new int[n][n];// 初始化:单个矩阵的计算量为0for (int i = 0; i < n; i++) {dp[i][i] = 0;}// 按区间长度从小到大遍历(len=2到n)for (int len = 2; len <= n; len++) {for (int i = 0; i + len - 1 < n; i++) {int j = i + len - 1;dp[i][j] = Integer.MAX_VALUE; // 初始化为最大值// 枚举拆分点kfor (int k = i; k < j; k++) {// 计算当前拆分的总代价int cost = dp[i][k] + dp[k + 1][j] + p[i] * p[k + 1] * p[j + 1];dp[i][j] = Math.min(dp[i][j], cost);}}}return dp[0][n - 1];}public static void main(String[] args) {MatrixChainMultiplication solution = new MatrixChainMultiplication();int[] p = {10, 20, 30, 40}; // 3个矩阵:10×20, 20×30, 30×40System.out.println(solution.minCalculationCost(p)); // 输出18000}
}
3.3 案例3:戳气球
问题描述
有n
个气球,编号0
到n-1
,每个气球有分数nums[i]
。戳破第i
个气球可得nums[left] * nums[i] * nums[right]
(left
和right
为未戳破的相邻气球,边界外视为1
)。求戳破所有气球的最大得分。
- 示例:
nums = [3,1,5,8]
,输出167
(最优顺序:戳1→戳3→戳5→戳8,得分3×1×5 + 3×5×8 + 1×3×8 + 1×8×1 = 15 + 120 + 24 + 8 = 167
)。
区间DP设计
- 状态定义:
dp[i][j]
表示戳破(i,j)
之间(不包括i
和j
)所有气球的最大得分(i
和j
为边界,始终未被戳破)。 - 递推关系:
枚举最后戳破的气球k
(i<k<j
),此时i
和j
为相邻边界,得分:
dp[i][j] = max(dp[i][k] + dp[k][j] + nums[i] * nums[k] * nums[j])
(最后戳破k
,左右区间(i,k)
和(k,j)
已戳破,得分累加) - 边界条件:
dp[i][j] = 0
(当j = i+1
时,区间内无气球可戳)。
代码实现
public class BurstBalloons {public int maxCoins(int[] nums) {int n = nums.length;// 扩展数组,添加左右边界(值为1)int[] arr = new int[n + 2];arr[0] = 1;arr[n + 1] = 1;for (int i = 0; i < n; i++) {arr[i + 1] = nums[i];}int[][] dp = new int[n + 2][n + 2];// 按区间长度从小到大遍历(len=2到n+1,因为扩展后边界差至少为2才有气球)for (int len = 2; len <= n + 1; len++) {for (int i = 0; i + len < n + 2; i++) {int j = i + len;// 枚举最后戳破的气球k(i < k < j)for (int k = i + 1; k < j; k++) {int current = dp[i][k] + dp[k][j] + arr[i] * arr[k] * arr[j];dp[i][j] = Math.max(dp[i][j], current);}}}return dp[0][n + 1];}public static void main(String[] args) {BurstBalloons solution = new BurstBalloons();int[] nums = {3, 1, 5, 8};System.out.println(solution.maxCoins(nums)); // 输出167}
}
四、区间DP的关键技巧与优化
4.1 状态定义的灵活性
区间DP的状态定义需根据问题灵活调整:
- 基础型:
dp[i][j]
直接表示区间[i,j]
的最优解(如最长回文子序列)。 - 边界型:
dp[i][j]
表示“不包含i
和j
”的区间最优解(如戳气球,用边界简化计算)。 - 扩展型:加入额外维度(如
dp[i][j][k]
表示区间[i,j]
在k
状态下的最优解)。
4.2 遍历顺序的严格性
区间DP必须按区间长度递增的顺序遍历,否则会出现“子区间未计算”的错误。例如,计算dp[i][j]
(长度len
)时,所有长度小于len
的子区间[i,k]
和[k+1,j]
必须已计算完成。
4.3 复杂度分析与优化
- 时间复杂度:通常为
O(n^3)
(三层循环:区间长度、起点、拆分点),n
为区间长度。 - 优化方向:
- 减少拆分点枚举:部分问题中拆分点
k
的范围可缩小(如回文子序列无需枚举k
)。 - 状态压缩:对特殊问题(如区间长度为2的情况)可简化计算。
- 记忆化搜索:递归实现时用备忘录避免重复计算(适用于非连续区间)。
- 减少拆分点枚举:部分问题中拆分点
区间DP与其他DP的对比
维度 | 区间DP | 线性DP(如0/1背包) | 树形DP |
---|---|---|---|
状态维度 | 二维(i,j 表示区间) | 一维或二维(i 表示前i个元素) | 二维(u 表示节点,k 表示状态) |
遍历顺序 | 按区间长度递增 | 按元素顺序递增 | 按树的后序遍历 |
核心思想 | 区间拆分与合并 | 前i个元素的最优解 | 子树合并为父节点的解 |
典型应用 | 字符串区间、矩阵链 | 背包问题、序列问题 | 树的最大独立集、树形背包 |
总结
区间DP是解决区间最优问题的高效策略,其核心在于“以区间长度为线索,通过拆分与合并子区间求解”。从最长回文子序列的字符匹配,到矩阵链乘法的最优顺序,再到戳气球的得分最大化,区间DP始终遵循“子问题→合并→大问题”的逻辑,只是具体的状态定义和递推关系因问题而异。
掌握区间DP的关键在于:
- 准确定义
dp[i][j]
的含义,确保能通过子区间推导。 - 严格按区间长度递增的顺序遍历,避免子问题未计算的错误。
- 针对问题设计合理的递推关系,尤其是合并子区间时的代价计算。
That’s all, thanks for reading~~
觉得有用就点个赞
、收进收藏
夹吧!关注
我,获取更多干货~