[USACO05MAR] Space Elevator 太空电梯

先按允许高度排序,再用多重 01 背包判断哪些电梯高度可达。

OJ: luogu

题目 ID: P6771

难度:普及+/提高

标签:动态规划多重背包排序

日期: 2026-06-19 16:51

题意

n 种方块,每种方块有:

  • 高度 h_i
  • 数量 c_i
  • 允许到达的最高高度 a_i

如果某个方块被放到高度 x 的位置,那么它整个方块的最高点不能超过 a_i。 因此,只有当当前堆叠高度足够低时,这种方块才能放上去。

目标是堆出尽可能高的太空电梯。

这张表把题意翻成了背包模型:

原题对象 含义
一种方块 有数量上限的物品
方块高度 物品重量
允许高度 a_i 这类物品的使用上限
最高可达高度 目标答案

思路

一句话本质:按限高排序后,做多重背包可行性 DP——限高小的必须在下面,排序消除了依赖顺序的不确定性。

先看最直接的暴力:

py
import sys

data = list(map(int, sys.stdin.buffer.read().split()))
N = data[0]
blocks = []
idx = 1
for _ in range(N):
    h, a, c = data[idx], data[idx + 1], data[idx + 2]
    idx += 3
    blocks.append((a, h, c))

blocks.sort()

best = 0

def dfs(i, cur_h):
    global best
    if i == N:
        best = max(best, cur_h)
        return
    a, h, c = blocks[i]
    dfs(i + 1, cur_h)
    for k in range(1, c + 1):
        nh = cur_h + k * h
        if nh <= a:
            dfs(i + 1, nh)

dfs(0, 0)
print(best)

brute.py 枚举每种方块用 0ci0 \sim c_i 个,递归尝试所有组合。NN 最大 400400cic_i 最大 1010,组合数爆炸。

为什么必须先按 aia_i 排序?

如果不排序,就可能把限高小的方块放在限高大的方块之上。但题目要求方块的任何部分不能超过自己的 aia_i,限高小的方块放在上面意味着它的底部已经很高,顶部更容易超标。反之,限高小的方块放在底部,限高大的在上——限高大的 aia_i 大,容纳得下。

所以先处理 aia_i 小的方块,它们只能放在较低的位置(必须堆在更底部)。排序把依赖顺序变成了一个确定的处理序列。

排序后怎么建模?

每种方块有数量上限,是多重背包。"高度"是物品重量,目标是看哪些高度可达。设 dphdp_h 表示高度 hh 是否可达。

数量限制怎么处理而不炸复杂度?

不用二进制拆分,用 usedhused_h 数组记录到达高度 hh 时已经用了多少个当前方块。转移时,只有 usedhhi<ciused_{h - h_i} \lt c_i 才允许再放一块。这样每种方块内部只用一重循环就能跑完其全部数量,总时间复杂度 O(ciai)O(\sum c_i \cdot a_i)

转移条件是什么?

对于当前方块高度 hih_i、限高 aia_i,枚举 jjhih_iaia_i:如果 dpjhi=truedp_{j-h_i}=\text{true}usedjhi<ciused_{j-h_i} \lt c_i,则 dpj=truedp_j=\text{true}usedj=usedjhi+1used_j = used_{j-h_i}+1。处理完所有方块后,从高到低找到最大的 dph=truedp_h=\text{true}

代码

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

struct Block {
    int h, a, c;
    bool operator<(const Block& o) const {
        return a < o.a;                  // 按最大高度限制升序排列
    }
};

const int MAXH = 40005;

int N;
Block b[405];
// dp[j] 表示高度 j 是否可达。
bool dp[MAXH];
// used[j] 记录在处理当前类型方块时,达到高度 j 已经用了几个该方块。
int used[MAXH];

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

    cin >> N;
    for (int i = 0; i < N; i++) {
        cin >> b[i].h >> b[i].a >> b[i].c;
    }
    sort(b, b + N);                      // 先处理最大高度限制低的方块

    dp[0] = true;
    for (int i = 0; i < N; i++) {
        int h = b[i].h, a = b[i].a, c = b[i].c;
        fill(used, used + a + 1, 0);     // 每种方块重新计数
        // 多重背包可行性,用 used 数组限制每种的用量。
        for (int j = h; j <= a; j++) {
            if (!dp[j] && dp[j - h] && used[j - h] < c) {
                dp[j] = true;
                used[j] = used[j - h] + 1;
            }
        }
    }

    // 从最大高度向下找第一个可达高度。
    for (int j = b[N - 1].a; j >= 0; j--) {
        if (dp[j]) {
            cout << j << '\n';
            break;
        }
    }
    return 0;
}

复杂度

  • 时间复杂度:O(ciA)O(∑ c_i * A),其中 A 是最大允许高度
  • 空间复杂度:O(A)O(A)

总结

这题的关键是排序:

  • 先按 a_i 从小到大处理
  • 再把“能不能放”转成“当前高度是否足够低”

一旦状态写成“高度是否可达”,问题就变成了一个很标准的多重背包可达性 DP。

一图流解析

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

一图流解析