区间DP的状态转移通常有两种形式:
合并相邻区间:将区间[i,j]分为[i,k]和[k+1,j]两部分
dp[i][j] = min/max(dp[i][k] + dp[k+1][j] + cost(i,j,k))
其中cost(i,j,k)是合并这两个子区间的代价
枚举区间中的点:考虑区间[i,j]中的每个点k
dp[i][j] = min/max(dp[i][k-1] + dp[k+1][j] + cost(i,j,k))
dp[i][j] = min/max(dp[i][k] + dp[k][j] + cost(i,j,k))
关键是找到适合问题的状态转移方式和正确的计算顺序。
i和j表示区间[i,j]区间DP的优势:
区间DP的局限性:
如何识别区间DP问题:
dp[i][j]表示区间[i,j]的解常见优化技巧:
dp[i][j]表示区间[i,j]的最优解dp[i]通常表示以位置i结尾的最优解dp[i][j]通常表示前i个物品在容量j下的最优解理解这些区别有助于正确识别问题类型并选择合适的解决方案。
vector<vector<int>> dp(n, vector<int>(n, initial_value));// 初始化DP数组
// 初始化长度为1的区间(基础情况)
for (int i = 0; i < n; i++) {
dp[i][i] = base_case_value;
}
// 按照区间长度递增计算
for (int len = 2; len <= n; len++) {
// 枚举区间起点
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1; // 计算区间终点
dp[i][j] = initial_extreme_value; // 初始化为最大/最小值
// 枚举分割点
for (int k = i; k < j; k++) {
dp[i][j] = min/max(dp[i][j], dp[i][k] + dp[k+1][j] + cost(i, j, k));// 状态转移
}
}
}
// 最终结果通常是dp[0][n-1]或dp[1][n]
return dp[0][n-1];
vector<vector<int>> memo(n, vector<int>(n, -1));// 初始化记忆化数组
// 递归函数
int solve(int i, int j) {
// 基础情况
if (i == j) return base_case_value;
// 如果已经计算过,直接返回结果
if (memo[i][j] != -1) return memo[i][j];
int result = initial_extreme_value;// 初始化为最大/最小值
// 枚举分割点
for (int k = i; k < j; k++) {
result = min/max(result, solve(i, k) + solve(k+1, j) + cost(i, j, k)); // 状态转移
}
// 存储结果
memo[i][j] = result;
return result;
}
return solve(0, n-1);// 调用入口
// 预处理前缀和
vector<int> prefix_sum(n + 1, 0);
for (int i = 1; i <= n; i++) {
prefix_sum[i] = prefix_sum[i-1] + arr[i-1];
}
// 计算区间[i,j]的和
int range_sum = prefix_sum[j+1] - prefix_sum[i];
// 记录每个区间的最优分割点
vector<vector<int>> opt(n, vector<int>(n));
for (int len = 2; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
// 利用单调性,只在[opt[i][j-1], opt[i+1][j]]范围内枚举k
// for (int k = opt[i][j-1]; k <= opt[i+1][j]; k++) {
// // 状态转移
// // ...
}
}
问题描述:
有N堆石子排成一排,每堆石子有一定的质量。现在要将这N堆石子合并成一堆,每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和。问题是:将N堆石子合并成一堆,最小的总代价是多少?
分析:
dp[i][j]表示将区间[i,j]内的石子合并成一堆的最小代价dp[i][j] = min(dp[i][k] + dp[k+1][j] + sum(i,j))sum(i,j)是区间[i,j]内所有石子的质量之和dp[i][i] = 0(单堆石子不需要合并)// 石子合并问题
int mergeStones(vector<int>& stones) {
int n = stones.size();
if (n <= 1) return 0;
// 计算前缀和,用于快速获取区间和
vector<int> prefix_sum(n + 1, 0);
for (int i = 0; i < n; i++) {
prefix_sum[i + 1] = prefix_sum[i] + stones[i];
}
// dp[i][j]表示合并区间[i,j]的最小代价
vector<vector<int>> dp(n, vector<int>(n, INT_MAX));
// 初始化:单个石子堆不需要合并
for (int i = 0; i < n; i++) {
dp[i][i] = 0;
}
// 按区间长度递增计算
for (int len = 2; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
// 枚举分割点
for (int k = i; k < j; k++) {
dp[i][j] = min(dp[i][j],
dp[i][k] + dp[k+1][j] +
prefix_sum[j+1] - prefix_sum[i]);
}
}
}
return dp[0][n-1];
}
时间复杂度:O(n³)
空间复杂度:O(n²)
问题描述:
给定一个字符串s,找出其中最长的回文子序列,并返回该序列的长度。子序列是指从原序列中可以通过删除某些字符(也可以不删除)而不改变剩余字符相对位置形成的新序列。
分析:
dp[i][j]表示字符串s在区间[i,j]上的最长回文子序列长度s[i] == s[j],则dp[i][j] = dp[i+1][j-1] + 2dp[i][j] = max(dp[i+1][j], dp[i][j-1])dp[i][i] = 1(单个字符是长度为1的回文)// 最长回文子序列
int longestPalindromeSubseq(string s) {
int n = s.length();
if (n <= 1) return n;
// dp[i][j]表示区间[i,j]上的最长回文子序列长度
vector<vector<int>> dp(n, vector<int>(n, 0));
// 初始化:单个字符是长度为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;
if (s[i] == s[j]) {
dp[i][j] = dp[i+1][j-1] + 2;
} else {
dp[i][j] = max(dp[i+1][j], dp[i][j-1]);
}
}
}
return dp[0][n-1];
}
时间复杂度:O(n²)
空间复杂度:O(n²)
问题描述:
给定n个矩阵的链,矩阵i的维度为p[i-1] x p[i]。求完成所有矩阵相乘所需的最小标量乘法次数。
分析:
dp[i][j]表示计算矩阵链A[i]A[i+1]...A[j]所需的最小乘法次数dp[i][j] = min(dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j])p[i-1]*p[k]*p[j]是合并两个子矩阵链的代价dp[i][i] = 0(单个矩阵不需要乘法)// 矩阵链乘法
int matrixChainMultiplication(vector<int>& p) {
int n = p.size() - 1; // n个矩阵
if (n <= 1) return 0;
// dp[i][j]表示计算矩阵链A[i]A[i+1]...A[j]所需的最小乘法次数
vector<vector<int>> dp(n + 1, vector<int>(n + 1, INT_MAX));
// 初始化:单个矩阵不需要乘法
for (int i = 1; i <= n; i++) {
dp[i][i] = 0;
}
// 按区间长度递增计算
for (int len = 2; len <= n; len++) {
for (int i = 1; i <= n - len + 1; i++) {
int j = i + len - 1;
// 枚举分割点
for (int k = i; k < j; k++) {
dp[i][j] = min(dp[i][j],
dp[i][k] + dp[k+1][j] +
p[i-1] * p[k] * p[j]);
}
}
}
return dp[1][n];
}
时间复杂度:O(n³)
空间复杂度:O(n²)
记录最优分割点:
// 记录最优分割点
vector<vector<int>> split(n + 1, vector<int>(n + 1, 0));
// 在状态转移时记录
for (int len = 2; len <= n; len++) {
for (int i = 1; i <= n - len + 1; i++) {
int j = i + len - 1;
for (int k = i; k < j; k++) {
int cost = dp[i][k] + dp[k+1][j] +
p[i-1] * p[k] * p[j];
if (cost < dp[i][j]) {
dp[i][j] = cost;
split[i][j] = k; // 记录最优分割点
}
}
}
}
重构最优解:
// 打印最优括号化方案
void printOptimalParens(vector<vector<int>>& split,
int i, int j) {
if (i == j) {
cout << "A" << i;
} else {
cout << "(";
printOptimalParens(split, i, split[i][j]);
printOptimalParens(split, split[i][j] + 1, j);
cout << ")";
}
}
// 调用入口
printOptimalParens(split, 1, n);
优化思路:
以下是一些适合练习区间动态规划的经典题目:
区间DP的核心要点:
dp[i][j]表示区间[i,j]的最优解区间DP的应用场景:
常见错误:
学习建议:
区间DP的进阶内容:
常见优化技巧:
实践建议:
记住:区间DP是一种思想,关键在于将大区间问题分解为小区间问题,并找到正确的状态转移方式。

