目录
1000. 合并石头的最低成本 - 力扣(LeetCode)
区间DP定义
区间类动态规划是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来有很大的关系。
令状态 f(i, j) 表示将下标位置 i 到 j 的所有元素合并能获得的价值的最大值,那么有状态转移方程
其中cost 为将这两组元素合并起来的代价。
性质
区间 DP 有以下特点:
合并:即将两个或多个部分进行整合,当然也可以反过来;
特征:能将问题分解为能两两合并的形式;
求解:对整个问题设最优值,枚举合并点,将问题分解为左右两个部分,最后合并两个部分的最优值得到原问题的最优值。
例题
[NOIP] [一本通5.1 例1] 石子合并
思路解析
首先发现这道题的关键是 “区间合并”、“最优值”。所以我们可以是使用区间DP。但是这道题有两个难点:1.嵌套循环过多。2.是环型区间DP,而不是链型区间DP。
PS:可以自行设计初始版本的答案,你会发现难点1的。
策略1:区间和优化--前缀数组
而合并区间的代价是 。而面对区间和,我们完全可以使用前缀和数组优化。即
。
至此,我们解决了第一个难点嵌套循环过多。
策略2:环型区间--倍长
当然最简单的策略是剪断环,让其成为链型区间DP。但是那样我们需要N次剪断操作。这样我们就白优化了。
在这里我们使用倍长,或者是合并N条简短获取的链条。
我们答案就在 f(1,n), f(2, n + 1)...f(n, 2*n-1)中产生。
AC代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int INF = 0x3F3F3F3F;
const int N = 205;
int arr[N];
struct info {
int max, min;
info() :max(0), min(INF) {};
};
info dp[2 * N][2 * N];
int main() {
int n;
cin >> n;
for (int i = 0; i < n; ++i) scanf("%d", arr + i);
vector<int> preSum(2 * n + 1);
for (int i = 1; i <= 2 * n; ++i) {
preSum[i] = preSum[i - 1] + arr[(i - 1) % n];
}
for (int i = 1; i <= 2*n; ++i) {
dp[i][i].min = 0;
}
for (int len = 2; len <= n; ++len) { //长度和起点配合固定区间
for (int i = 1; i <= 2 * n - len + 1; ++i) { // 起点
int j = i + len - 1;
for (int k = i; k < j; ++k) {
dp[i][j].max = max(dp[i][j].max, dp[i][k].max + dp[k + 1][j].max + preSum[j] - preSum[i - 1]);
dp[i][j].min = min(dp[i][j].min, dp[i][k].min + dp[k + 1][j].min + preSum[j] - preSum[i - 1]);
}
}
}
info ans;
for (int i = 1; i <= n; ++i) {
ans.max = max(ans.max, dp[i][i + n - 1].max);
ans.min = min(ans.min, dp[i][i + n - 1].min);
}
printf("%d\n%d", ans.min, ans.max);
return 0;
}
1000. 合并石头的最低成本 - 力扣(LeetCode)
思路解析
同样的,这道题也是区间合并,但是稍有不同的是限制了区间合并的长度。而这造就了一个难题。就是我们需要枚举区间是如何合并的。而枚举带来的代价毛估估也是O(),因为这是一个排列组合问题。
但是注意到一个基本事实,而这个事实也是众多最优值优化的关键。最优值产生是固定的状态,可确定的。所以,我只需要额外记录这部分信息即可。
那么,难点就在于如何记录这部分的信息。最朴实的想法是,我们想知道他们是如何合并的。可是我记录他们的讯息有些高昂,所以我们将次策略滞后。
再深入分析,我真的需要知道它们具体是如何合并的吗?我只需要知道最小合并代价。我们似乎并不需要关心它们是如何合并的。假设你现在在处理一种情况,例如你现在想将 [i, j] 的石子分成 K 段然和在合并成一段。那么在分成 K 段的情况中,我们将 [i, k] 分成一段,那么需要将 [k + 1, j] 分成 K - 1 段。在这过程中,我们不关心[k + 1, j] 分成 K - 1 段具体的细节,而是关心最后的代价。
所以,我们可以给出 动规数组 d[i][j][k]表示将 [i, j] 分成 k 段的代价。
那么AC代码如下:
class Solution {
static constexpr int inf = 0x3f3f3f3f;
public:
int mergeStones(vector<int>& stones, int k) {
int n = stones.size();
if ((n - 1) % (k - 1) != 0) {// (n - k) % (k - 1) != 0 等价于此式子,判断是否能合并成一堆
return -1;
}
vector d(n, vector(n, vector<int>(k + 1, inf)));
vector<int> sum(n, 0);
for (int i = 0, s = 0; i < n; i++) {
d[i][i][1] = 0;//将 [i,i] 合并为 1堆代价为0
s += stones[i];
sum[i] = s;//前缀和,优化区间合并代价
}
for (int len = 2; len <= n; len++) {
for (int l = 0; l < n && l + len - 1 < n; l++) {
int r = l + len - 1;
for (int t = 2; t <= k; t++) {//先考虑将该区间分成若干段,为之后DP和合并成1段做信息准备
for (int p = l; p < r; p += k - 1) {// 为了可以分成合并为1段,所以p+=k-1,即p满足 (p-l) % (k - 1) == 0;
d[l][r][t] = min(d[l][r][t], d[l][p][1] + d[p + 1][r][t - 1]);
}
}
d[l][r][1] = min(d[l][r][1], d[l][r][k] +
sum[r] - (l == 0 ? 0 : sum[l - 1]));
}
}
return d[0][n - 1][1];
}
};