把每个元素看成最终都会被某个不小于它的相邻块吞并一次,它的最优贡献是左右第一个不小于它的值中的较小者,用单调递减栈即可线性求解。
OJ: luogu
题目 ID: P4393
难度:提高+/省选-
标签:单调栈贪心区间dp思维
日期: 2026-06-21 01:59
题意
给定一个序列,每次可以选择一对相邻元素,把它们合并成一个数:
max(a[i], a[i+1])
这次操作的代价也等于这个最大值。
经过 n-1 次操作后,整个序列会缩成一个数。题目要求最小总代价。
思路
先看一个能帮助理解题意的小数据暴力:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 205;
int n;
int a[MAXN];
int mx[MAXN][MAXN];
long long dp[MAXN][MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
mx[i][i] = a[i];
dp[i][i] = 0;
}
for (int len = 2; len <= n; len++) {
for (int l = 1; l + len - 1 <= n; l++) {
int r = l + len - 1;
mx[l][r] = max(mx[l][r - 1], a[r]);
dp[l][r] = (1LL << 62);
// 最后一次合并一定是把 [l, k] 和 [k+1, r] 两段合并。
// 这一段的最终值一定是整个区间最大值 mx[l][r]。
for (int k = l; k < r; k++) {
long long current = dp[l][k] + dp[k + 1][r] + mx[l][r];
if (current < dp[l][r]) {
dp[l][r] = current;
}
}
}
}
cout << dp[1][n] << '\n';
return 0;
}brute.cpp 用的是区间 DP。
设 dp[l][r] 表示把区间 [l, r] 缩成一个数的最小代价。
最后一次合并一定是把 [l, k] 和 [k+1, r] 合在一起,而合并后的值一定是整个区间最大值,所以最后一次代价恒为 max(l..r)。
这个思路是对的,但复杂度太高,只能做对拍。
真正的关键是换一个角度:
每次合并后,较小的一侧会“消失”,较大值会保留下来。
所以除了全局最大值外,其他每个元素最终都会在某一步消失一次。
对某个元素 x 来说,它消失时必须并入一个不小于它的相邻块。为了让代价最小,它当然应该并到“左右两边第一个不小于它的值”中较小的那个上。
也就是说,如果:
- 左边第一个不小于
x的值是L - 右边第一个不小于
x的值是R
那么 x 的最优贡献就是:
min(L, R)
于是问题就转成了一个经典单调栈模型。
维护一个单调递减栈。
当扫描到当前值 x 时,只要栈顶 mid <= x:
x就是mid右边第一个不小于它的值- 弹出后新的栈顶就是
mid左边第一个不小于它的值
所以这时可以立刻结算:
mid 的贡献 = min(左侧更大值, x)
扫描结束后,栈中剩下的是一个严格递减序列。它们右边没有更大的值了,只能依次向左并入,因此继续清栈即可。
代码
#include <bits/stdc++.h>
using namespace std;
const int INF = 2000000000;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> st(n + 5);
int top = 0;
st[++top] = INF;
long long answer = 0;
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
// 当栈顶不大于当前值时,说明这个较小值已经找到了
// 左右两边第一个不小于它的候选值,可以现在结算它的最优贡献。
while (top > 1 && st[top] <= x) {
int mid = st[top--];
answer += min(st[top], x);
}
st[++top] = x;
}
// 扫描结束后,栈里剩下的是一个严格递减序列。
// 这些数只能依次向左合并,贡献就是它左边那个更大的值。
while (top > 2) {
top--;
answer += st[top];
}
cout << answer << '\n';
return 0;
}复杂度
每个元素最多入栈一次、出栈一次。
总时间复杂度:
空间复杂度:
总结
这题最难的不是写单调栈,而是先把问题重构成“每个元素各自贡献一次代价”。
一旦看清楚:
- 每个元素会被吞并一次
- 最优代价是左右第一个不小于它的值中的较小者
后面的单调栈就是非常自然的线性实现。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
