区间动态规划概述
区间动态规划(Interval Dynamic Programming)是动态规划的一种特殊形式,主要用于解决涉及区间或子序列的问题。这类问题通常需要计算某个区间的最优解,并通过合并子区间的最优解来构造更大区间的最优解。常见应用包括矩阵链乘法、石子合并、最长回文子序列等。
区间动态规划的基本思想
区间动态规划的核心思想是将问题分解为若干子区间,通过求解子区间的最优解来逐步构建更大区间的最优解。其基本步骤如下:
- 定义状态 :通常使用二维数组
dp[i][j]表示区间[i, j]的最优解。 - 初始化状态:对长度为1的区间进行初始化。
- 状态转移方程:根据问题特点,设计如何通过子区间的最优解合并得到更大区间的最优解。
- 计算顺序:按照区间长度从小到大进行计算。
区间动态规划的经典问题
石子合并问题
问题描述:有 n 堆石子排成一列,每次只能合并相邻的两堆石子,合并的代价为两堆石子的数量之和。求将所有石子合并成一堆的最小总代价。
状态定义 :
dp[i][j] 表示合并第 i 堆到第 j 堆石子的最小代价。
初始化 :
对于长度为1的区间,合并代价为0,即 dp[i][i] = 0。
状态转移方程 :
对于区间 [i, j],枚举分割点 k(i ≤ k < j),将区间分为 [i, k] 和 [k+1, j],合并代价为 dp[i][k] + dp[k+1][j] + sum[i][j],其中 sum[i][j] 是区间 [i, j] 的石子总数。
cpp
for (int len = 2; len <= n; ++len) {
for (int i = 1; i + len - 1 <= n; ++i) {
int j = i + len - 1;
dp[i][j] = INF;
for (int k = i; k < j; ++k) {
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1]);
}
}
}
矩阵链乘法
问题描述:给定一系列矩阵,计算它们相乘的最小乘法次数。矩阵乘法的次数由矩阵的维度决定。
状态定义 :
dp[i][j] 表示计算第 i 个到第 j 个矩阵相乘的最小乘法次数。
初始化 :
对于单个矩阵,乘法次数为0,即 dp[i][i] = 0。
状态转移方程 :
对于区间 [i, j],枚举分割点 k(i ≤ k < j),将问题分解为计算 [i, k] 和 [k+1, j] 的最小乘法次数,再加上合并这两个子问题的代价。
cpp
for (int len = 2; len <= n; ++len) {
for (int i = 1; i + len - 1 <= n; ++i) {
int j = i + len - 1;
dp[i][j] = INF;
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]);
}
}
}
区间动态规划的优化
在某些情况下,区间动态规划可以通过优化技术(如四边形不等式)降低时间复杂度。例如,石子合并问题的时间复杂度可以从 O(n\^3) 优化到 O(n\^2)。
区间动态规划的代码实现
以下是一个完整的石子合并问题的C++实现:
cpp
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> stones(n + 1, 0);
vector<int> prefix_sum(n + 1, 0);
for (int i = 1; i <= n; ++i) {
cin >> stones[i];
prefix_sum[i] = prefix_sum[i - 1] + stones[i];
}
vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0));
for (int len = 2; len <= n; ++len) {
for (int i = 1; i + len - 1 <= n; ++i) {
int j = i + len - 1;
dp[i][j] = INT_MAX;
for (int k = i; k < j; ++k) {
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + prefix_sum[j] - prefix_sum[i - 1]);
}
}
}
cout << dp[1][n] << endl;
return 0;
}
区间动态规划的常见问题
- 边界条件:初始化时需注意区间长度为1的情况。
- 计算顺序:必须按照区间长度从小到大计算。
- 时间复杂度:未经优化的区间动态规划通常为 O(n\^3),需注意数据规模。
区间动态规划的扩展
区间动态规划可以与其他算法结合,例如记忆化搜索、单调队列优化等。以下是一个使用记忆化搜索实现的区间动态规划示例:
cpp
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
vector<vector<int>> dp;
vector<int> prefix_sum;
int solve(int i, int j) {
if (i == j) return 0;
if (dp[i][j] != -1) return dp[i][j];
int res = INT_MAX;
for (int k = i; k < j; ++k) {
res = min(res, solve(i, k) + solve(k + 1, j) + prefix_sum[j] - prefix_sum[i - 1]);
}
return dp[i][j] = res;
}
int main() {
int n;
cin >> n;
prefix_sum.resize(n + 1, 0);
for (int i = 1; i <= n; ++i) {
cin >> prefix_sum[i];
prefix_sum[i] += prefix_sum[i - 1];
}
dp.assign(n + 1, vector<int>(n + 1, -1));
cout << solve(1, n) << endl;
return 0;
}
区间动态规划的注意事项
- 状态定义 :明确
dp[i][j]的具体含义。 - 初始化:确保所有必要的初始状态已正确设置。
- 状态转移:确保转移方程覆盖所有可能的分割点。
- 计算顺序:避免重复计算或遗漏某些状态。
区间动态规划的练习题
- 石子合并:计算合并相邻石子的最小代价。
- 矩阵链乘法:计算矩阵相乘的最小乘法次数。
- 最长回文子序列:寻找给定字符串的最长回文子序列。
- 括号匹配:计算最长的合法括号子序列。
区间动态规划的总结
区间动态规划是一种高效解决区间或子序列问题的算法。通过将问题分解为子区间并逐步合并,能够有效求解复杂的最优化问题。掌握区间动态规划的关键在于理解状态定义、转移方程和计算顺序。通过大量练习,可以熟练应用区间动态规划解决实际问题。