形式化题目

给定序列 x1,x2,…,xnx_1, x_2, \ldots, x_n 与函数 g(u)=au2+bu+cg(u) = a u^2 + b u + c,其中 a<0a < 0。记前缀和

Si=x1+x2+⋯+xi,S0=0.S_i = x_1 + x_2 + \cdots + x_i, \qquad S_0 = 0 .

选择一组下标 0=p0<p1<⋯<pk=n0 = p_0 < p_1 < \cdots < p_k = n,把序列切成 kk 个连续段 (pt−1,pt](p_{t-1}, p_t],使

∑t=1kg(Spt−Spt−1)\sum_{t=1}^{k} g\big(S_{p_t} - S_{p_{t-1}}\big)

最大,输出这个最大值。

约束:1⩽n⩽1061 \leqslant n \leqslant 10^6,−5⩽a⩽−1-5 \leqslant a \leqslant -1,∣b∣,∣c∣⩽107|b|, |c| \leqslant 10^7,1⩽xi⩽1001 \leqslant x_i \leqslant 100,故 Sn⩽108S_n \leqslant 10^8。

正解

思路

第一步:写出划分 DP。

"同一支特别行动队编号连续"这句话把问题完全定死了:任何编队方案就是序列的一个划分,一支队伍对应一段连续区间。于是设 f[i]f[i] 为前 ii 个士兵的最大修正战斗力和,f[0]=0f[0] = 0,枚举最后一支队伍的起点 jj:

f[i]=max⁡0⩽j<i{f[j]+g(Si−Sj)}.(1)f[i] = \max_{0 \leqslant j < i} \Big\{ f[j] + g(S_i - S_j) \Big\} . \tag{1}

答案就是 f[n]f[n],且 f[0]=0f[0] = 0 对应"一个人不编队"这个唯一合法起点。任何合法方案都能被"最后一次切在哪里"唯一描述,所以 (1) 式不重不漏。注意 ff 可以取到负数(例如 a=−1,b=−3,c=0a=-1,b=-3,c=0 时 g(1)=−4g(1)=-4),所以不能把初值当癈断下界。

以样例 n=4, a=−1, b=10, c=−20, x=[2,2,3,4]n=4,\ a=-1,\ b=10,\ c=-20,\ x=[2,2,3,4] 为例,前缀和为 S=[0,2,4,7,11]S = [0,2,4,7,11],把每个候选 jj 的取值都算出来(✅ 是取到最大值的 jj):

ii SiS_i 候选 jj(单元格为 f[j]+g(Si−Sj)f[j]+g(S_i-S_j)) f[i]f[i] 最优 jj
1 2 j=0j=0: -4✅ -4 0
2 4 j=0j=0: 4✅ / j=1j=1: -8 4 0
3 7 j=0j=0: 1 / j=1j=1: 1 / j=2j=2: 5✅ 5 2
4 11 j=0j=0: -31 / j=1j=1: -15 / j=2j=2: 5 / j=3j=3: 9✅ 9 3

读表可以看出 f[4]=9f[4] = 9 与样例一致,最优的切点是 j=3j=3:把最后一名士兵单独编一队(g(4)=4g(4)=4),前三人的最优值是 f[3]=5f[3]=5——而 f[3]f[3] 又切在 j=2j=2 处,对应"{1,2},{3},{4}\{1,2\},\{3\},\{4\}"三支队伍。同时注意每行的候选值随 jj 先升后降,最优 jj 随 ii 单调右移(0→0→2→30 \to 0 \to 2 \to 3),这正是后面优化的线索。

第二步:瓶颈在哪里。 (1) 式状态数 O(n)O(n),每个状态要扫 O(n)O(n) 个候选 jj,总时间 O(n2)O(n^2)。n=106n = 10^6 时约 5×10115 \times 10^{11} 次转移,即使 C++ 也远远超时。瓶颈是"每个 ii 都要把所有 jj 扫一遍"。

第三步:把转移线性化。 展开 gg:

