题解:洛谷 P4799 世界冰球锦标赛
【题目来源】
洛谷:P4799 [CEOI 2015] 世界冰球锦标赛(Day2) - 洛谷
【题目描述】
译自 CEOI2015 Day2 T1「****Ice Hockey World Championship 」
今年的世界冰球锦标赛在捷克举行。Bobek 已经抵达布拉格,他不是任何团队的粉丝,也没有时间观念。他只是单纯的想去看几场比赛。如果他有足够的钱,他会去看所有的比赛。不幸的是,他的财产十分有限,他决定把所有财产都用来买门票。
给出 Bobek 的预算和每场比赛的票价,试求:如果总票价不超过预算,他有多少种观赛方案。如果存在以其中一种方案观看某场比赛而另一种方案不观看,则认为这两种方案不同。
【输入】
第一行,两个正整数 \(N\) 和 \(M(1 \leq N \leq 40,1 \leq M \leq 10^{18})\),表示比赛的个数和 Bobek 那家徒四壁的财产。
第二行,\(N\) 个以空格分隔的正整数,均不超过 \(10^{16}\),代表每场比赛门票的价格。
【输出】
输出一行,表示方案的个数。由于 \(N\) 十分大,注意:答案 \(\le 2^{40}\)。
【输入样例】
5 1000
100 1500 500 500 1000
【输出样例】
8
【核心思想】
-
问题分析:给定 \(N\) 个比赛门票价格和预算 \(M\),求总票价不超过 \(M\) 的观赛方案数。\(N \leq 40\),\(M \leq 10^{18}\),直接枚举 \(2^{40}\) 个子集会超时。这是一个折半搜索(Meet in the Middle)问题,关键在于将集合分成两半,分别处理后再合并结果。
-
算法选择:
- 折半搜索:将 \(N\) 个数分成两半(前 \(N/2\) 个和后 \(N/2\) 个),分别枚举子集和
- DFS生成子集和:对每一半使用DFS枚举所有子集和,时间复杂度 \(O(2^{N/2})\)
- 排序 + 二分查找:对前半部分的子集和排序,对于后半部分的每个子集和,用二分查找统计前半部分有多少个子集和满足条件
- 剪枝优化:排序后,当剩余数字的最小值已经超过剩余预算时,直接返回
-
关键步骤:
- 预处理:筛选出价格 \(\leq M\) 的有效数字,并排序
- 分治:将数字分成前后两半
- DFS枚举:
- 对前半部分DFS,生成所有子集和存入
sa - 对后半部分DFS,生成所有子集和存入
sb
- 对前半部分DFS,生成所有子集和存入
- 排序:对
sa和sb进行排序 - 合并统计:
- 对于
sb中的每个子集和 \(s\),在sa中二分查找满足 \(sa[i] \leq M - s\) 的个数 - 累加所有合法方案数
- 对于
- 输出答案:注意空集也算一种方案
-
时间/空间复杂度:
- 时间复杂度:\(O(2^{N/2} \times N)\),枚举每半部分的子集需要 \(O(2^{N/2})\),合并时用二分查找 \(O(2^{N/2} \times \log 2^{N/2}) = O(2^{N/2} \times N)\)
- 空间复杂度:\(O(2^{N/2})\),存储子集和数组
-
折半搜索的核心思想:
- 分治策略:将大问题分成两个规模相近的子问题,降低指数级复杂度
- 从 \(O(2^N)\) 到 \(O(2^{N/2})\):将 \(2^{40}\) 降为 \(2 \times 2^{20}\),大幅降低计算量
- 空间换时间:存储中间结果,通过排序和二分查找快速合并
- 适用场景:\(N\) 在 \(30\) 到 \(40\) 之间,直接枚举超时,但折半后可行的问题
- 适用于子集枚举、组合计数等问题
【解题思路】


