题解:洛谷 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

【核心思想】

  1. 问题分析:给定 \(N\) 个比赛门票价格和预算 \(M\),求总票价不超过 \(M\) 的观赛方案数。\(N \leq 40\)\(M \leq 10^{18}\),直接枚举 \(2^{40}\) 个子集会超时。这是一个折半搜索(Meet in the Middle)问题,关键在于将集合分成两半,分别处理后再合并结果。

  2. 算法选择

    • 折半搜索:将 \(N\) 个数分成两半(前 \(N/2\) 个和后 \(N/2\) 个),分别枚举子集和
    • DFS生成子集和:对每一半使用DFS枚举所有子集和,时间复杂度 \(O(2^{N/2})\)
    • 排序 + 二分查找:对前半部分的子集和排序,对于后半部分的每个子集和,用二分查找统计前半部分有多少个子集和满足条件
    • 剪枝优化:排序后,当剩余数字的最小值已经超过剩余预算时,直接返回
  3. 关键步骤

    • 预处理:筛选出价格 \(\leq M\) 的有效数字,并排序
    • 分治:将数字分成前后两半
    • DFS枚举
      • 对前半部分DFS,生成所有子集和存入 sa
      • 对后半部分DFS,生成所有子集和存入 sb
    • 排序:对 sasb 进行排序
    • 合并统计
      • 对于 sb 中的每个子集和 \(s\),在 sa 中二分查找满足 \(sa[i] \leq M - s\) 的个数
      • 累加所有合法方案数
    • 输出答案:注意空集也算一种方案
  4. 时间/空间复杂度

    • 时间复杂度:\(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})\),存储子集和数组
  5. 折半搜索的核心思想

    • 分治策略:将大问题分成两个规模相近的子问题,降低指数级复杂度
    • \(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
posted @ 2026-06-25 09:15  团爸讲算法  阅读(24)  评论(0)    收藏  举报