f[i]=max⁡j{f[j]+a(Si−Sj)2+b(Si−Sj)+c}=max⁡j{f[j]+aSj2−bSj⏟只与 j 有关  −  2aSi⋅Sj}  +  aSi2+bSi+c⏟只与 i 有关. \begin{aligned} f[i] &= \max_j \big\{ f[j] + a(S_i - S_j)^2 + b(S_i - S_j) + c \big\} \\ &= \max_j \Big\{ \underbrace{f[j] + aS_j^2 - bS_j}_{\text{只与 } j \text{ 有关}} \;-\; 2aS_i \cdot S_j \Big\} \;+\; \underbrace{aS_i^2 + bS_i + c}_{\text{只与 } i \text{ 有关}} . \end{aligned}

最后那个括号与 jj 无关,可以直接提到 max⁡\max 外面。含 jj 的部分里,只有 −2aSi⋅Sj-2aS_i \cdot S_j 把 ii 和 jj 乘在一起。记

Yj=f[j]+aSj2−bSj,ui=−2aSi,Y_j = f[j] + aS_j^2 - bS_j, \qquad u_i = -2aS_i,

得到等价的转移:

f[i]=max⁡0⩽j<i{Yj+ui⋅Sj}+aSi2+bSi+c.(2)f[i] = \max_{0 \leqslant j < i} \Big\{ Y_j + u_i \cdot S_j \Big\} + aS_i^2 + bS_i + c . \tag{2}

第四步:看成"直线族的上包络"。 每个候选 jj 对应一条以 uu 为自变量的直线

lj(u)=Sj⋅u+Yj(斜率 Sj, 截距 Yj),l_j(u) = S_j \cdot u + Y_j \qquad (\text{斜率 } S_j,\ \text{截距 } Y_j),(2)

第五步:双单调性让上包络退化成单调队列。 两条单调性在本数据范围内同时成立:

  • 新直线的斜率单调增:xi⩾1x_i \geqslant 1 使 SjS_j 随 jj 严格递增。上包络上直线的斜率从左到右递增,所以新直线只会接在包络的右端;
  • 查询点单调增:a<0a < 0 且 SiS_i 递增 ⇒ui=−2aSi\Rightarrow u_i = -2aS_i 严格递增。查询点只会向右走,最优直线在包络上也只会越来越靠右。

于是双端队列(单调队列)就能维护整条上包络:

  1. 查询:设队头为 h0h_0、次队头为 h1h_1。若在 uiu_i 处 h1h_1 已经不劣于 h0h_0,由于后续要用的查询点只会更大,h0h_0 不可能再赢,直接弹出;反复执行直到队头严格最优;
  2. 插入:新直线 ii 入队前,检查队尾两条直线 p,qp, q。设 xpqx_{pq} 是 p,qp,q 的交点横坐标(xpq=Yq−YpSq−Spx_{pq} = \frac{Y_q-Y_p}{S_q-S_p})。上包络要求交点位置递增:xpq<xqix_{pq} < x_{qi}。若 xpq⩾xqix_{pq} \geqslant x_{qi},说明 qq 还没等到超过 pp,就已经被 ii 超过,它的领先区间为空,不再出现在包络上,弹出;反复执行后把 ii 接到队尾;
  3. 判定 xpq⩾xqix_{pq} \geqslant x_{qi} 时用整数交叉相乘代替除法(两边分母 Sq−Sp>0S_q-S_p>0、Si−Sq>0S_i-S_q>0 均正,不等号不翻转):
    Yq−YpSq−Sp⩾Yi−YqSi−Sq  ⟺  (Yq−Yp)(Si−Sq)⩾(Yi−Yq)(Sq−Sp).\frac{Y_q - Y_p}{S_q - S_p} \geqslant \frac{Y_i - Y_q}{S_i - S_q} \iff (Y_q - Y_p)(S_i - S_q) \geqslant (Y_i - Y_q)(S_q - S_p).
    代码里的 slope_pq >= slope_qi 正是左边这个不等式(取等号时三线共点,丢中间那条不影响答案);整数运算在 Python 中没有精度问题。

下图把样例的单调队列执行过程逐步列出来(ki=2aSi=−uik_i = 2aS_i = -u_i,即代码里取负后的写法):

ii 查询用 ki=2aSik_i=2aS_i 查询时弹出队头 队头(最优 jj) 插入前弹出队尾 入队后上包络(各直线的 (Sj,Yj)(S_j, Y_j))
1 -4 无 j=0j=0(f=0f=0) 无 (0,0) → (2,-28)
2 -8 无 j=0j=0(f=0f=0) j=1j=1 (0,0) → (4,-52)
3 -14 j=0j=0 j=2j=2(f=4f=4) 无 (4,-52) → (7,-114)
4 -22 j=2j=2 j=3j=3(f=5f=5) 无 (7,-114) → (11,-222)

