按测试点分档推进:枚举组合 → 固定 m 的四个特判 → 值域开桶的超集计数 → 从高到低逐位贪心,全范围 O(30n)。
OJ: luogu
题目 ID: P17829
难度:普及
目录
形式化题目
给定
也就是所选
解法路线
题面的测试点不是按难度均匀铺开的,而是卡在三种不同的条件上:
| 层次 | 适用约束 | 对应测试点 | 做法 | 复杂度 | 相对前层的推进 |
|---|---|---|---|---|---|
| 层 1:暴力 | 枚举全部 |
基线 | |||
| 层 2:固定 |
按 |
不再枚举 |
|||
| 层 3:值域开桶 | 桶计数加超集和,取满足 |
换视角:不选数,只判断“掩码 |
|||
| 层 4:逐位贪心 | 全范围 | 从第 29 位到第 0 位维护必修掩码,候选数够 |
取消值域限制: |
||
| 分支:性质 A | 每个 |
这类数的按位与等于最小值,取最大的 |
不在主线上,靠数值结构把问题化成排序 |
- 层 2 的四个
之间互不包含。 的 和 的 彼此没有依赖,它们只是“先知道会选几个,再直接造选法”这个想法的四个实例。 - 层 3 是主线上唯一一次换视角:从“挑哪
个数”变成“某个掩码能不能被至少 个数同时包含”。这个判据与 的具体取值无关,代价是必须把整个值域开成桶。 - 性质 A 是分支,不是通向正解的一步。它对数值结构的依赖太强,换一组数据立刻失效。保留它的理由有两条:测试点 13 正是给这个观察留的档位;它也顺带说明按位与在高位上只看公共前缀。
性质 B(
暴力解法
适用范围
思路
按定义枚举:把“选
brute.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-31 18:55
* update_at: 2026-10-31 19:01
*/
// brute.cpp:枚举所有 m 元下标组合,取按位与的最大值。只适用于 n 很小的情况。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 25;
int n, m;
int a[maxn];
int best;
// 从下标 dep 开始决定选不选,已经选了 picked 个,当前与值为 cur
void dfs(int dep, int picked, int cur) {
if (picked == m) {
best = max(best, cur);
return;
}
if (dep > n) return;
if (n - dep + 1 < m - picked) return; // 剩下的数不够选了
// 选第 dep 个
dfs(dep + 1, picked + 1, picked == 0 ? a[dep] : (cur & a[dep]));
// 不选第 dep 个
dfs(dep + 1, picked, cur);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; ++i) cin >> a[i];
best = 0;
dfs(1, 0, 0);
cout << best << '\n';
return 0;
}复杂度与瓶颈
组合数是
真正的瓶颈不在常数,而在“把
子任务解法:固定 m 的四个取值
适用范围
思路
前三个都很直接:
:只选一个数,答案就是最大值,扫一遍。 :所有数都要选,答案是全体按位与,扫一遍。 : 时数对只有约 个,直接枚举。
先算全体按位与 base。第 base 里是 1,说明没有数在这一位为 0,丢谁都不影响;base 里为 0 的位,说明至少有一个数在这一位为 0,而丢掉一个数只能补回“恰好只有一个数为 0”的那些位。
于是统计每一位上为 0 的数的个数:
- 0 个:这一位已经在
base里,不用管; - 1 个:把那个在这位上为 0 的数丢掉,这一位就能补回 1;
- 2 个及以上:丢掉一个数后仍有数为 0,这一位救不回来。
把“只能靠丢掉第 extra[i],丢掉第 base | extra[i],枚举
以 base = 7 & 5 & 3 = 1;第 1 位只有 5 是 0,丢 5 得到
代码
/**
* 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-31 19:50
* update_at: 2026-10-31 19:50
*/
// main2.cpp:把 m 钉成四个具体取值来拿部分分,覆盖 m = 1、m = 2、m = n、m = n - 1。
// 其余 m 这一份代码不给答案,直接输出 0。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 5;
int n, m;
int a[maxn]; // a_i < 2^30,int 足够
int extra[maxn]; // extra[i]:只丢掉第 i 个数时能额外补回来的位
// m = 1:任选一个数,答案就是最大值
int solve_m1() {
int best = 0;
for (int i = 1; i <= n; ++i) best = max(best, a[i]);
return best;
}
// m = n:全部数都要选,答案就是全体按位与
int solve_full() {
int cur = a[1];
for (int i = 2; i <= n; ++i) cur &= a[i];
return cur;
}
// m = 2:直接枚举所有数对
int solve_m2() {
int best = 0;
for (int i = 1; i <= n; ++i)
for (int j = i + 1; j <= n; ++j) best = max(best, a[i] & a[j]);
return best;
}
// m = n - 1:恰好丢掉一个数。先算全体与 base,再逐位看这一位是不是只被一个数否决
int solve_drop_one() {
int base = a[1];
for (int i = 2; i <= n; ++i) base &= a[i];
for (int i = 1; i <= n; ++i) extra[i] = 0;
// 第 b 位上为 0 的数如果恰好只有第 who 个,那么丢掉 who 就能把这一位补回来
for (int b = 0; b < 30; ++b) {
int zero_cnt = 0, who = 0;
for (int i = 1; i <= n; ++i)
if (((a[i] >> b) & 1) == 0) {
++zero_cnt;
who = i;
}
if (zero_cnt == 1) extra[who] |= 1 << b;
}
// 丢掉第 i 个数得到的答案就是 base | extra[i],取最大
int best = 0;
for (int i = 1; i <= n; ++i) best = max(best, base | extra[i]);
return best;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; ++i) cin >> a[i];
int ans = 0;
if (m == 1) ans = solve_m1();
else if (m == n) ans = solve_full();
else if (m == 2) ans = solve_m2();
else if (m == n - 1) ans = solve_drop_one();
// 其余 m 在二分点上没有对应的特判做法,这一支不做处理
cout << ans << '\n';
return 0;
}这份代码只处理这四个
复杂度与瓶颈
这一层完全靠
特例分支:全部是一串 1(性质 A)
适用范围
每个
思路
设
于是题目变成“从
代码
/**
* 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-31 19:50
* update_at: 2026-10-31 19:50
*/
// main3.cpp:只对特殊性质 A 成立(所有 a_i 都是 2^k - 1 的形状)。
// 这种数末尾是一串连续的 1,两个这样的数按位与等于较小的那个,
// 所以选出 m 个数后的答案就是这 m 个数里的最小值,取最大的 m 个数即可。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 5;
int n, m;
int a[maxn]; // a_i < 2^30,int 足够
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; ++i) cin >> a[i];
// 升序排序后,第 m 大的数就在下标 n - m + 1 上
sort(a + 1, a + n + 1);
cout << a[n - m + 1] << '\n';
return 0;
}这份代码只在性质 A 的数据上正确,一般数据会算错。
复杂度与瓶颈
时间 nth_element 可以做到
局限很明显:结论依赖“每个数都是
子任务解法:值域能开桶就换一个刻画
适用范围
思路
层 2 留下的是“对任意
对一个掩码
充分性(
- 把满足
的 个下标选出来。 - 在
为 1 的每一位上,这 个数都是 1。 - 所以它们的按位与在这一位也是 1,即按位与包含
的全部位。
必要性(能选出按位与包含
- 设某个选法的按位与包含
的全部位。 - 那么被选中的每个数都含
的每一位,即每个被选中的数都是 的超集。 - 这样的下标至少有
个,所以 。
有了这个等价,答案就是满足
- 最优选法的按位与
本身就满足 ,所以 在可行集合里; - 反过来,任何可行的
都对应某个按位与 的选法,而 ,所以 不会超过答案; - 两边的界合起来,可行集合的最大值就是答案。
剩下的事只有一件:把所有
- 桶
记值 出现的次数。 - 按位做 20 轮:对每一位
,把所有第 位为 1 的桶加到“去掉这一位”的桶上,即 ,这里 的第 位为 0。 - 每一轮相当于允许这一位自由翻转,20 轮之后
把 的所有超掩码都统计过一遍,正是 。
拿样例 #1 验算(
| 掩码 |
包含 |
能选出 |
|
|---|---|---|---|
| 14 | 15 | 1 | 否 |
| 13 | 15 | 1 | 否 |
| 12 | 15、12、12 | 3 | 是 |
12 可行而 14、13 不可行,所以答案是 12,与逐位贪心的结论一致。
代码
/**
* 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-31 19:50
* update_at: 2026-10-31 19:58
*/
// main4.cpp:值域可枚举时的解法,要求 max a_i < 2^20。
// 换成另一种看问题的角度:一个掩码 X 能被选出来,当且仅当至少有 m 个数包含 X 的全部位。
// 于是先把每个值的出现次数放进桶,再用超集和(SOS DP)求出
// super_cnt[X] = 包含 X 全部位的数的个数,最后取 super_cnt[X] >= m 的最大 X。
#include <bits/stdc++.h>
using namespace std;
const int BITS = 20; // 本层要求 max a_i < 2^20
const int SIZE = 1 << BITS; // 桶的大小 2^20,int 数组约占 4MB
int n, m;
int super_cnt[SIZE]; // super_cnt[X]:包含掩码 X 全部位的数的个数,初值是该值的出现次数
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
int x;
cin >> x;
++super_cnt[x];
}
// 超集和:按位逐个把「多含一位」的计数并进来
for (int b = 0; b < BITS; ++b)
for (int x = 0; x < SIZE; ++x)
if (((x >> b) & 1) == 0) super_cnt[x] += super_cnt[x ^ (1 << b)];
// 从大到小找第一个能被选出 m 个数的掩码
for (int x = SIZE - 1; x >= 0; --x)
if (super_cnt[x] >= m) {
cout << x << '\n';
return 0;
}
cout << 0 << '\n'; // 只有 m > n 才会走到这里,题面保证不会出现
return 0;
}复杂度与瓶颈
时间由三部分组成:装桶 int,约 4 MB。
瓶颈在桶的大小。int,也就是 4 GB,测试点 20 过不去。想过它,必须把“枚举整个值域”换成“只看这
正解
关键观察
第
所以只要存在
思路
把层 3 的“可行掩码”换成按位决策,并且只统计这
用 ans 记已经确定必须为 1 的位。判断第 want = ans | (1 << b);再数一数有多少个数同时含 ans 的全部位和第 (a[i] & want) == want 的个数。个数不少于 ans 换成 want,否则这一位放弃。
正确性靠按位归纳:
- 归纳假设:处理完高于
的位后, ans的每一位都出现在最优答案里。 - 候选数不少于
个时,这 个数同时含 ans和第位,能凑出包含 want的按位与。而任何按位与含ans、第位却是 0 的选法,在高于 的位上只能与 ans完全一致:更高的位如果还能再多含一位,那一步就已经把它保留进ans了。高位相同而第位为 0,它的值就严格小于保留这一位得到的结果。所以这一位必须保留。 - 候选数不足
个时,不存在 个数同时含 ans和第位,最优答案的第 位只能是 0,放弃它不损失任何东西。 - 每一步都不破坏最优性,循环结束时
ans就是答案。
以样例 #1 为例,
| 位 |
试探掩码 | 同时含这些位的数 | 个数 | 决策 |
|---|---|---|---|---|
| 逐位试探 | 无(所有数都在低四位以内) | 0 | 放弃,ans 保持 0 |
|
| 3 | 8 | 15、12、12 | 3 | ans = 8 |
| 2 | 12 | 15、12、12 | 3 | ans = 12 |
| 1 | 14 | 15 | 1 | ans 仍为 12 |
| 0 | 13 | 15 | 1 |
第 3、2 位定下来之后,第 1、0 位无论怎么挑都凑不出两个同时含这些位的数,所以答案是
还有一个看起来很像、实际错误的直觉:直接选数值最大的
代码
/**
* 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-31 18:55
* update_at: 2026-10-31 19:01
*/
// main.cpp:从高位到低位逐位贪心。
// 已经确定的位必须被选中的每个数都含有,再看当前位能不能也凑够 m 个数。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 5;
int n, m;
int a[maxn];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; ++i) cin >> a[i];
int ans = 0; // 已确定必须为 1 的位构成的掩码
for (int b = 29; b >= 0; --b) {
int want = ans | (1 << b); // 假设第 b 位也取 1
int cnt = 0; // 有多少个数同时含有 want 的全部位
for (int i = 1; i <= n; ++i) {
if ((a[i] & want) == want) ++cnt;
}
if (cnt >= m) ans = want; // 够 m 个,第 b 位可以取 1
}
cout << ans << '\n';
return 0;
}复杂度
- 时间:
个位,每一位扫一遍 个数, 。 时约 次与运算,本地实测 36 至 49 ms。 - 空间:存下输入的
个数, ,实测 1.3 至 1.6 MB。 - 位掩码用
int就够:题面限定,最高只探到第 29 位, 1 << 29在 32 位int内不会溢出。如果值域扩到以上,这里要改成 1ll << b,否则会碰到有符号整数的溢出。
总结
这题的入手点是把“选
按位与的每一位只关心“够不够
分档的意义也在这里。固定
再往外推一步:目标是最大化一个按位组合出来的值、并且每一位的贡献按指数递减时,都可以从高位到低位贪心,判定一位时只保留满足已定高位的那批候选,看这一位还能不能凑够所需的个数。
相关 rbook 文章
做这题用到的知识点在 rbook 算法书 上有专门章节:
| 知识点 | 对应文章 | 用在哪 |
|---|---|---|
| 位运算基础 | 《二进制与位运算》 | 第 (a[i] & want) == want 就是在检查若干位 |
| 子集枚举 | 《子集枚举》 | 值域能开桶那一层,按掩码统计“包含它全部位的数有多少个” |
| 贪心与正确性证明 | 《贪心算法》 | 从高位到低位“能保就保”,靠 |