这个讲义使用了Awesome Marp主题的蓝色风格。 演讲者备注不会在演示模式中显示给观众,仅供演讲者参考。 每个关键页面都添加了备注,帮助演讲者更好地讲解内容。
封面页: - 欢迎学生,简要介绍本次课程的主题和目标 - 强调区间DP是动态规划的重要分支,在算法竞赛和实际应用中都很常见 - 可以简单提及本课程将从基础概念到实际应用,全面讲解区间DP的思想和技巧
目录页: - 简要介绍本次讲座的结构和内容安排 - 可以提及讲座的逻辑流程:从基础概念到具体应用,再到实践建议 - 强调我们将通过三个经典例子(石子合并问题、最长回文子序列、矩阵链乘法)来深入理解区间DP - 提醒学生关注每个部分之间的联系,以及如何将理论应用到实践中
转场页: - 这是一个简单的转场页,用于引入动态规划概述部分 - 可以简单提及动态规划是算法设计中的重要范式,有着广泛的应用
动态规划概述: - 介绍动态规划的基本概念和核心思想 - 强调最优子结构和重叠子问题的重要性 - 简要提及动态规划的常见应用场景
动态规划的分类: - 介绍动态规划的几种常见分类 - 强调区间DP是其中一种重要的类型 - 简要说明每种类型的特点和应用场景
转场页: - 引入区间DP的基本概念部分 - 可以简单提及区间DP是处理区间问题的有力工具
区间DP基本概念: - 详细介绍区间DP的定义和基本思想 - 强调区间DP的状态定义和转移特点 - 解释为什么某些问题适合用区间DP解决
区间DP的计算过程: - 详细解释区间DP的计算过程和状态转移 - 使用图示直观展示区间DP的计算顺序 - 强调区间长度的重要性
区间DP的状态转移: - 详细解释区间DP的状态转移方程 - 使用具体例子说明状态转移的过程 - 强调区间划分点k的重要性
转场页: - 引入区间DP的特点部分 - 可以简单提及理解这些特点有助于更好地应用区间DP
区间DP的特点: - 详细介绍区间DP的主要特点 - 强调这些特点如何帮助识别和解决区间DP问题 - 解释区间DP与其他DP类型的区别
区间DP的优缺点: - 分析区间DP的优势和局限性 - 讨论何时应该选择区间DP - 提供一些优化区间DP的思路
区间DP与其他DP的区别: - 比较区间DP与其他类型DP的异同 - 强调区间DP的独特之处 - 提供一些实际应用的例子
转场页: - 引入区间DP的模板代码部分 - 可以简单提及掌握这些模板有助于快速实现区间DP算法
区间DP的基本模板: - 提供区间DP的基本代码模板 - 解释模板中的关键部分 - 强调计算顺序的重要性
区间DP的记忆化搜索模板: - 提供使用记忆化搜索实现区间DP的代码模板 - 解释记忆化搜索的优势 - 比较迭代实现和记忆化搜索实现的异同
区间DP的优化技巧: - 提供一些常见的优化技巧 - 解释这些技巧如何提高算法效率 - 提供一些实际应用的例子
转场页: - 引入实例分析部分 - 可以简单提及我们将通过三个经典例子来深入理解区间DP
石子合并问题: - 详细介绍石子合并问题的描述和分析 - 解释如何使用区间DP解决这个问题 - 强调状态定义和转移方程的推导过程
石子合并问题的代码实现: - 提供完整的代码实现 - 解释代码中的关键部分 - 分析算法的时间和空间复杂度
最长回文子序列问题: - 详细介绍最长回文子序列问题的描述和分析 - 解释如何使用区间DP解决这个问题 - 强调状态定义和转移方程的推导过程
最长回文子序列问题的代码实现: - 提供完整的代码实现 - 解释代码中的关键部分 - 分析算法的时间和空间复杂度
矩阵链乘法问题: - 详细介绍矩阵链乘法问题的描述和分析 - 解释如何使用区间DP解决这个问题 - 强调状态定义和转移方程的推导过程
矩阵链乘法问题的代码实现: - 提供完整的代码实现 - 解释代码中的关键部分 - 分析算法的时间和空间复杂度
矩阵链乘法问题的优化: - 介绍矩阵链乘法问题的优化方法 - 解释如何记录最优分割点以重构最优解 - 分析优化后的算法效率
转场页: - 引入练习题推荐部分 - 可以简单提及练习是掌握区间DP的关键
练习题推荐: - 提供一系列适合练习区间DP的题目 - 简要介绍每道题的内容和难度 - 提供题目链接或参考资料
转场页: - 引入总结部分 - 可以简单提及总结将回顾本次讲座的主要内容
总结: - 回顾区间DP的核心概念和特点 - 强调区间DP的应用场景和解题思路 - 提供一些学习和实践的建议
进阶内容和实践建议: - 介绍区间DP的进阶内容和优化技巧 - 提供一些实践建议和学习资源 - 鼓励学生继续深入学习和实践
封底页: - 感谢学生的参与 - 提供联系方式或其他资源 - 鼓励学生继续学习和实践