Zonal Score Minimization
Zonal Score Minimization
划分dp + 线段树 + 单调栈
1. 题目描述
给定一个长度为 (N) 的正整数序列 (A)。现在需要将 (A) 划分为若干连续子序列,要求每个子序列的长度至少为 (2)。
一个序列的得分为该序列中最大值与最小值的差。求所有划分方案中,所有子序列得分总和的最小值。
数据范围:(2 \le N \le 2 \times 10^5),(1 \le A_i \le 10^9)。
2. 动态规划定义
设 (dp[i]) 表示将前 (i) 个元素划分成若干合法段的最小总得分。
最后一段如果是从 (A_{j+1}) 到 (A_i),那么需要满足长度至少为 (2),即切点 (j \le i-2)。
最后一段的得分是 (\max(A_{j+1}, \ldots, A_i) - \min(A_{j+1}, \ldots, A_i))。
状态转移方程:
[
dp[i] = \min_{0 \le j \le i-2} \left( dp[j] + \max_{j+1 \le k \le i} A_k - \min_{j+1 \le k \le i} A_k \right)
]
3. 线段树 + 单调栈优化
直接枚举 (j) 的复杂度是 (O(N^2)),需要优化。
对于固定的右端点 (i),我们需要维护所有合法切点 (j) 的 (dp[j] + \max(A_{j+1 \ldots i}) - \min(A_{j+1 \ldots i}))。
我们可以用线段树来维护这些值。
3.1 坐标系映射(关键技巧)
为了将区间左右端点和线段树下标完美对齐,我们作如下映射:
- 线段树位置 (p):代表最后一段从原数组的第 (p) 个元素开始(即切点 (j = p - 1))。
- 初始值:对于切点 (j),线段树 (p = j+1) 的初始值为 (dp[j])。
- 约束条件:最后一段长度至少为 (2),意味着 (j \le i-2),对应线段树查询范围就是 (p \in [0, i-1])。
3.2 单调栈维护最值变化
随着右端点 (i) 向右推进,加入新元素 (A_i) 时,不同起点的区间的最大值和最小值会发生阶梯状的变化。
- 最大值:用单调递减栈维护。如果 (A_i) 大于栈顶记录的值 (v),说明这些区间的最大值变成了 (A_i),增量就是 (A_i - v)。这部分增量可以通过线段树的区间加来实现。
- 最小值:用单调递增栈维护。如果 (A_i) 小于栈顶记录的值 (v),说明这些区间的最小值变成了 (A_i)。因为状态中减去了最小值,整体增量是 (v - A_i),同样通过线段树区间加实现。
3.3 避免区间更新的重叠(易错点)
在弹出栈顶时,假设栈中存放的是 (值 v, 原数组左端点 l)。
当连续弹出多个段时,当前段和下一段的更新区间在右边界上会发生重叠。
正确做法:维护一个动态右边界 right,初始为 (i)。每次弹出一个段后更新 [last, right],然后更新 right = l - 1,向左收缩,确保每个段的影响区间被精确、不重叠地更新。
4. 算法流程
- 遍历 (i) 从 (1) 到 (N)。
- 更新最大值单调栈,计算区间增量并更新线段树。
- 更新最小值单调栈,计算区间增量并更新线段树。
- 查询线段树区间 ([0, i-1]) 的最小值,赋给 (dp[i])(前提是 (i \ge 2))。
- 将 (dp[i-1]) 插入线段树位置 (i),作为后续的候选切点。
- 输出 (dp[N])。
5. 复杂度分析
- 每个元素最多入栈、出栈一次,线段树区间更新 (O(\log N)),总更新次数 (O(N))。
- 每次查询和单点赋值 (O(\log N))。
- 总时间复杂度:(O(N \log N))。
- 空间复杂度:(O(N))。
6. C++ 参考代码
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
using pli = pair<ll, int>;
using tpi = tuple<ll, int, int>;
const ll INF = 1e18;
const ll MOD = 998244353;
struct SegTree {
int n;
vector<ll> mn, lazy;
SegTree(int n) : n(n), mn(4 * n + 10, INF), lazy(4 * n + 10, 0) {}
void down(int p, ll v) {
mn[p] += v;
lazy[p] += v;
}
void push(int p) {
if(lazy[p] != 0) {
down(p << 1, lazy[p]);
down(p << 1 | 1, lazy[p]);
lazy[p] = 0;
}
}
void point_update(int p, int l, int r, int pos, ll val) {
if(l == r) {
mn[p] = val;
lazy[p] = 0;
return;
}
push(p);
int m = (l + r) >> 1;
if(pos <= m) point_update(p << 1, l, m, pos, val);
else point_update(p << 1 | 1, m + 1, r, pos, val);
mn[p] = min(mn[p << 1], mn[p << 1 | 1]);
}
void update_val(int p, int l, int r, int ql, int qr, ll val) {
if(ql > r || qr < l) return;
if(ql <= l && r <= qr) {
down(p, val);
return;
}
push(p);
int m = (l + r) >> 1;
update_val(p << 1, l, m, ql, qr, val);
update_val(p << 1 | 1, m + 1, r, ql, qr, val);
mn[p] = min(mn[p << 1], mn[p << 1 | 1]);
}
ll query(int p, int l, int r, int ql, int qr) {
if(ql > r || qr < l) return INF;
if(ql <= l && r <= qr) return mn[p];
push(p);
int m = (l + r) >> 1;
return min(query(p << 1, l, m, ql, qr), query(p << 1 | 1, m + 1, r, ql, qr));
}
void point_update(int id, ll val) {
point_update(1, 0, n, id, val);
}
void update_val(int l, int r, ll dif) {
if(l > r) return;
update_val(1, 0, n, l, r, dif);
}
ll query(int l, int r) {
if(l > r) return INF;
return query(1, 0, n, l, r);
}
};
void solve() {
int n; cin >> n;
vector<ll> a(n + 1);
for(int i = 1; i <= n; i ++) cin >> a[i];
SegTree seg(n);
vector<pli> minst, maxst;
vector<ll> dp(n + 1, INF); dp[0] = 0;
for(int i = 1; i <= n; i ++) {
{
//更新最大值
int last = (maxst.empty() ? 0 : maxst.back().second + 1);
seg.update_val(last, i, a[i]);
while(!maxst.empty() && maxst.back().first <= a[i]) {
auto [v, l] = maxst.back(); maxst.pop_back(); // l为当前v能影响到的最前面的范围
last = (maxst.empty() ? 0 : maxst.back().second + 1);
seg.update_val(last, l, a[i] - v);
}
maxst.push_back({a[i], i});
//更新最小值
last = (minst.empty() ? 0 : minst.back().second + 1);
seg.update_val(last, i, -a[i]);
while(!minst.empty() && minst.back().first >= a[i]) {
auto [v, l] = minst.back(); minst.pop_back();
last = (minst.empty() ? 0 : minst.back().second + 1);
seg.update_val(last, l, v - a[i]);
}
minst.push_back({a[i], i});
}
if(i >= 2) dp[i] = seg.query(0, i - 1);
seg.point_update(i, dp[i - 1]); //更新 i - 1,下一步到i + 1,刚好区间长度至少为2
}
//for(int i = 1; i <= n; i ++) cout << dp[i] << " ";
cout << dp[n] << "\n";
}
int main() {
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int _ = 1; //cin >> _;
while(_--) solve();
return 0;
}

浙公网安备 33010602011771号