目录
一、区间DP的本质与核心思想
区间动态规划(Interval DP) 是处理序列型问题的利器,其核心思想是将问题分解为连续子区间,通过合并相邻区间的解来推导更大区间的解。这类问题的典型特征是:
操作对象为连续区间(如字符串、数组、序列)
决策影响具有传递性(当前选择影响后续状态)
状态转移依赖区间划分(将大区间拆分为小区间组合)
二、区间DP的三大特征
状态定义:
dp[i][j]
表示处理区间[i,j]
的最优解递推方向:从短区间向长区间扩展(长度优先遍历)
决策点枚举:需要枚举区间分割点
k
进行状态合并
三、区间DP的经典问题类型
问题类型 | 典型问题 | 状态转移关键点 |
---|---|---|
合并类问题 | 石子合并、能量项链 | 合并代价计算与区间划分 |
回文类问题 | 最长回文子序列、分割回文串 | 首尾字符比较与区间收缩 |
匹配类问题 | 括号匹配得分、正则表达式匹配 | 匹配规则与区间配对 |
结构构造类问题 | 最优二叉搜索树、矩阵链乘法 | 分割点选择与权值计算 |
四、区间DP解题四步法
1. 状态定义
明确语义:
dp[i][j]
表示区间[i,j]
的最优解示例:石子合并问题中,
dp[i][j]
表示合并第i到第j堆石子的最小代价
2. 状态转移方程
枚举分割点k:将区间
[i,j]
分解为[i,k]
和[k+1,j]
合并子结果:根据问题规则合并两个子区间的解
示例:
dp[i][j] = min(dp[i][k] + dp[k+1][j] + cost(i,j))
3. 初始化
基础情况:长度为1的区间(i=j时)
示例:
dp[i][i] = 0
(单堆石子无需合并)
4. 遍历顺序
长度优先:外层循环枚举区间长度
len
起点遍历:内层循环枚举区间起点
i
,计算终点j = i + len - 1
五、经典案例:石子合并问题
1. 问题描述
有
N
堆石子排成一排,每堆石子重量为w[i]
。每次可以合并相邻两堆,合并代价为两堆重量之和。求将所有石子合并成一堆的最小总代价。
2. 输入示例
输入:w = [4, 2, 5, 3]
输出:34
解释:
1. 合并前两堆:4+2=6 → [6,5,3] 代价6
2. 合并后两堆:5+3=8 → [6,8] 代价8
3. 合并最后两堆:6+8=14 → [14] 代价14
总代价:6 + 8 + 14 = 28(实际最优解更小)
3. Java实现
public class StoneMerge {
public static int minCost(int[] w) {
int n = w.length;
int[] prefix = new int[n+1];
for (int i = 1; i <= n; i++) {
prefix[i] = prefix[i-1] + w[i-1]; // 前缀和预处理
}
int[][] dp = new int[n][n];
for (int len = 2; len <= n; len++) { // 枚举区间长度
for (int i = 0; i + len <= n; i++) { // 枚举起点
int j = i + len - 1;
dp[i][j] = Integer.MAX_VALUE;
int sum = prefix[j+1] - prefix[i]; // 当前区间总重量
for (int k = i; k < j; k++) { // 枚举分割点
dp[i][j] = Math.min(dp[i][j], dp[i][k] + dp[k+1][j] + sum);
}
}
}
return dp[0][n-1];
}
public static void main(String[] args) {
int[] stones = {4, 2, 5, 3};
System.out.println(minCost(stones)); // 输出34
}
}
4. 复杂度分析
时间复杂度:O(n³)
空间复杂度:O(n²)
六、优化技巧:四边形不等式
1. 核心思想
通过数学证明确定最优分割点
k
的单调性,将内层循环从O(n)
优化到O(1)
2. 优化后代码(部分)
int[][] s = new int[n][n]; // 记录最优分割点
for (int len = 2; len <= n; len++) {
for (int i = 0; i + len <= n; i++) {
int j = i + len - 1;
int left = s[i][j-1], right = s[i+1][j];
dp[i][j] = Integer.MAX_VALUE;
for (int k = left; k <= right; k++) {
if (dp[i][k] + dp[k+1][j] < dp[i][j]) {
dp[i][j] = dp[i][k] + dp[k+1][j];
s[i][j] = k;
}
}
dp[i][j] += prefix[j+1] - prefix[i];
}
}
七、其他经典问题实现
1. 最长回文子序列
public int longestPalindromeSubseq(String s) {
int n = s.length();
int[][] dp = new int[n][n];
for (int i = n-1; i >= 0; i--) {
dp[i][i] = 1;
for (int j = i+1; j < n; j++) {
if (s.charAt(i) == s.charAt(j)) {
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];
}
2. 戳气球
public int maxCoins(int[] nums) {
int n = nums.length;
int[] arr = new int[n+2];
System.arraycopy(nums, 0, arr, 1, n);
arr[0] = arr[n+1] = 1;
int[][] dp = new int[n+2][n+2];
for (int len = 1; len <= n; len++) {
for (int i = 1; i + len <= n + 1; i++) {
int j = i + len - 1;
for (int k = i; k <= j; k++) {
dp[i][j] = Math.max(dp[i][j],
dp[i][k-1] + arr[i-1]*arr[k]*arr[j+1] + dp[k+1][j]);
}
}
}
return dp[1][n];
}
八、区间DP的常见陷阱
循环顺序错误:必须先处理短区间再处理长区间
前缀和未预处理:重复计算区间和导致超时
分割点范围错误:
k
应满足i ≤ k < j
状态初始化遗漏:忘记处理长度为1的基础情况
九、LeetCode实战训练
-
基础练习
-
进阶挑战
十、总结与提升
区间DP的解题套路可归纳为:
1.定义区间状态 → 2. 预处理前缀和 → 3. 枚举分割点 → 4. 合并子区间解
高阶优化方向:
记忆化搜索实现
四边形不等式优化
状态压缩(如滚动数组)
问题转换(将环形问题转为线性)