[HNOI2008] 玩具装箱

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

把连续分段费用写成前缀和形式后,展开平方得到标准斜率优化 DP,用单调队列维护下凸壳。

OJ: luogu

题目 ID: P3195

难度:提高+/省选-

标签:动态规划前缀和斜率优化凸包优化

日期: 2026-06-21 07:42

题意

要把 n 件玩具按顺序分成若干连续段,每段放进一个容器。

若一段长度是 x,则费用是:

(x - L)^2

要求总费用最小。

思路

先看朴素 DP:

cpp
#include <bits/stdc++.h>
using namespace std;

const long long INF = (1LL << 62);
const int MAXN = 5005;

int n;
long long L;
long long c[MAXN], sum[MAXN];
long long dp[MAXN];

long long X(int i) {
    return sum[i] + i;
}

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

    // brute.cpp:小数据朴素 DP。
    // 枚举最后一个箱子从哪里开始。
    cin >> n >> L;
    for (int i = 1; i <= n; i++) {
        cin >> c[i];
        sum[i] = sum[i - 1] + c[i];
    }

    dp[0] = 0;
    for (int i = 1; i <= n; i++) {
        dp[i] = INF;
        for (int j = 0; j < i; j++) {
            long long t = X(i) - X(j) - L - 1;
            dp[i] = min(dp[i], dp[j] + t * t);
        }
    }

    cout << dp[n] << '\n';
    return 0;
}

设前缀和 sum[i] = C_1 + ... + C_i

定义:

X(i) = sum[i] + i

如果最后一段是 j+1..i,那么这段容器长度减去 L 后,正好是:

X(i) - X(j) - L - 1

于是:

dp[i] = min(dp[j] + (X(i) - X(j) - L - 1)^2)

展开平方:

dp[i] = X(i)^2 + min(dp[j] + (X(j)+L+1)^2 - 2X(i)(X(j)+L+1))

这已经是标准斜率优化形式。

又因为 X(i) 单调递增,所以可以用单调队列维护候选决策点。

DP 转移方程

核心状态:

X(i)=sum[i]+idp[i] 为前 i 件最小费用

核心转移:

dp[i]=min(dp[j]+(X(i)-X(j)-L-1)^2)

答案收束:

dp[n]

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 50005;

int n;
long long L;
long long c[MAXN], sum[MAXN];
long long dp[MAXN];
int q[MAXN];

long long X(int i) {
    return sum[i] + i;
}

long long S(int i) {
    return X(i) + L + 1;
}

long long Y(int i) {
    long long t = S(i);
    return dp[i] + t * t;
}

bool better_front(int a, int b, long long k) {
    return (__int128) (Y(b) - Y(a)) <= (__int128) k * (S(b) - S(a));
}

bool bad(int a, int b, int cidx) {
    return (__int128) (Y(b) - Y(a)) * (S(cidx) - S(b))
         >= (__int128) (Y(cidx) - Y(b)) * (S(b) - S(a));
}

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

    cin >> n >> L;
    for (int i = 1; i <= n; i++) {
        cin >> c[i];
        sum[i] = sum[i - 1] + c[i];
    }

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

    for (int i = 1; i <= n; i++) {
        long long k = 2LL * X(i);
        while (head < tail && better_front(q[head], q[head + 1], k)) {
            head++;
        }

        int j = q[head];
        long long t = X(i) - X(j) - L - 1;
        dp[i] = dp[j] + t * t;

        while (head < tail && bad(q[tail - 1], q[tail], i)) {
            tail--;
        }
        q[++tail] = i;
    }

    cout << dp[n] << '\n';
    return 0;
}

复杂度

时间复杂度 O(n)O(n),空间复杂度 O(n)O(n)

总结

这题是非常典型的“分段平方代价 DP -> 展开平方 -> 斜率优化”。

记住 X(i) = sum[i] + i 这一步,后面的形式就会很自然。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析