[CSP-S 2019] 划分

GitHub跳转原题关系图返回列表

用单调队列维护每个前缀的最优上一段结尾,贪心取尽量靠后的可行断点。

OJ: luogu

题目 ID: P5665

难度:提高+/省选-

标签:动态规划贪心单调队列前缀和

日期: 2026-07-06 08:46

题意

给定正整数序列 a[1..n],要把它划分成若干个连续段,要求每一段的段和非递减。设各段段和为:

text
s1 <= s2 <= ... <= sk

目标是最小化:

text
s1^2 + s2^2 + ... + sk^2

type=0 时直接给出所有 a_itype=1 时按题面给出的递推和区间参数生成数据。

思路

小数据可以做动态规划:枚举最后一段从哪里开始,再检查它的段和是否不小于上一段。

cpp
// brute.cpp:小数据动态规划,枚举最后一段起点,要求段和非递减。
#include <bits/stdc++.h>
using namespace std;

const long long INF = (1LL << 62);

int n, type_id_input;
long long a[105], prefix_sum[105];
long long dp[105][105]; // dp[i][j]:前 i 个数,最后一段从 j+1 到 i

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> type_id_input;
    if (type_id_input == 0) {
        for (int i = 1; i <= n; i++) {
            cin >> a[i];
        }
    } else {
        // 对拍生成器只生成 type=0。这里保留读取入口,避免格式不完整。
        return 0;
    }

    for (int i = 1; i <= n; i++) {
        prefix_sum[i] = prefix_sum[i - 1] + a[i];
    }

    for (int i = 0; i <= n; i++) {
        for (int j = 0; j <= n; j++) {
            dp[i][j] = INF;
        }
    }

    for (int i = 1; i <= n; i++) {
        long long sum = prefix_sum[i];
        dp[i][0] = sum * sum;
    }

    for (int i = 1; i <= n; i++) {
        for (int last = 1; last < i; last++) {
            long long last_sum = prefix_sum[i] - prefix_sum[last];
            for (int prev = 0; prev < last; prev++) {
                if (dp[last][prev] == INF) {
                    continue;
                }
                long long prev_sum = prefix_sum[last] - prefix_sum[prev];
                if (prev_sum <= last_sum) {
                    dp[i][last] = min(dp[i][last], dp[last][prev] + last_sum * last_sum);
                }
            }
        }
    }

    long long answer = INF;
    for (int j = 0; j < n; j++) {
        answer = min(answer, dp[n][j]);
    }
    cout << answer << '\n';
    return 0;
}

满分做法的关键观察是:所有 a_i 都是正数。对于固定前缀 i,如果最后一段越短,它的平方贡献越小;因此在满足“上一段段和不超过最后一段段和”的前提下,我们希望最后一段尽量短,也就是上一段结尾尽量靠后。

令:

text
c[i] = a[1] + a[2] + ... + a[i]
g[i] = 前缀 i 的最优上一段结尾

最后一段是 (g[i]+1)..i,段和为:

text
c[i] - c[g[i]]

为了让它接在 g[i] 的最优划分后面,需要满足:

text
c[g[i]] - c[g[g[i]]] <= c[i] - c[g[i]]

移项得到:

text
2*c[g[i]] - c[g[g[i]]] <= c[i]

也就是说,每个候选断点 t 有一个关键值:

text
key(t) = 2*c[t] - c[g[t]]

key(t) <= c[i] 时,t 可以作为前缀 i 的上一段结尾。我们要选满足条件且尽量靠后的 t

扫描 i=1..n 时,c[i] 单调递增;同时可以证明最优候选的 key 也适合用单调队列维护。队头给出当前可行且最靠后的断点,队尾则维护候选 key 的单调性。

算出所有 g[i] 后,从 n 沿着 g[n]g[g[n]] 往前跳,就得到最优划分的所有段,累加段和平方即可。答案可能超过 long long,代码用 __int128 输出。

代码

cpp
// main.cpp:线性贪心。g[i] 表示处理到 i 时,最后一段从 g[i]+1 开始最优。
#include <bits/stdc++.h>
using namespace std;

const long long MOD_GEN = 1LL << 30;

int n, type_id_input;
vector<long long> prefix_sum;
vector<int> pre_pos, q;

void print_int128(__int128 x) {
    if (x == 0) {
        cout << 0;
        return;
    }
    if (x < 0) {
        cout << '-';
        x = -x;
    }
    string s;
    while (x > 0) {
        s.push_back((char)('0' + x % 10));
        x /= 10;
    }
    reverse(s.begin(), s.end());
    cout << s;
}

long long key_value(int idx) {
    return 2 * prefix_sum[idx] - prefix_sum[pre_pos[idx]];
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> type_id_input;
    prefix_sum.assign(n + 1, 0);
    pre_pos.assign(n + 1, 0);
    q.assign(n + 2, 0);

    if (type_id_input == 0) {
        for (int i = 1; i <= n; i++) {
            long long a;
            cin >> a;
            prefix_sum[i] = prefix_sum[i - 1] + a;
        }
    } else {
        long long x, y, z, b1, b2;
        int m;
        cin >> x >> y >> z >> b1 >> b2 >> m;
        vector<long long> b(n + 1, 0);
        b[1] = b1;
        b[2] = b2;
        for (int i = 3; i <= n; i++) {
            b[i] = (x * b[i - 1] + y * b[i - 2] + z) % MOD_GEN;
        }

        int last_p = 0;
        for (int i = 1; i <= m; i++) {
            int p;
            long long l, r;
            cin >> p >> l >> r;
            for (int j = last_p + 1; j <= p; j++) {
                long long a = b[j] % (r - l + 1) + l;
                prefix_sum[j] = prefix_sum[j - 1] + a;
            }
            last_p = p;
        }
    }

    int head = 1;
    int tail = 1;
    q[1] = 0;

    for (int i = 1; i <= n; i++) {
        while (head < tail && key_value(q[head + 1]) <= prefix_sum[i]) {
            head++;
        }
        pre_pos[i] = q[head];
        while (head < tail && key_value(i) <= key_value(q[tail])) {
            tail--;
        }
        q[++tail] = i;
    }

    __int128 answer = 0;
    int pos = n;
    while (pos > 0) {
        __int128 sum = prefix_sum[pos] - prefix_sum[pre_pos[pos]];
        answer += sum * sum;
        pos = pre_pos[pos];
    }

    print_int128(answer);
    cout << '\n';
    return 0;
}

复杂度

每个位置最多进出单调队列一次,时间复杂度 O(n)O(n)

空间复杂度为 O(n)O(n),需要保存前缀和、前驱位置和队列。

总结

本题表面是划分 DP,核心优化来自正数序列的贪心性质:最后一段在可行时应尽量短。把可行条件整理成 key(t) <= c[i] 后,就可以用单调队列在线维护最优断点。