【算法标签】
普及+ #折半搜索
【代码详解】
// 40分版本
#include <bits/stdc++.h>
using namespace std;
#define int long long // 将int定义为long long类型,避免大数溢出
const int N = 45; // 数组最大容量
int a[N], n, m, cur, ans = 1; // a: 存储有效数字,n: 输入数字总数,m: 目标值,cur: 有效数字计数,ans: 方案数(初始为1表示空集)
// 深度优先搜索,计算所有子集和不超过m的方案数
void dfs(int step, int x)
{
// 剪枝:如果剩余可用数字的最小值已经超过当前剩余容量x
if (x < a[step])
{
return;
}
// 从step位置开始遍历所有可能的数字
for (int i = step; i <= cur; i++)
{
// 如果当前数字可以加入子集
if (x >= a[i])
{
ans++; // 增加方案数(选择当前数字)
dfs(i + 1, x - a[i]); // 递归搜索下一个数字
}
else
{
return; // 由于数组已排序,后续数字更大,直接返回
}
}
}
signed main() // 使用signed main()替代int main(),因为int被重定义为long long
{
cin >> n >> m; // 读入数字总数n和目标值m
// 筛选有效数字(不大于m的数字)
for (int i = 1; i <= n; i++)
{
int x;
cin >> x;
if (x > m) continue; // 跳过大于m的数字
a[++cur] = x; // 存储有效数字
}
// 对有效数字进行升序排序,便于剪枝
sort(a + 1, a + cur + 1);
// 开始深度优先搜索
dfs(1, m);
// 输出所有子集和不超过m的方案数
cout << ans;
return 0;
}
// 满分版本
#include <bits/stdc++.h>
using namespace std;
#define int long long // 将int定义为long long类型,避免大数溢出
const int N = 45; // 数组最大容量
int a[N], n, m, cur, ans = 1; // a: 存储有效数字,n: 输入数字总数,m: 目标值,cur: 有效数字计数,ans: 方案数(初始为1表示空集)
vector<int> sa, sb; // sa: 前半部分数字的子集和,sb: 后半部分数字的子集和
// 深度优先搜索,生成子集和
void dfs(int pos, int x, int type)
{
// 剪枝:如果剩余可用数字的最小值已经超过当前剩余容量
if (m - x < a[pos])
{
return;
}
// 确定搜索范围
int end = cur; // 默认搜索到末尾
if (type == 0) // 如果是搜索前半部分
{
end = cur / 2; // 只搜索到中间位置
}
// 从pos位置开始遍历数字
for (int i = pos; i <= end; i++)
{
// 如果当前数字可以加入子集
if (m - x >= a[i])
{
// 将新子集和存入对应向量
if (type == 0)
{
sa.push_back(x + a[i]);
}
else
{
sb.push_back(x + a[i]);
}
// 递归搜索下一个数字
dfs(i + 1, x + a[i], type);
}
else
{
return; // 由于数组已排序,后续数字更大,直接返回
}
}
}
signed main() // 使用signed main()替代int main(),因为int被重定义为long long
{
cin >> n >> m; // 读入数字总数n和目标值m
// 筛选有效数字(不大于m的数字)
for (int i = 1; i <= n; i++)
{
int x;
cin >> x;
if (x > m) continue; // 跳过大于m的数字
a[++cur] = x; // 存储有效数字
}
// 对有效数字进行升序排序,便于剪枝
sort(a + 1, a + cur + 1);
// 分治:将数字分成前后两部分,分别生成子集和
dfs(1, 0, 0); // 搜索前半部分数字
dfs(cur / 2 + 1, 0, 1); // 搜索后半部分数字
// 对子集和数组进行排序,便于使用二分查找
sort(sa.begin(), sa.end());
sort(sb.begin(), sb.end());
// 合并两部分结果:计算sa[i] + sb[j] <= m的方案数
for (int i = 0; i < sb.size(); i++)
{
// 对于sb中的每个和,找到sa中满足sa[j] <= m - sb[i]的最大索引
int x = m - sb[i];
int pos = upper_bound(sa.begin(), sa.end(), x) - sa.begin();
ans += pos; // 增加满足条件的方案数
}
// 加上单一部分的方案数(只使用前半部分或后半部分数字)
ans += sa.size(); // 只使用前半部分数字的方案
ans += sb.size(); // 只使用后半部分数字的方案
// 输出所有子集和不超过m的方案数
cout << ans;
return 0;
}
【运行结果】
5 1000
100 1500 500 500 1000
8
浙公网安备 33010602011771号