用单调队列维护每个前缀的最优上一段结尾,贪心取尽量靠后的可行断点。
OJ: luogu
题目 ID: P5665
难度:提高+/省选-
标签:动态规划贪心单调队列前缀和
日期: 2026-07-06 08:46
题意
给定正整数序列 a[1..n],要把它划分成若干个连续段,要求每一段的段和非递减。设各段段和为:
s1 <= s2 <= ... <= sk目标是最小化:
s1^2 + s2^2 + ... + sk^2type=0 时直接给出所有 a_i;type=1 时按题面给出的递推和区间参数生成数据。
思路
小数据可以做动态规划:枚举最后一段从哪里开始,再检查它的段和是否不小于上一段。
// 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,如果最后一段越短,它的平方贡献越小;因此在满足“上一段段和不超过最后一段段和”的前提下,我们希望最后一段尽量短,也就是上一段结尾尽量靠后。
令:
c[i] = a[1] + a[2] + ... + a[i]
g[i] = 前缀 i 的最优上一段结尾最后一段是 (g[i]+1)..i,段和为:
c[i] - c[g[i]]为了让它接在 g[i] 的最优划分后面,需要满足:
c[g[i]] - c[g[g[i]]] <= c[i] - c[g[i]]移项得到:
2*c[g[i]] - c[g[g[i]]] <= c[i]也就是说,每个候选断点 t 有一个关键值:
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 输出。
代码
// 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;
}复杂度
每个位置最多进出单调队列一次,时间复杂度
空间复杂度为
总结
本题表面是划分 DP,核心优化来自正数序列的贪心性质:最后一段在可行时应尽量短。把可行条件整理成 key(t) <= c[i] 后,就可以用单调队列在线维护最优断点。