蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:折半搜索优化子集枚举(Meet in the Middle)解析

进入十一月,青少儿编程赛事进入密集备考期,几大标志性比赛的时间节点都摆在这里:

  • 第十八届蓝桥杯软件赛 (C/C++ 程序设计、Python 程序设计均为核心科目)报名已于 2026-10-08 10:00 开启,至 2026-12-18 14:00 截止;省赛 / 全国选拔赛定于 2027 年 4 月 (4 月 10、11 日),全国总决赛 2027 年 6 月中上旬 (dasai.lanqiao.cn 通知 3004 已证实)。

  • 中国电子学会 2026 年 12 月青少年软件编程等级考试 (C/C++ 一级至十级、Python 一级至六级)报名 2026-09-23 --- 12-02 16:00 ,全国测试 12-10 19:00--21:00 ,统考 12-12 (C/C++ 8:30--10:30 / Python 14:00--15:00)(qceit.org.cn)。

  • 全国青少年信息素养大赛 2026---2027 赛季算法应用主题赛,大纲明确包含搜索、枚举与基础算法。

三个赛事共同高频出现一类题:"从若干物品里选一部分,使某指标恰好等于目标"。当物品数量偏多、纯暴力会超时,却还没到动态规划能解的程度时,折半搜索(Meet in the Middle) 就是那把打开局面的钥匙。


一、题目 / 项目目标

我们把问题包装成一道校园场景题------「校园能量币密码验证」:

有 n 枚能量币,第 i 枚面值为 a[i](正整数)。社团想验证:能否从中选出若干枚(每枚选或不选),使选出能量币的总面值恰好等于目标能量 T? 若能,共有多少种不同选法?

数据范围:1 ≤ n ≤ 40,1 ≤ a[i] ≤ 10^9,1 ≤ T ≤ 10^15。

为什么这道题值得讲? 因为 n 最大到 40,全选/不选的暴力枚举有 2^40 ≈ 1e12 种情况,绝对超时;而 a[i] 和 T 又大到没法用普通背包 DP(O(n·T) 直接爆掉)。这正是折半搜索的"黄金区间":把 40 拆成 20+20,每半只枚举 2^20 ≈ 1e6 次。

样例 :a = [2, 3, 5, 7],T = 10。

总面值恰为 10 的选法有两种:{3, 7} 和 {2, 3, 5},所以答案为 可行,方案数 = 2。


二、核心考点

  • 折半搜索(Meet in the Middle)思想:把大问题对半拆,分别暴力,再用哈希表"拼"起来。
  • 位运算枚举子集 :用 mask 的每一位表示"选 / 不选"第 i 个物品。
  • 哈希表计数:前半所有子集和 → 出现次数;后半查"互补值"是否存在、有多少。
  • 数据类型与边界:大数用 64 位整数(或语言自带大整数),避免溢出。
  • 复杂度判断:能区分"该用折半还是该用 DP",是蓝桥杯选对算法的关键。

三、解法 / 拆解

3.1 原理

  1. 把 n 枚能量币从中间 mid = n / 2 切成两半。
  2. 枚举前半 :遍历前 mid 个物品的所有 2^mid 种选 / 不选组合,算出每个组合的子集和 s,用哈希表 left[s] 记录"和为 s 的组合出现了几次"。
  3. 枚举后半 :遍历后 n - mid 个物品的 2^(n-mid) 种组合,算出子集和 s。要让总面值等于 T,前半需要补上 T - s。直接查 left[T - s] 有多少,累加到答案。

整体时间复杂度从 O(2^n) 降到 O(2^(n/2)) ,n=40 时约 2^20 ≈ 1e6 次操作,轻松通过。

3.2 C++ 实现

cpp 复制代码
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;

using ll = long long;

// 折半搜索:返回"选若干能量币使总和恰为 T"的方案数
// a: 能量币面值,T: 目标能量
ll meetInMiddle(const vector<ll>& a, ll T) {
    int n = a.size();
    int mid = n / 2;
    unordered_map<ll, ll> left;   // 前半部分所有子集和 -> 出现次数

    // 枚举前半部分 2^mid 种选法
    for (int mask = 0; mask < (1 << mid); ++mask) {
        ll s = 0;
        for (int i = 0; i < mid; ++i)
            if (mask & (1 << i)) s += a[i];
        left[s]++;
    }

    ll ans = 0;
    int r = n - mid;
    // 枚举后半部分 2^r 种选法
    for (int mask = 0; mask < (1 << r); ++mask) {
        ll s = 0;
        for (int i = 0; i < r; ++i)
            if (mask & (1 << i)) s += a[mid + i];
        // 后半和为 s,需要前半补上 T - s
        auto it = left.find(T - s);
        if (it != left.end()) ans += it->second;
    }
    return ans;
}

int main() {
    vector<ll> a = {2, 3, 5, 7};
    ll T = 10;
    ll ways = meetInMiddle(a, T);
    cout << (ways > 0 ? "可行" : "不可行")
         << ",方案数 = " << ways << "\n";   // 输出:可行,方案数 = 2
    return 0;
}

3.3 Python 实现

python 复制代码
from collections import defaultdict

def meet_in_middle(a, T):
    n = len(a)
    mid = n // 2
    left = defaultdict(int)
    # 枚举前半部分 2^mid 种选法
    for mask in range(1 << mid):
        s = 0
        for i in range(mid):
            if mask & (1 << i):
                s += a[i]
        left[s] += 1

    ans = 0
    r = n - mid
    # 枚举后半部分 2^r 种选法
    for mask in range(1 << r):
        s = 0
        for i in range(r):
            if mask & (1 << i):
                s += a[mid + i]
        ans += left.get(T - s, 0)
    return ans

