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. 算法流程

  1. 遍历 (i) 从 (1) 到 (N)。
  2. 更新最大值单调栈,计算区间增量并更新线段树。
  3. 更新最小值单调栈,计算区间增量并更新线段树。
  4. 查询线段树区间 ([0, i-1]) 的最小值,赋给 (dp[i])(前提是 (i \ge 2))。
  5. 将 (dp[i-1]) 插入线段树位置 (i),作为后续的候选切点。
  6. 输出 (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;
}
posted @ 2026-11-03 21:54  thenrify  阅读(2)  评论(0)    收藏  举报