表中括号里的两个数是直线的参数 (Sj,Yj)(S_j, Y_j),即斜率与截距。可以看到 i=2i=2 时直线 j=1j=1 刚入队就被 j=2j=2 顶掉(队尾弹出),i=3i=3 时队头 j=0j=0 因为查询点变大而失效(队头弹出)——两种弹出合起来保证每条直线最多进出队各一次,主循环是摊还 O(1)O(1) 的。

代码

max_power() 就是上面第四、五步的实现:hull 存直线编号,第一个 while 是查询前弹队头,第二个 while 是插入前弹队尾,y[i] 就是截距 YiY_i,k = 2*a*s 是 −ui-u_i(两边同乘 −1-1 后把 max⁡lj(u)\max l_j(u) 写成 max⁡(Yj−kSj)\max (Y_j - k S_j))。solve() 只负责读入、算前缀和、输出;pre、f、y 用 array('q') 存放(n=106n = 10^6 时把峰值内存压到 128 MB 以内),读入用的临时表在求完前缀和后立即释放。

Python 版:

python
#!/usr/bin/env python3
# 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-09-30 21:53
# update_at: 2026-09-30 22:05

import sys
from array import array
from collections import deque
from itertools import accumulate


def max_power(a: int, b: int, c: int, pre: array) -> int:
    """前 n 个士兵编队的最大修正战斗力和(斜率优化,O(n))。

    设 f[i] 为前 i 人的答案,最后一队是 (j, i],则
        f[i] = max_j { f[j] + g(S[i] - S[j]) },  g(u) = a·u² + b·u + c。
    展开 g,把只与 j 有关的项收成 Y[j] = f[j] + a·S[j]² - b·S[j]:
        f[i] = max_j { Y[j] + u[i]·S[j] } + a·S[i]² + b·S[i] + c,  u[i] = -2a·S[i]。
    每个候选切点 j 于是是一条直线 l_j(u) = S[j]·u + Y[j](斜率 S[j]、截距 Y[j]),
    转移变成"在直线族的上包络上查询 u = u[i] 处的最大值"。
    a < 0 ⇒ 查询点 u[i] 随 i 递增;x[i] ≥ 1 ⇒ 直线斜率 S[j] 随 j 递增。
    新直线只接在包络右端、查询点又只向右走,两条单调 ⇒ 单调队列:
    队头弹出已被超过的直线,队尾弹出被新直线压得不再可能是最优的直线;
    每条直线进出队各一次,摊还 O(1)。
    """
    n = len(pre) - 1
    f = array('q', [0]) * (n + 1)  # f[i]:前 i 人的最优值,f[0] = 0
    y = array('q', [0]) * (n + 1)  # 直线截距 Y[i] = f[i] + a·S[i]² - b·S[i]
    hull = deque([0])              # 上包络上的直线编号,斜率 S[j] 随入队递增

    for i in range(1, n + 1):
        s = pre[i]
        k = 2 * a * s  # k = -u[i]:两边同乘 -1,把 max l_j(u) 写成 max(Y[j] - k·S[j])
        while len(hull) >= 2:
            head, second = hull[0], hull[1]
            if y[head] - k * pre[head] <= y[second] - k * pre[second]:
                hull.popleft()  # 次队头已不比队头差,u[i] 只会更大,队头此后再也用不到
            else:
                break
        j = hull[0]  # 最优直线:最后一段的左边界
        f[i] = f[j] + a * (s - pre[j]) ** 2 + b * (s - pre[j]) + c
        y[i] = f[i] + a * s * s - b * s
        while len(hull) >= 2:
            p, q = hull[-2], hull[-1]
            # 判据即交点位置 x[p][q] ≥ x[q][i]:直线 q 的领先区间为空,可丢;交叉相乘免去除法
            slope_pq = (y[q] - y[p]) * (s - pre[q])
            slope_qi = (y[i] - y[q]) * (pre[q] - pre[p])
            if slope_pq <= slope_qi:
                hull.pop()  # q 被直线 p 与 i 夹在包络之下,不再出现在上包络上
            else:
                break
        hull.append(i)
    return f[n]


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n = next(data)  # 士兵数
    a, b, c = next(data), next(data), next(data)  # 经验公式系数
    pre = array('q', accumulate((next(data) for _ in range(n)), initial=0))  # S[0..n]
    print(max_power(a, b, c, pre))


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

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-05 12:53
 * update_at: 2026-10-05 12:53
 */

