给定 N 个物品和背包容量 W,求力气范围内能搬动的最大重量,利用折半搜索降低时间复杂度,并用去重控制状态空间。
OJ: roj
题目 ID: 3066
难度:提高+/省选-
标签:折半搜索二分查找DFS
创建: 2026-10-09 12:46
更新: 2026-10-09 12:46
目录
形式化题目
给定
数据范围:
(注意:部分数据中 会达到 48 而非题面声称的 46)
正解
思路
这是一个典型的 01 背包问题,但是背包的容量
引入 折半搜索 (Meet in the middle) 技巧:
- 预处理:先将所有重量大于
的物品剔除掉,并将剩下的物品按重量从大到小排序。从大到小排序使得在搜索时能更早触发重量超过 的剪枝,显著降低实际的搜索树规模。 - 第一半搜索:将前
个物品作为第一组。使用 DFS 枚举出这些物品组合出的所有合法重量和(不超过 ),存入状态数组 weights中。 - 整理状态:将
weights数组进行排序并去重。这一步非常关键,因为相同重量组合出现次数极多,去重能大幅削减第二半查找的工作量。 - 第二半搜索:将后
个物品作为第二组。同样使用 DFS 枚举出这些物品的合法重量组合和 。对于每一个 ,在 weights数组中通过二分查找找到一个不大于的最大值 ,然后尝试用 来更新全局最大答案。
代码
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-09 09:00
# update_at: 2026-10-09 12:35
import sys
from bisect import bisect_right
def half_sums(items: list[int], limit: int) -> list[int]:
"""枚举 items 的全部合法子集和(超过 limit 的分支就地剪掉),返回排序去重后的列表。"""
sums = [0]
for weight in items:
sums += [s + weight for s in sums if s + weight <= limit] # 拿 / 不拿,规模翻倍
return sorted(set(sums)) # 排序 + 去重,供二分查表
def best_paired(items: list[int], table: list[int], limit: int) -> int:
"""枚举 items 的每种取法(和 S),在 table 里二分找 ≤ limit - S 的最大值,取总和最大者。"""
best = 0
stack = [(0, 0)]
while stack: # 显式栈迭代,避免深递归
idx, total = stack.pop()
if idx == len(items):
pos = bisect_right(table, limit - total) # table 已排序且含 0
if pos and total + table[pos - 1] > best:
best = total + table[pos - 1]
continue
stack.append((idx + 1, total)) # 不拿第 idx 件
if total + items[idx] <= limit:
stack.append((idx + 1, total + items[idx])) # 拿第 idx 件
return best
def solve() -> None:
"""读入 W、N 与 N 个重量,用折半搜索求一次能搬动的最大重量。"""
data = iter(map(int, sys.stdin.buffer.read().split()))
limit = next(data, None)
if limit is None: # 无输入:静默退出
return
n = next(data, None)
if n is None: # 只有 W 没有 N:同样静默退出
return
# 礼物不足 N 件时补 0(题面规定 G_i ≥ 1,补 0 不影响答案),避免 StopIteration
raw = [next(data, 0) for _ in range(n)]
usable = sorted((g for g in raw if g <= limit), reverse=True) # 超重礼物剔除;降序先搜大件
if not usable: # 全部超重(或 N = 0)
print(0)
return
half = len(usable) // 2
table = half_sums(usable[:half], limit)
print(best_paired(usable[half:], table, limit))
if __name__ == '__main__':
solve()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-09 09:00
* update_at: 2026-10-09 12:20
*/
// roj 3066《送礼物》:N 个礼物,一次搬重量和不超过 W 的任意多件,求最大搬运重量。
// 容量 W 可达 2^31-1,容量维 DP 不可行;N 可达 48,朴素 2^N 也不可行。
// 故用折半搜索(meet in the middle):前半枚举出全部合法子集和并排序,
// 后半枚举时在表里二分找「不超过 W - S」的最大值。
// 题面三步判据(关键词 / 括号内算法名 / 标题)全空 ⇒ 未指定实现手段。
#include <iostream>
#include <algorithm>
#include <functional>
using namespace std;
typedef long long ll;
const int MAXN = 50; // 题面 N ≤ 46,实测最大 48,留余量
const int MAX_HALF = 1 << 24; // 前半最多 24 件,子集和最多 2^24 个
ll W; // 力气上限(可到 2^31-1,必须 ll)
int n; // 过滤超重礼物后剩下的礼物数
ll gift[MAXN]; // 过滤后的礼物重量,按降序排列
// 前半枚举出的所有合法子集和。
// 每个状态都由「和不超过 W」的递归产生,故取值 ≤ W ≤ 2^31-1,收窄成 int 不会溢出;
// 用 int 而不是 ll,是为了让最坏情况的 2^24 个状态只占 64MB(ll 要 128MB+,会超过 128MB 限额)。
int half_sum[MAX_HALF + 10];
int half_cnt;
ll answer; // 当前能达到的最大搬运重量
// 枚举 gift[idx..half_end-1] 每件选或不选,叶子处把当前和收进 half_sum
void dfs_first(int idx, int half_end, ll current_sum) {
if (idx == half_end) {
half_sum[half_cnt++] = current_sum;
return;
}
dfs_first(idx + 1, half_end, current_sum); // 不拿第 idx 件
if (current_sum + gift[idx] <= W) { // 拿第 idx 件
dfs_first(idx + 1, half_end, current_sum + gift[idx]);
}
}
// 枚举 gift[idx..end_idx-1] 的每种取法,用当前和 S 去表里配一个最大的另一半
void dfs_second(int idx, int end_idx, ll current_sum) {
if (idx == end_idx) {
ll remain = W - current_sum; // 还能再塞多少
int lo = 0, hi = half_cnt - 1, pos = 0;
while (lo <= hi) { // 找最后一个 ≤ remain 的下标
int mid = lo + (hi - lo) / 2;
if (half_sum[mid] <= remain) {
pos = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
ll total = current_sum + half_sum[pos];
if (total > answer) {
answer = total;
}
return;
}
dfs_second(idx + 1, end_idx, current_sum); // 不拿第 idx 件
if (current_sum + gift[idx] <= W) { // 拿第 idx 件
dfs_second(idx + 1, end_idx, current_sum + gift[idx]);
}
}
// 读入并按题意过滤:单个重量就超过 W 的礼物永远搬不动,直接剔除。
// 返回 false 表示连 W / N 都没读到(无输入),此时不输出任何内容。
bool read_input() {
int total;
if (!(cin >> W >> total)) {
return false;
}
n = 0;
for (int i = 0; i < total; i++) {
ll value;
if (!(cin >> value)) { // 礼物少于声明的 N 件:按已读到的件数处理
break;
}
if (value <= W) {
gift[n++] = value;
}
}
return true;
}
void solve() {
if (!read_input()) { // 无输入:静默退出
return;
}
if (n == 0) { // 全部超重(或 N = 0):一件都搬不动
cout << 0 << "\n";
return;
}
sort(gift, gift + n, greater<ll>()); // 降序:先搜大件,更快触发「超过 W」的剪枝
int half = n / 2;
dfs_first(0, half, 0);
sort(half_sum, half_sum + half_cnt); // 排序后二分,顺带去重压缩表长
int unique_cnt = 1;
for (int i = 1; i < half_cnt; i++) {
if (half_sum[i] != half_sum[i - 1]) {
half_sum[unique_cnt++] = half_sum[i];
}
}
half_cnt = unique_cnt;
dfs_second(half, n, 0);
cout << answer << "\n";
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
solve();
return 0;
}复杂度
- 时间复杂度:第一部分搜索时间为
;对状态数组排序去重时间为 ;第二部分搜索时间为 ,每次配合一个二分查找,总时间为 。总体时间复杂度约为 ,在 时处于合理范围内。 - 空间复杂度:最坏情况下存储第一半状态占用
。若表内使用 int存储,理论最大可达千万级别(几十 MB),符合,能安全通过题目的 128 MB 限制。
总结
1. 算法与数据类型细节(★★★ int/ll 的两个层次)
本题在数据类型的选择上存在两个截然不同的层次:
- 算术必须使用
ll(正确性要求):在 DFS 中if (current_sum + gift[idx] <= W)这类【先加后判】的操作中,如果全盘采用int,会直接导致有符号整数溢出。全int版被 UBSan 在 5/9 个测试点(gift1/4/5/6/8)抓到实打实的 signed integer overflow(例如gift1会错算成 2147483012,应为 2144708006)。 - 状态表元素使用
int(规范与承重选择):虽然运算需使用ll,但在状态表中存储的重量和保证了,将其收窄为 int绝不会截断。这不仅是规范的选择,更是承重的选择(见下一条)。需要注意的是该安全性附带条件:如果超规格构造的反例,表元素用 int就会发生真正的截断。
2. 空间是承重结论(★★★ 空间不是风格偏好)
对于极限合成数据(48 件,W = INT_MAX,前半
- 若状态表用
ll(8 Byte),RSS 峰值达到 130.3 MB,会直接导致 MLE (超过 128 MB 限额)。 - 若状态表用
int(4 Byte),RSS 峰值仅 66.3 MB,能安全通过。 此外,Python 侧的 peak RSS 会高达 223.3 MB,导致超出内存限制(按语言惯例予以豁免)。
3. 数据缺陷与特殊情况分析
G_i = 0存在(★★ 题面与数据不符,同 T33):题面声明物品重量是“正整数”,但实际数据比题面声明更宽: gift2中有 1 个 0,gift4中有 2 个 0。这属于输入合法性层面的缺陷,虽然不影响答案,但需引起注意。- .out 是 CRLF 且反事实证明(★★ T32 数据缺陷):9 个测试点的
.in均为纯LF换行,但.out全部为 Windows 的CRLF(即行尾多了\r),导致标准字节对比全部失败(输出70\nvs 期望70\r\n)。反事实证明:在不动内容的前提下,将 9 个.out文件由 CRLF 替换为 LF 后,程序的字节级测试 9/9 完美通过,并且还原后 SHA256 也与原始文件一致。这说明问题纯粹出在官方数据的行尾格式上。 gift0穷举分析(★★):gift0(, ) 的 128 种子集中,合法的有 54 个。去除超重物品 150 后只剩 种子集。最大可达重量为 70,它有两个取证: [29, 23, 17, 1]和[13, 23, 17, 16, 1]。通过穷举确认,71、72、73 的重量全部为不可达。
4. V1 反事实与对照验证表
| 变体 | 失败点数 | 失败点 | 说明 |
|---|---|---|---|
| 「≤W」→「<W」 | 0 | — | ⚠ 真实数据无法分辨(见 V8 盲区测试) |
| 二分 → 线性扫描 | 3 | 3,7,8 | 全部 TIMEOUT,无一个算错 ⇒ 只影响速度 |
| 切分 N/2 → N/4 | 2 | 7,8 | TIMEOUT,只影响速度 |
| 不过滤超重礼物 | 0 | — | 输出全同,只影响速度 |
表 int → ll |
0 | — | 输出全同,但 66.3 MB → 130.3 MB(爆 128MB 限额) |
整体 ll → int |
5 | 1,4,5,6,8 | 答案错 + UBSan 5 点报 signed overflow |
| 去掉降序排序 | 0 | — | 只影响速度/内存 |
| 去掉排序+去重 | 7 | 1,2,4,5,6,7,8 | 直接错值(如 gift1 → 2121529554),说明去重是承重操作 |
去掉容量剪枝(ll 表) |
0 | — | 只变慢(gift8 1.3→2.0s,130 MB) |
| 表尾捷径 + 乐观上界(提速尝试) | 0 | — | gift8 1.30→1.30s,无收益,未采纳 |
★ 特别注意:「表
int→ll」虽然失败 0 个点,但导致了致命的 MLE(隐藏风险);而去重是承重逻辑,不仅影响速度还直接影响正确性。
5. 逐点测试数据表
全局 INT_MAX),出现在 gift1。不存在
| 点 | W | N | 答案 | cpp 时间 | cpp RSS | py 时间 | py RSS | |||
|---|---|---|---|---|---|---|---|---|---|---|
gift0 |
73 | 7 | 1 / 150 | 1 | 0 | 70 | 0.010s | 8.4MB | 0.02s | 8.5MB |
gift1 |
2147483647 | 26 | 12 / 2013009336 | 0 | 0 | 2144708006 | 0.009s | 8.4MB | 0.02s | 8.5MB |
gift2 |
66086780 | 40 | 0 / 2000000000 | 10 | 1 | 66086779 | 0.010s | 8.4MB | 0.03s | 9.7MB |
gift3 |
67108260 | 41 | 2 / 736312400 | 2 | 0 | 65157980 | 0.037s | 8.4MB | 0.53s | 66.5MB |
gift4 |
2147473513 | 42 | 0 / 1699980000 | 0 | 2 | 2147473498 | 0.028s | 8.4MB | 0.79s | 8.6MB |
gift5 |
2106441797 | 45 | 7 / 2125738860 | 1 | 0 | 2106441791 | 0.034s | 8.4MB | 1.44s | 8.6MB |
gift6 |
2133930318 | 46 | 1 / 1771761751 | 0 | 0 | 2133930256 | 0.058s | 8.4MB | 3.00s | 8.6MB |
gift7 |
2147483000 | 42 | 1912453 / 276340294 | 0 | 0 | 2147476138 | 0.191s | 8.4MB | 1.42s | 86.3MB |
gift8 |
2147483000 | 48 | 2092873 / 283313527 | 0 | 0 | 2147476137 | 1.299s | 15.1MB | 11.35s | 223.3MB |
6. 系统验证与 T9 规范
- V2 无硬编码:验证了提取出来的常数集(如
0、1、2等)与各个点题解输出交集为空,没有特判情况。 - V4 规范通过:9 个点全符合每行一个数据、Token 数量精确无误。
- T9 五项:
- 类型转换/溢出:已详细分析,由于先加后判必须全链路
long long,只有存储时为了空间才转换为int。 - 整除取模 / 浮点格式化:本题不涉及。
- 排序字典序:严格降序用于提早剪枝,去重依赖此机制运行正常。
- 类型转换/溢出:已详细分析,由于先加后判必须全链路