文章目录
- 解决"区间划分"问题的一般方法论
-
- 方法论:解决区间划分问题的四步法
-
- [1. 问题分析与建模](#1. 问题分析与建模)
- [2. 动态规划状态的定义](#2. 动态规划状态的定义)
- [3. 状态转移方程](#3. 状态转移方程)
- [4. 初始条件与边界](#4. 初始条件与边界)
- 方法论应用:最小和最大石子合并得分
解决"区间划分"问题的一般方法论
在算法竞赛和动态规划领域,"区间划分"问题是一类重要的题型。它们的共同点是:
- 一个序列需要按某种规则被分成若干部分,
- 通过特定操作合并这些部分,达到最优目标(最小化或最大化某个值)。
这类问题需要我们构造合适的动态规划模型,掌握核心思路后,可以灵活解决多种场景下的区间划分问题。
方法论:解决区间划分问题的四步法
1. 问题分析与建模
对于"区间划分"问题,通常会问:
- 给定一个序列或区间,如何通过某种方式划分(或合并)使得某种代价最小化或收益最大化?
我们需要:
- 定义一个合理的状态表示当前问题的局部最优解。
- 找到问题的递归关系,即如何用更小的子问题求解当前问题。
2. 动态规划状态的定义
核心是找到一个合适的状态表示:
- 令
dp[i][j]
表示某个区间[i, j]
的最优解 - 比如,合并区间的最小代价或最大收益:
- 根据题目要求,
dp[i][j]
的定义可以是:- 从第
i
堆到第j
堆合并的最小得分。 - 从第
i
堆到第j
堆合并的最大得分。 - 等等。
- 从第
3. 状态转移方程
为了求解 dp[i][j]
,需要找到如何将 [i, j]
分成两部分,从而通过递归子问题解决区间问题:
- 通常通过一个分割点
k
,将区间[i, j]
分成两部分[i, k]
和[k+1, j]
。 - 我们假设
[i, k]
和[k+1, j]
的最优解已经通过dp
数组计算出来,那么:
d p [ i ] [ j ] = min i ≤ k < j ( d p [ i ] [ k ] + d p [ k + 1 ] [ j ] + 额外的代价 ) dp[i][j] = \min_{i \leq k < j} (dp[i][k] + dp[k+1][j] + \text{额外的代价}) dp[i][j]=i≤k<jmin(dp[i][k]+dp[k+1][j]+额外的代价)
或者:
d p [ i ] [ j ] = max i ≤ k < j ( d p [ i ] [ k ] + d p [ k + 1 ] [ j ] + 额外的收益 ) dp[i][j] = \max_{i \leq k < j} (dp[i][k] + dp[k+1][j] + \text{额外的收益}) dp[i][j]=i≤k<jmax(dp[i][k]+dp[k+1][j]+额外的收益)
额外的代价或收益通常与当前区间 [i, j]
的属性相关,比如它的总和、长度等。
4. 初始条件与边界
- 例如,常见的边界条件可以是:区间
[i, j]
的长度为 1,即i == j
,那么区间无需操作,从而dp[i][i] = 0
。 - 确保
dp[i][j]
的计算顺序满足依赖关系(从小区间到大区间逐步计算)。
方法论应用:最小和最大石子合并得分
接下来,将我们可以试着一步步将刚学到的方法论应用于实际的例子中
问题描述
我们有 n
堆石子排成一排,石子数依次为 [a1, a2, ..., an]
。我们需要通过 n-1
次合并,使所有石子堆合并成一堆,每次合并的得分为两堆石子的总和:
- 求使得总得分 最小 的合并方式。
- 求使得总得分 最大 的合并方式。
步骤 1:问题分析与建模
对于区间 [i, j]
,其最优合并得分(最小或最大)取决于:
- 如何将区间
[i, j]
分成两部分[i, k]
和[k+1, j]
。 - 每次合并产生的额外得分是区间
[i, j]
的总石子数sum[i][j]
。
我们定义:
dp_min[i][j]
:区间[i, j]
合并成一堆的最小得分。dp_max[i][j]
:区间[i, j]
合并成一堆的最大得分。
步骤 2:动态规划状态定义
- 状态
dp_min[i][j]
表示从第i
堆到第j
堆合并的最小代价。 - 状态
dp_max[i][j]
表示从第i
堆到第j
堆合并的最大代价。
步骤 3:状态转移方程
我们选择分割点 k
,将区间 [i, j]
分成两部分 [i, k]
和 [k+1, j]
:
- 最小得分:
d p _ m i n [ i ] [ j ] = min i ≤ k < j ( d p _ m i n [ i ] [ k ] + d p _ m i n [ k + 1 ] [ j ] + s u m [ i ] [ j ] ) dp\min[i][j] = \min{i \leq k < j} (dp\_min[i][k] + dp\_min[k+1][j] + sum[i][j]) dp_min[i][j]=i≤k<jmin(dp_min[i][k]+dp_min[k+1][j]+sum[i][j]) - 最大得分:
d p _ m a x [ i ] [ j ] = max i ≤ k < j ( d p _ m a x [ i ] [ k ] + d p _ m a x [ k + 1 ] [ j ] + s u m [ i ] [ j ] ) dp\max[i][j] = \max{i \leq k < j} (dp\_max[i][k] + dp\_max[k+1][j] + sum[i][j]) dp_max[i][j]=i≤k<jmax(dp_max[i][k]+dp_max[k+1][j]+sum[i][j])
其中 sum[i][j]
是区间 [i, j]
的总和,可以通过前缀和快速计算。
步骤 4:初始条件与实现
- 当
i == j
时,dp_min[i][i] = 0
和dp_max[i][i] = 0
,因为单个石子堆无需合并。 - 我们从长度为 2 的区间开始,逐步扩展到整个区间
[0, n-1]
。
代码实现
cpp
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> stones(n);
for (int i = 0; i < n; ++i) {
cin >> stones[i];
}
// 计算前缀和
vector<vector<int>> sum(n, vector<int>(n, 0));
for (int i = 0; i < n; ++i) {
sum[i][i] = stones[i];
for (int j = i + 1; j < n; ++j) {
sum[i][j] = sum[i][j - 1] + stones[j];
}
}
// 初始化 dp 数组
vector<vector<int>> dp_min(n, vector<int>(n, 0));
vector<vector<int>> dp_max(n, vector<int>(n, 0));
// 动态规划求解
for (int len = 2; len <= n; ++len) {
for (int i = 0; i <= n - len; ++i) {
int j = i + len - 1;
dp_min[i][j] = INT_MAX;
dp_max[i][j] = INT_MIN;
for (int k = i; k < j; ++k) {
dp_min[i][j] = min(dp_min[i][j], dp_min[i][k] + dp_min[k+1][j] + sum[i][j]);
dp_max[i][j] = max(dp_max[i][j], dp_max[i][k] + dp_max[k+1][j] + sum[i][j]);
}
}
}
// 输出最小和最大得分
cout << dp_min[0][n-1] << endl;
cout << dp_max[0][n-1] << endl;
return 0;
}
示例运行
输入:
4
4 5 9 4
输出:
44
54
解释:
- 最小得分:合并顺序通过动态规划找到,使得代价最小。
- 最大得分:合并顺序通过动态规划找到,使得收益最大。
总结
通过本例我们总结了解决"区间划分"问题的步骤:
- 问题建模:将问题描述转化为求解某个区间的最优解。
- 状态定义:用动态规划数组表示子区间的最优解。
- 状态转移:通过分割区间,递归地构建当前区间的最优解。
- 逐步求解:从小区间到大区间逐步扩展,最终得到整个问题的最优解。
掌握了这个方法论,你就可以轻松应对类似的区间动态规划问题了!
希望本篇博客对你有所帮助!