// main.cpp:一本通 5.6 练习 3「特别行动队」(APIO 2010 特别行动队)。
// 编号必须连续,所以任何编队方案就是把序列切成若干连续段的划分,
// 设 f[i] 为前 i 名士兵的最大修正战斗力和,枚举最后一支队伍的起点 j:
//     f[i] = max_{j<i} { f[j] + g(S_i - S_j) },g(u) = a*u^2 + b*u + c。
// 展开 g 后,只与 j 有关的项收成截距 Y_j = f[j] + a*S_j^2 - b*S_j,
// 与 i 有关的项提到 max 外面,转移变成「在直线族 l_j(u) = S_j*u + Y_j 的
// 上包络上查询 u_i = -2*a*S_i」:
//     f[i] = max_j { S_j*u_i + Y_j } + a*S_i^2 + b*S_i + c。
// x_i >= 1 使直线斜率 S_j 随 j 递增,a < 0 使查询点 u_i 随 i 递增,
// 两条单调同向,于是用单调队列维护上包络即可:队头弹出已被超过的直线,
// 队尾弹出被新直线压得不再可能最优的直线,摊还 O(1),总时间 O(n)。

#include <cstdio>

typedef long long ll;
typedef __int128 lll;

const int MAXN = 1000005; // n 的上限

ll n;           // 士兵总数
ll a, b, c;     // 经验公式系数,满足 a < 0
ll pre[MAXN];   // pre[i] = 前 i 名士兵的初始战斗力之和 S_i,S_0 = 0
ll f[MAXN];     // f[i] = 前 i 名士兵编队后能得到的最大修正战斗力和
ll y[MAXN];     // y[i] = 直线 l_i 的截距 Y_i = f[i] + a*S_i^2 - b*S_i
int hull[MAXN]; // 上包络上的直线编号队列,斜率 S_j 随入队递增
int head;       // 队列头下标,head 指向当前最优直线
int tail;       // 队列尾下标

// 快速读入,n 最大 10^6,避免 cin / scanf 的额外开销
ll read_ll() {
    int ch = getchar();
    while (ch != '-' && (ch < '0' || ch > '9')) {
        ch = getchar();
    }
    ll sign = 1;
    if (ch == '-') {
        sign = -1;
        ch = getchar();
    }
    ll value = 0;
    while (ch >= '0' && ch <= '9') {
        value = value * 10 + (ch - '0');
        ch = getchar();
    }
    return sign * value;
}

// 两个数相乘,用 __int128 承接结果:叉积可达 10^25,超出 long long
lll product(lll x, lll y) {
    return x * y;
}

// 直线 l_j 在查询点 u 处的取值 l_j(u) = S_j * u + Y_j
lll value_at(int j, ll u) {
    return y[j] + product(u, pre[j]);
}

// 直线 q 是否被直线 p 与 i 夹在上包络之下(q 的领先区间为空,可以丢掉)。
// 等价于两交点位置比较 x[p][q] >= x[q][i],即
//     (Y_q - Y_p) * (S_i - S_q) <= (Y_i - Y_q) * (S_q - S_p),
// 交叉相乘代替除法,既避免浮点误差也不需要判分母符号。
// 取等号时三线共点,丢掉中间那条不影响答案。
bool is_hidden(int p, int q, int i) {
    return product(y[q] - y[p], pre[i] - pre[q])
           <= product(y[i] - y[q], pre[q] - pre[p]);
}

