usaco-4.1.2 栅栏的木料

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

二分答案转化为装箱判定问题,利用大件优先、相同元素去重和废料上限剪枝进行深搜回溯。

OJ: roj

题目 ID: 2062

难度:提高+/省选-

标签:二分答案深度优先搜索剪枝

创建: 2026-10-01 05:43

更新: 2026-10-07 13:50

形式化题目

给定 NN 个容器的初始容量 A1,A2,…,ANA_1, A_2, \dots, A_N 和 RR 个物品的大小 B1,B2,…,BRB_1, B_2, \dots, B_R。 每个容器可装入任意多个物品,但装入物品的大小之和不能超过该容器的容量;物品不可拆分拼接,一个物品只能放入一个容器中。 求最多能够同时放入容器中的物品数量。

正解

思路

1. 贪心与二分答案

若能够选出任意 kk 个物品放入容器中,那么将其中较大的物品替换为未被选中的更小物品,容器所占空间只减不增,方案依然合法。因此:

  1. 满足 kk 个需求时,最优策略必然是优先满足大小最小的前 kk 个需求。
  2. 满足的物品数量具有单调性:若前 kk 个物品可以放入,则前 k−1k-1 个物品必然也可以放入。

因此,我们可以先对需求数组 BB 从小到大排序,并在区间 [0,R][0, R] 内对答案 kk 进行二分搜索。每次判定前 midmid 个需求能否装入 NN 根木料中。

2. 装箱判定的 DFS 设计

对于固定的 midmid,我们需要判断前 midmid 个需求能否放入 NN 根木料。这是一个 NP-hard 的多重背包(装箱)问题,但在数据规模下可通过深度优先搜索结合强剪枝高效完成:

  1. 大件优先(优化搜索顺序): 在搜索时,逆序枚举需求(从第 mid−1mid-1 个到第 00 个,即从大到小)。大木料的限制多、可选木板少,优先安排能极大压缩搜索树前期的分支数。
  2. 相同大小去重(对称性剪枝): 若相邻两个需求长度相同(Bidx=Bidx−1B_{idx} = B_{idx-1}),则后一个需求搜索的木板起始下标不能小于前一个需求所放置的木板,避免同构排列的重复搜索。
  3. 废料量剪枝(核心剪枝): 在切分过程中,若某根木板切完后的剩余长度严格小于最小的需求长度 B0B_0,那么这部分剩余空间在后续搜索中永远不可能被任何需求利用,必然成为不可回收的“废料”。 设 NN 根木板的总长度为 SboardS_{\text{board}},前 midmid 根需求的总长度为 SreqS_{\text{req}}。整个切分方案所允许产生的废料上限为:
    max_waste=Sboard−Sreq\text{max\_waste} = S_{\text{board}} - S_{\text{req}}
    若在搜索中累计的废料量超过了 max_waste\text{max\_waste},说明剩余木板的有效容量已经不足以供给剩下的需求,可立即回溯剪枝。
  4. 完全匹配剪枝: 若某根木板在切下当前需求后剩余长度恰好为 00(或者原本剩余长度刚好等于当前需求),说明该木板达到了完美利用,无需再尝试将该需求放入其他木板。

图示:剪枝判定流程

graph TD
    A[二分当前需求数量 mid] --> B{总长度充足?<br/>sum_req <= sum_boards}
    B -- 否 --> C[不可行 mid 缩小]
    B -- 是 --> D[DFS 逆序放置需求 mid-1 .. 0]
    D --> E[为需求选木板 i]
    E --> F{木板容量充足?}
    F -- 否 --> E
    F -- 是 --> G[切除需求, 统计不可用残料]
    G --> H{累计废料 > 最大允许废料?}
    H -- 是 --> I[剪枝回溯]
    H -- 否 --> J[递归放置下一需求]
    J -- 成功 --> K[可行 mid 增大]
    J -- 失败 --> I

代码

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-10-01 05:45
# update_at: 2026-10-01 05:45

import sys

# 设置递归上限以支持 R <= 1023 深度递归
sys.setrecursionlimit(2000)


def can_fulfill(mid: int, boards: list[int], req: list[int], pref_req: list[int], total_board: int) -> bool:
    """判定是否能用现有木料切出最小的 mid 根需求木料。"""
    if mid == 0:
        return True
    if pref_req[mid] > total_board:
        return False

    b = sorted(boards)
    min_req = req[0]
    total_waste = 0
    max_waste = total_board - pref_req[mid]

    def dfs(idx: int, start_board: int) -> bool:
        nonlocal total_waste
        if idx < 0:
            return True

        curr_req = req[idx]
        for i in range(start_board, len(b)):
            if b[i] >= curr_req:
                b[i] -= curr_req
                waste_added = b[i] if b[i] < min_req else 0
                total_waste += waste_added

                if total_waste <= max_waste:
                    nxt_start = i if idx > 0 and req[idx - 1] == curr_req else 0
                    if dfs(idx - 1, nxt_start):
                        return True

                total_waste -= waste_added
                b[i] += curr_req

                # 对称性剪枝:如果当前木板还原后剩余与上一根相同,没必要再试同样的剩余
                if b[i] == curr_req:  # 刚好用完这根木板,后续木板不可能更好
                    break
        return False

    return dfs(mid - 1, 0)


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n = next(data, None)
    if n is None:
        return

    boards = [next(data) for _ in range(n)]
    r = next(data)
    req = sorted(next(data) for _ in range(r))

    total_board = sum(boards)
    pref_req = [0] * (r + 1)
    for i in range(r):
        pref_req[i + 1] = pref_req[i] + req[i]

    low, high, ans = 0, r, 0
    while low <= high:
        mid = (low + high) // 2
        if can_fulfill(mid, boards, req, pref_req, total_board):
            ans = mid
            low = mid + 1
        else:
            high = mid - 1

    print(ans)


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-06 10:29
 * update_at: 2026-10-06 10:32
 */