a = [2, 3, 5, 7]
T = 10
ways = meet_in_middle(a, T)
print("可行" if ways > 0 else "不可行", "方案数 =", ways)  # 可行 方案数 = 2

3.4 时间与空间复杂度

  • 时间 :枚举前半 2^(n/2) + 后半 2^(n-mid),每次哈希查询 O(1),总体 O(2^(n/2))。
  • 空间 :left 哈希表最多 2^(n/2) 个键,O(2^(n/2))。
  • n=40 时约 2^20 ≈ 1e6,内存与耗时都在安全范围内,远优于纯暴力 2^40。

四、七大易错点

  1. 子集枚举的循环边界 :枚举掩码要写 mask < (1 << mid),即共 2^mid 个(含空集 0)。写成 mask <= (1 << mid) 会越界多算;写成 i <= n 会把后半漏掉。
  2. 分割点下标算错 :mid = n / 2,第二半必须从 a[mid] 开始、长度为 n - mid。常见错误是把 a[mid] 既算进前半又算进后半,或前后两半都没覆盖它。
  3. 只存"是否存在"而丢次数 :left 必须记出现次数,若只存布尔,多个子集和相同的情况会被合并成 1,方案数被严重低估。
  4. 整数溢出 :a[i] ≤ 1e9,40 个全选总和可达 4e10,远超 32 位 int。C++ 必须用 long long,Python 虽自动大整数但字典键也要能容纳负数(T - s 可能为负)。
  5. 负数键未处理 :当后半和 s > T 时,T - s 为负。Python / C++ unordered_map 都支持负数键(返回 0 或 find 失败),但若自行用数组下标就需整体偏移,否则越界。
  6. 重复面值是否去重 :题目按"第几枚能量币"区分选法,相同面值的不同币算不同方案;若题意要"按面值去重"则须改算组合而非排列------动手前先确认题意。
  7. 多测未清空字典 :若在多组数据上反复调用,必须每次重建 left,否则会累积上一轮的子集和,得到错误答案。

五、进阶

  • 双向 BFS:和折半同源,都是"从两端往中间对半扩展"------状态空间搜索时从起点与终点同时扩展,相遇即合并。
  • 迭代加深搜索(IDDFS):当解的深度未知、纯 DFS 易爆栈时,用"限制深度、逐步加深"替代盲目深搜。
  • 折半的适用边界 :它适合"每个元素二选一 / 分支有限、且 2^(n/2) 可接受"的场景;若 n 大到 50,需改用排序 + 双指针去重、或母函数 / DP。
  • 折半 vs 背包 DP :若 a[i]、T 都较小(如 T ≤ 1e4),布尔背包 O(n·T) 更直观;本题 T 高达 1e15,DP 直接不可用------判断该用哪种算法,正是蓝桥杯拉开差距的地方。
  • 输出具体方案 :left 不只记次数,也记录到达该和的 mask,合并时把前后 mask 拼回原数组下标,即可还原"选了哪几枚能量币"。

六、小结与互动

折半搜索把"指数级暴力"拦腰砍半,用哈希表在中间会师,是蓝桥杯、电子学会考级与信息素养大赛里处理"选 / 不选"大类题型的利器。记住它的适用信号:分支少、总数中等、目标值大、DP 不可行。

你还在哪道题上用过"对半拆开再合并"的思路?是双向 BFS、还是别的搜索优化?欢迎在评论区聊聊你遇到的同类题,我们一起拆解。

(本文为原创算法解析,转载请附出处。关注获取更多蓝桥杯 / 信息素养 / 电子学会考级备考干货。)


📚 免费少儿编程资料(夸克网盘领取)

以下资料来自夸克网盘分享,点击链接可直接保存;若需在 App 内打开,也可复制下方明文链接:

  1. 全国青少年信息素养大赛复赛集训题目Python&C++.docx
    https://pan.quark.cn/s/93995d3cb150
  2. 2024信息素养-智能算法应用挑战赛-复赛初中组题目7月7日.pdf
    https://pan.quark.cn/s/da97b5dbf75d
  3. Python背记手册.pdf
    https://pan.quark.cn/s/7568ae9ca92b
  4. Python课程
    https://pan.quark.cn/s/a94bf02d00c6
  5. 2024信息素养大赛图形化复赛集训题答案3-9
    https://pan.quark.cn/s/6ccab7ec3cbc
  6. 2025年03月份电子学会考级真题
    https://pan.quark.cn/s/4403c4228912
  7. 2025全国青少年信息素养大赛赛项说明
    https://pan.quark.cn/s/d9d0df4a9f29
  8. 青少儿信息素养大赛编程资料
    https://pan.quark.cn/s/4ab6bd83be8a

资料持续更新,关注获取最新分享。

相关推荐
用户956256254291 小时前
五分钟用 E2B SDK 打通沙箱:环境变量只改三行
python·api
用户95179422510291 小时前
最小循环:让 Agent 跑起来(I1)
python·agent
沉默小管1 小时前
上班鼓励器_桌宠版
python
SomeOtherTime1 小时前
Rust 面向对象介绍(AI回答)
开发语言·python·rust
duansamve1 小时前
Flask中视图函数接收请求数据和返回响应实例
python·flask
wo_jack2 小时前
C++ 入门笔记(十七):list 入门 —— 为什么 list 不能 sort?
数据结构·c++·面试·stl·list
Oneshot2 小时前
DLX(Dancing Links)算法详解:求解数独最快的算法
算法
用户298698530142 小时前
用 Python 自动化把 JavaScript 代码转换为 Word 文档
javascript·python·api
用户019027581612 小时前
AlphaFeed 标的池(universes)全解:CN_Stock / CN_ETF 与权限边界
python