void solve() {
    n = read_ll();
    a = read_ll();
    b = read_ll();
    c = read_ll();
    for (int i = 1; i <= n; i++) {
        pre[i] = pre[i - 1] + read_ll(); // 前缀和
    }

    head = 0;
    tail = 0;
    hull[0] = 0; // 只有 j = 0 可选,f[0] = 0、S_0 = 0、Y_0 = 0

    for (int i = 1; i <= n; i++) {
        ll s = pre[i];
        ll u = -2 * a * s; // 查询点 u_i = -2*a*S_i,随 i 严格递增

        // 插入前先查询:次队头在 u_i 处已不劣于队头,说明队头的领先区间已经过去,
        // 而后续查询点只会更大,队头此后永远用不到,直接弹出
        while (tail > head) {
            int first = hull[head];
            int second = hull[head + 1];
            if (value_at(first, u) <= value_at(second, u)) {
                head++;
            } else {
                break;
            }
        }
        int j = hull[head]; // 最优切点:最后一支队伍的起点是 j+1

        ll len = s - pre[j]; // 最后一支队伍的初始战斗力
        f[i] = f[j] + a * len * len + b * len + c;
        y[i] = f[i] + a * s * s - b * s; // 新直线的截距

        // 新直线斜率 S_i 最大,只会接在包络右端;入队前检查队尾两条直线,
        // 若 q 还没超过 p 就被 i 超过,q 的领先区间为空,弹出
        while (tail > head) {
            int p = hull[tail - 1];
            int q = hull[tail];
            if (is_hidden(p, q, i)) {
                tail--;
            } else {
                break;
            }
        }
        tail++;
        hull[tail] = i;
    }

    printf("%lld\n", f[n]);
}

int main() {
    solve();
    return 0;
}

复杂度

  • 时间复杂度:读入、前缀和、主循环都是 O(n)O(n),主循环的两次 while 摊还 O(1)O(1)(每条直线最多入队一次、出队一次),总计 O(n)O(n),与标准 C++ 斜率优化解法同阶。
  • 空间复杂度:O(n)O(n),即前缀和、ff、YY 三张表和队列;n=106n = 10^6 时实测峰值约 74 MB。
  • 数值范围:ff 可达 101310^{13} 量级,叉积比较约 102410^{24},Python int 任意精度不会溢出(C++ 需要 __int128)。
  • Python 侧风险:本地 10 组真实数据 n=106n = 10^6 用时约 0.9 s,若 OJ 解释器更慢存在 TLE 风险(按任务约定允许)。

总结

  • 建模:编号连续 ⇒ 问题就是"把序列切成若干连续段、段费用由段和决定"的划分 DP,f[i]=max⁡j{f[j]+g(Si−Sj)}f[i] = \max_j \{ f[j] + g(S_i - S_j) \}。
  • 线性化:展开 gg 后,只与 jj 有关的项收成截距 Yj=f[j]+aSj2−bSjY_j = f[j] + aS_j^2 - bS_j,与 ii 有关的项提到 max⁡\max 外,转移变成"在直线族 lj(u)=Sju+Yjl_j(u) = S_j u + Y_j 的上包络上查询 ui=−2aSiu_i = -2aS_i"。
  • 上包络:只有上包络上的直线可能成为最大值,被压在下方的直线可以永久丢弃。
  • 双单调:a<0a < 0 使查询点 uiu_i 单调增,xi⩾1x_i \geqslant 1 使直线斜率 SjS_j 单调增,两者同向 ⇒ 双端队列摊还 O(1)O(1) 维护上包络,总复杂度 O(n)O(n)。
  • 易错点:交点判据必须用交叉相乘(浮点除法会在大数下失准);插入用 <= 弹出可顺带处理三线共点;SS 严格递增是"分母恒正、不等号不翻转"的前提,若 xix_i 可能为 0 需要改写。

图示解析

从输入到答案的完整推导链路,每一步只做一件事:

flowchart LR
  A["输入 n, a, b, c, x"] --> B["前缀和 S_i"]
  B --> C["划分 DP<br/>f[i] = max_j f[j]+g(S_i-S_j)"]
  C --> D["展开 g<br/>切点 j 变成直线 l_j(u)=S_j·u+Y_j"]
  D --> E["在 u_i = -2aS_i 处查询<br/>直线族的上包络"]
  E --> F["双单调性<br/>u_i 递增、直线斜率 S_j 递增"]
  F --> G["单调队列维护上包络<br/>队头弹出 + 队尾弹出"]
  G --> H["f[n] 即答案"]

观察重点:C→D 是分水岭,它把"枚举切点"换成"在直线族的上包络上查询";D→E→F 说明为什么可以只用双端队列而不是三分查找或李超树——查询点与直线斜率同向单调,最优直线只向右移,队头可以直接弹掉;G 的两个 while 分别对应"旧直线过期"与"新直线让旧直线失去包络地位"。