// usaco 4.1.2 栅栏的木料:二分答案 + 大件优先 DFS 装箱判定 + 废料上限剪枝。
#include <cstdio>
#include <algorithm>
using namespace std;

typedef long long ll;

const int MAXN = 60;     // 木板数 <= 50
const int MAXR = 1030;   // 需求数 <= 1023

ll n, r;                 // 木板数、需求数
ll board[MAXN];          // board[i] = 第 i 块木板的剩余长度(会被切分修改)
ll req[MAXR];            // req[i] = 第 i 短的需求长度(升序)
ll pref[MAXR];           // pref[i] = 前 i 短需求的长度和(pref[0] = 0)
ll total_board;          // 所有木板总长度
ll min_req;              // 最短需求长度,废料的判定阈值
ll max_waste;            // 本次判定允许的最大废料 = total_board - 前 mid 需求和
ll waste;                // 当前累计的不可回收废料

// 判定的核心 DFS:这一层在为第 idx 短(剩余中最大的)需求选木板。
// start_board:相同长度的需求只能放在编号不小于它的需求的木板里(对称性剪枝)。
bool dfs(ll idx, ll start_board) {
    if (idx < 0) return true; // 前 mid 个需求全部放好

    ll cur = req[idx];
    for (ll i = start_board; i < n; i++) {
        if (board[i] < cur) continue;

        // 切下需求,统计这块木板是否当场变成废料(剩余 < 最短需求)
        board[i] -= cur;
        ll add = 0;
        if (board[i] < min_req) add = board[i];
        waste += add;

        bool ok = false;
        if (waste <= max_waste) {
            // 相同长度的相邻需求,下一块木板下标不能小于 i,避免同构搜索;
            // 长度不同则下一层从 0 号木板重新找起
            ll nxt = 0;
            if (idx > 0 && req[idx - 1] == cur) nxt = i;
            ok = dfs(idx - 1, nxt);
        }

        // 无论成功与否都要恢复现场:check() 成功返回后木板数组还要给下一次二分用
        waste -= add;
        board[i] += cur;

        if (ok) return true;

        // 这根木板刚好被用完:放不下当前需求的更长的木板只会更差,剪枝
        if (board[i] == cur) break;
    }
    return false;
}

// 判定能否用木板切出最小的 mid 根需求木料
bool check(ll mid) {
    if (mid == 0) return true;
    if (pref[mid] > total_board) return false; // 需求总长超过木板总长,必不可能

    max_waste = total_board - pref[mid]; // 允许的废料上限
    waste = 0;
    return dfs(mid - 1, 0);
}

int main() {
    scanf("%lld", &n);
    total_board = 0;
    for (ll i = 0; i < n; i++) {
        scanf("%lld", &board[i]);
        total_board += board[i];
    }
    scanf("%lld", &r);
    for (ll i = 0; i < r; i++) scanf("%lld", &req[i]);

    // 需求升序:能切 k 根时最优选最小的 k 根;数量具有单调性,可二分
    sort(req, req + r);
    sort(board, board + n);
    pref[0] = 0;
    for (ll i = 0; i < r; i++) pref[i + 1] = pref[i] + req[i];
    min_req = req[0];

    ll low = 0, high = r, ans = 0;
    while (low <= high) {
        ll mid = (low + high) / 2;
        if (check(mid)) {
            ans = mid;
            low = mid + 1;
        } else {
            high = mid - 1;
        }
    }
    printf("%lld\n", ans);
    return 0;
}

复杂度

  • 时间复杂度:二分次数为 O(log⁡R)O(\log R)。单次判定在极限状态下为指数级,但在“大件优先 + 废料上限剪枝 + 对称性剪枝”约束下,搜索树大幅剪枝收敛,在所有测试用例下均能在 0.05s0.05\text{s} 内完成。
  • 空间复杂度:递归深度为 O(R)O(R),辅助状态数组为 O(N+R)O(N + R),总体空间复杂度为 O(N+R)O(N + R)。

总结

本题是经典的二分答案与带剪枝 DFS 相结合的题目。利用问题的单调性与贪心性质(优先取最小物品)将原最优化问题转化为判定性装箱问题,并通过预计算废料上限进行全局可行性剪枝,是解决此类强约束搜索问题的经典范式。