[CSP-S 2019] 划分

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

题意

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

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

目标是最小化:

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

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

思路

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

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-01 20:58
 * update_at: 2026-10-01 20:58
 */
// brute.cpp:小数据暴力解,枚举最后一段起点,要求段和非递减。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

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

int n, type_id_input;
ll a[105], prefix_sum[105];
// dp[i][j]:前 i 个数划分完毕,最后一段为 (j+1)..i 时的最小代价。
// 要求最后一段的段和不小于前一段的段和。
ll dp[105][105];

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;
        }
    }

    // 第一段直接从 1 开始。
    for (int i = 1; i <= n; i++) {
        ll sum = prefix_sum[i];
        dp[i][0] = sum * sum;
    }

    // 枚举最后一段的起点 last,以及上一段的结尾 prev。
    for (int i = 1; i <= n; i++) {
        for (int last = 1; last < i; last++) {
            ll last_sum = prefix_sum[i] - prefix_sum[last];
            for (int prev = 0; prev < last; prev++) {
                if (dp[last][prev] == INF) {
                    continue;
                }
                ll 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);
                }
            }
        }
    }

    ll 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
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-01 20:58
 * update_at: 2026-10-01 20:58
 */
// main.cpp:线性贪心。g[i] 表示处理到 i 时,最后一段从 g[i]+1 开始最优。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const ll MOD_GEN = 1LL << 30;

int n, type_id_input;
vector<ll> prefix_sum;   // prefix_sum[i] = a[1] + ... + a[i]
vector<int> pre_pos;     // pre_pos[i]:前缀 i 的最优划分中,上一段结尾位置
vector<int> mono_q;      // 单调队列,维护候选断点的下标

// 输出 __int128 类型的整数。
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;
}

// 候选断点 idx 的关键值:key(idx) = 2*prefix_sum[idx] - prefix_sum[pre_pos[idx]]。
// 当 key(idx) <= prefix_sum[i] 时,idx 可以作为前缀 i 的上一段结尾。
ll 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);
    mono_q.assign(n + 2, 0);

    if (type_id_input == 0) {
        for (int i = 1; i <= n; i++) {
            ll a;
            cin >> a;
            prefix_sum[i] = prefix_sum[i - 1] + a;
        }
    } else {
        ll x, y, z, b1, b2;
        int m;
        cin >> x >> y >> z >> b1 >> b2 >> m;
        vector<ll> 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;
            ll l, r;
            cin >> p >> l >> r;
            for (int j = last_p + 1; j <= p; j++) {
                ll a = b[j] % (r - l + 1) + l;
                prefix_sum[j] = prefix_sum[j - 1] + a;
            }
            last_p = p;
        }
    }

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

    for (int i = 1; i <= n; i++) {
        // 队头:弹出 key 值已经 <= prefix_sum[i] 的候选,保留最后一个不满足的。
        while (head < tail && key_value(mono_q[head + 1]) <= prefix_sum[i]) {
            head++;
        }
        pre_pos[i] = mono_q[head];
        // 队尾:维护 key 值的单调递增性。
        while (head < tail && key_value(i) <= key_value(mono_q[tail])) {
            tail--;
        }
        mono_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] 后,就可以用单调队列在线维护最优断点。