[CSP-J 2026] 按位与 / and(民间数据)

GitHub跳转原题关系图返回列表

按测试点分档推进:枚举组合 → 固定 m 的四个特判 → 值域开桶的超集计数 → 从高到低逐位贪心,全范围 O(30n)。

OJ: luogu

题目 ID: P17829

难度:普及

标签:位运算贪心枚举sosdp

创建: 2026-10-31 18:55

更新: 2026-10-31 20:14

RBook 文章:
贪心算法

形式化题目

给定 nn 个非负整数 a1,a2,…,ana_1, a_2, \ldots, a_n 和正整数 m⩽nm \leqslant n。选取下标集合 S⊆{1,2,…,n}S \subseteq \{1, 2, \ldots, n\},要求 ∣S∣=m|S| = m,最大化

⋀i∈Sai,\bigwedge_{i \in S} a_i,

也就是所选 mm 个数的二进制按位与。同一个数值出现在不同下标时按不同下标分别计数,可以同时选中。约束为 1⩽m⩽n⩽2×1051 \leqslant m \leqslant n \leqslant 2 \times 10^5 和 0⩽ai<2300 \leqslant a_i < 2^{30}。下面把这样的 mm 元下标集合简称为“选法”。

解法路线

题面的测试点不是按难度均匀铺开的,而是卡在三种不同的条件上:nn 的大小、mm 的固定取值、值域与数值结构。这几档里只有一条主线,另有一档是独立分支。

层次 适用约束 对应测试点 做法 复杂度 相对前层的推进
层 1:暴力 n⩽20n \leqslant 20 1∼51 \sim 5 枚举全部 mm 元下标组合,逐个求按位与 O((nm)⋅m)O\left(\binom{n}{m}\cdot m\right) 基线
层 2:固定 mm m=1,2,n,n−1m = 1, 2, n, n-1 6∼126 \sim 12 按 mm 的形状直接构造最优选法 O(n)O(n)、O(n2)O(n^2)、O(30n)O(30n) 不再枚举 mm 元组,用“mm 等于多少”换掉组合枚举
层 3:值域开桶 max⁡ai<220\max a_i < 2^{20} 13∼1913 \sim 19 桶计数加超集和,取满足 super_cnt[X]⩾m\mathrm{super\_cnt}[X] \geqslant m 的最大 XX O(20⋅220+n)O(20 \cdot 2^{20} + n) 换视角:不选数,只判断“掩码 XX 能不能被 mm 个数同时包含”;代价是值域必须能开桶
层 4:逐位贪心 全范围 2020 从第 29 位到第 0 位维护必修掩码,候选数够 mm 个就保留这一位 O(30n)O(30n) 取消值域限制:2202^{20} 个桶换成 3030 次 nn 元扫描
分支:性质 A 每个 ai=2k−1a_i = 2^k - 1 1313 这类数的按位与等于最小值,取最大的 mm 个 O(nlog⁡n)O(n \log n) 不在主线上,靠数值结构把问题化成排序
  • 层 2 的四个 mm 之间互不包含。m=2m = 2 的 O(n2)O(n^2) 和 m=n−1m = n - 1 的 O(30n)O(30n) 彼此没有依赖,它们只是“先知道会选几个,再直接造选法”这个想法的四个实例。
  • 层 3 是主线上唯一一次换视角:从“挑哪 mm 个数”变成“某个掩码能不能被至少 mm 个数同时包含”。这个判据与 mm 的具体取值无关,代价是必须把整个值域开成桶。
  • 性质 A 是分支,不是通向正解的一步。它对数值结构的依赖太强,换一组数据立刻失效。保留它的理由有两条:测试点 13 正是给这个观察留的档位;它也顺带说明按位与在高位上只看公共前缀。

性质 B(n=2kn = 2^k,ai=i−1a_i = i - 1)没有单独立层。这组数据恰好铺满 00 到 n−1n - 1,层 3 和层 4 都能直接通过,没有自己的做法值得展开。顺带还能看出它的答案有闭式解:前 tt 位全为 1 的数在 00 到 n−1n - 1 里恰好有 2k−t2^{k-t} 个,只要 2k−t⩾m2^{k-t} \geqslant m,这 tt 位就都能保留;所以答案就是满足 2k−t⩾m2^{k-t} \geqslant m 的最大 tt 所对应的 (2t−1)⋅2k−t(2^t - 1) \cdot 2^{k-t}。

暴力解法

适用范围

n⩽20n \leqslant 20,对应测试点 1∼51 \sim 5。

思路

按定义枚举:把“选 mm 个下标”的所有组合都列出来,逐个算出按位与,取最大值。

brute.cpp 用递归决定每个下标选或不选,选满 mm 个就结算一次。加了一条剪枝:剩下的下标不够补齐 mm 个时直接返回。

代码

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;
}

复杂度与瓶颈

组合数是 (nm)\binom{n}{m},每组还要做 mm 次与运算,总时间 O((nm)⋅m)O\left(\binom{n}{m} \cdot m\right),空间 O(n)O(n)。n=20n = 20 的最坏情况是 (2010)≈1.8×105\binom{20}{10} \approx 1.8 \times 10^5 个组合,够用;n=2×105n = 2 \times 10^5 时完全没有希望。

真正的瓶颈不在常数,而在“把 mm 个位置当成一个整体来挑”。按位与的每一位只关心有没有至少 mm 个数在这一位上是 1,并不关心具体是哪 mm 个。这个观察是后面所有层的起点。

子任务解法:固定 m 的四个取值

适用范围

mm 取四个固定值:m=1m = 1(测试点 6)、m=nm = n(测试点 7)、m=2m = 2(测试点 8∼108 \sim 10,n⩽2000n \leqslant 2000)、m=n−1m = n - 1(测试点 11、12,n⩽2×105n \leqslant 2 \times 10^5)。

思路

前三个都很直接:

  • m=1m = 1:只选一个数,答案就是最大值,扫一遍。
  • m=nm = n:所有数都要选,答案是全体按位与,扫一遍。
  • m=2m = 2:n⩽2000n \leqslant 2000 时数对只有约 2×1062 \times 10^6 个,直接枚举。

m=n−1m = n - 1 值得单独想。选 n−1n - 1 个数等价于从全部 nn 个数里丢掉一个,而要丢哪个数可以逐位判断。

先算全体按位与 base。第 bb 位在 base 里是 1,说明没有数在这一位为 0,丢谁都不影响;base 里为 0 的位,说明至少有一个数在这一位为 0,而丢掉一个数只能补回“恰好只有一个数为 0”的那些位。

于是统计每一位上为 0 的数的个数:

  • 0 个:这一位已经在 base 里,不用管;
  • 1 个:把那个在这位上为 0 的数丢掉,这一位就能补回 1;
  • 2 个及以上:丢掉一个数后仍有数为 0,这一位救不回来。

把“只能靠丢掉第 ii 个数补回的位”记成 extra[i],丢掉第 ii 个数得到的答案就是 base | extra[i],枚举 ii 取最大即可。

以 a=(7,5,3)a = (7, 5, 3)、m=2m = 2 为例(n=3n = 3,恰好丢一个):base = 7 & 5 & 3 = 1;第 1 位只有 5 是 0,丢 5 得到 1or2=31 \mathbin{\mathrm{or}} 2 = 3;第 2 位只有 3 是 0,丢 3 得到 1or4=51 \mathbin{\mathrm{or}} 4 = 5。取最大是 5,与真实最优(选 7 和 5)一致。

代码

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 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;
}

这份代码只处理这四个 mm,其他 mm 会输出 0。

复杂度与瓶颈

m=1m = 1 和 m=nm = n 是 O(n)O(n),m=2m = 2 是 O(n2)O(n^2),m=n−1m = n - 1 是 O(30n)O(30n),空间 O(n)O(n)。n=2×105n = 2 \times 10^5 的 m=n−1m = n - 1 数据本地实测约 31 ms。

这一层完全靠 mm 的形状。m=3m = 3 或者 m=n−2m = n - 2 时四个分支一个都用不上;m=2m = 2 在 n=2×105n = 2 \times 10^5 时是 2×10102 \times 10^{10} 个数对,也跑不完。要往前走,必须放弃“按 mm 分类”这个前提,找一个对任意 mm 都成立的判据。

特例分支:全部是一串 1(性质 A)

适用范围

每个 aia_i 都能写成 2ki−12^{k_i} - 1,二进制下就是末尾一串连续的 1。对应测试点 13。

思路

设 p⩽qp \leqslant q,比较 2p−12^p - 1 与 2q−12^q - 1:前者的 1 位全部包含在后者的 1 位里,所以两者的按位与等于较小的那个。推广到 mm 个数,按位与等于其中最小的数。

于是题目变成“从 nn 个数里选 mm 个,让最小值尽量大”。答案就是第 mm 大的数,升序排序后取 an−m+1a_{n-m+1} 即可。

代码

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 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 的数据上正确,一般数据会算错。

复杂度与瓶颈

时间 O(nlog⁡n)O(n \log n)(换成 nth_element 可以做到 O(n)O(n)),空间 O(n)O(n)。

局限很明显:结论依赖“每个数都是 2k−12^k - 1”,题面只给了这一档。它不是通向正解的中间步骤,后面的层 3 和层 4 都能直接通过测试点 13。

子任务解法:值域能开桶就换一个刻画

适用范围

max⁡ai<220\max a_i < 2^{20},对应测试点 13∼1913 \sim 19。时间上允许 20⋅220≈2.1×10720 \cdot 2^{20} \approx 2.1 \times 10^7 次操作。

思路

层 2 留下的是“对任意 mm 都得有一个判据”这个问题。做法是把“挑 mm 个数”改写成对一个掩码的判定,mm 只出现在一个计数的门槛上,不再决定算法的形状。

对一个掩码 XX,记 super_cnt[X]\mathrm{super\_cnt}[X] 为满足 (ai&X)=X(a_i \mathbin{\&} X) = X 的下标个数,也就是“包含 XX 全部位”的数的个数。下面证明“能不能选出按位与包含 XX 的 mm 个数”和“super_cnt[X]⩾m\mathrm{super\_cnt}[X] \geqslant m”是一回事。

充分性(super_cnt[X]⩾m\mathrm{super\_cnt}[X] \geqslant m ⇒\Rightarrow 能选出按位与包含 XX 的 mm 个数)

  • 把满足 (ai&X)=X(a_i \mathbin{\&} X) = X 的 mm 个下标选出来。
  • 在 XX 为 1 的每一位上,这 mm 个数都是 1。
  • 所以它们的按位与在这一位也是 1,即按位与包含 XX 的全部位。

必要性(能选出按位与包含 XX 的 mm 个数 ⇒\Rightarrow super_cnt[X]⩾m\mathrm{super\_cnt}[X] \geqslant m)

  • 设某个选法的按位与包含 XX 的全部位。
  • 那么被选中的每个数都含 XX 的每一位,即每个被选中的数都是 XX 的超集。
  • 这样的下标至少有 mm 个,所以 super_cnt[X]⩾m\mathrm{super\_cnt}[X] \geqslant m。

有了这个等价,答案就是满足 super_cnt[X]⩾m\mathrm{super\_cnt}[X] \geqslant m 的最大 XX:

  • 最优选法的按位与 AA 本身就满足 super_cnt[A]⩾m\mathrm{super\_cnt}[A] \geqslant m,所以 AA 在可行集合里;
  • 反过来,任何可行的 XX 都对应某个按位与 Y⊇XY \supseteq X 的选法,而 Y⩾XY \geqslant X,所以 XX 不会超过答案;
  • 两边的界合起来,可行集合的最大值就是答案。

剩下的事只有一件:把所有 XX 的 super_cnt\mathrm{super\_cnt} 算出来。值域小,开桶即可。

  • 桶 cnt[v]\mathrm{cnt}[v] 记值 vv 出现的次数。
  • 按位做 20 轮:对每一位 bb,把所有第 bb 位为 1 的桶加到“去掉这一位”的桶上,即 cnt[x]←cnt[x]+cnt[x∣2b]\mathrm{cnt}[x] \leftarrow \mathrm{cnt}[x] + \mathrm{cnt}[x \mid 2^b],这里 xx 的第 bb 位为 0。
  • 每一轮相当于允许这一位自由翻转,20 轮之后 cnt[X]\mathrm{cnt}[X] 把 XX 的所有超掩码都统计过一遍,正是 super_cnt[X]\mathrm{super\_cnt}[X]。

拿样例 #1 验算(a=(3,15,12,12)a = (3, 15, 12, 12),m=2m = 2),只看真正有数的几个掩码:

掩码 XX 包含 XX 全部位的数 super_cnt[X]\mathrm{super\_cnt}[X] 能选出 m=2m = 2 个
14 15 1 否
13 15 1 否
12 15、12、12 3 是

12 可行而 14、13 不可行,所以答案是 12,与逐位贪心的结论一致。

代码

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 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;
}

复杂度与瓶颈

时间由三部分组成:装桶 O(n)O(n),超集和 20⋅220≈2.1×10720 \cdot 2^{20} \approx 2.1 \times 10^7 次加法,倒序扫描 2202^{20}。空间是 2202^{20} 个 int,约 4 MB。n=2×105n = 2 \times 10^5、ai<220a_i < 2^{20} 时本地实测 42 至 44 ms、内存 4.4 至 4.9 MB,都在限制内。

瓶颈在桶的大小。max⁡ai<230\max a_i < 2^{30} 时桶要 2302^{30} 个 int,也就是 4 GB,测试点 20 过不去。想过它,必须把“枚举整个值域”换成“只看这 nn 个数自己的 30 个位”。

正解

关键观察

第 bb 位的权值 2b2^b 比所有更低位权值之和还大:

2b>2b−1=∑j=0b−12j.2^b > 2^b - 1 = \sum_{j=0}^{b-1} 2^j .

所以只要存在 mm 个数能让第 bb 位为 1,就绝不该为了低位的任何组合放弃它。位的判断可以从第 29 位往第 0 位依次做,做完的位不必回头。

思路

把层 3 的“可行掩码”换成按位决策,并且只统计这 nn 个数本身,不开桶。

用 ans 记已经确定必须为 1 的位。判断第 bb 位时先假设它也取 1,得到试探掩码 want = ans | (1 << b);再数一数有多少个数同时含 ans 的全部位和第 bb 位,也就是满足 (a[i] & want) == want 的个数。个数不少于 mm 就把 ans 换成 want,否则这一位放弃。

正确性靠按位归纳:

  • 归纳假设:处理完高于 bb 的位后,ans 的每一位都出现在最优答案里。
  • 候选数不少于 mm 个时,这 mm 个数同时含 ans 和第 bb 位,能凑出包含 want 的按位与。而任何按位与含 ans、第 bb 位却是 0 的选法,在高于 bb 的位上只能与 ans 完全一致:更高的位如果还能再多含一位,那一步就已经把它保留进 ans 了。高位相同而第 bb 位为 0,它的值就严格小于保留这一位得到的结果。所以这一位必须保留。
  • 候选数不足 mm 个时,不存在 mm 个数同时含 ans 和第 bb 位,最优答案的第 bb 位只能是 0,放弃它不损失任何东西。
  • 每一步都不破坏最优性,循环结束时 ans 就是答案。

以样例 #1 为例,a=(3,15,12,12)a = (3, 15, 12, 12),m=2m = 2,只看真正起作用的低四位:

位 bb 试探掩码 同时含这些位的数 个数 决策
29∼429 \sim 4 逐位试探 无(所有数都在低四位以内) 0 放弃,ans 保持 0
3 8 15、12、12 3 ⩾2\geqslant 2,保留,ans = 8
2 12 15、12、12 3 ⩾2\geqslant 2,保留,ans = 12
1 14 15 1 <2< 2,放弃,ans 仍为 12
0 13 15 1 <2< 2,放弃,答案 12

第 3、2 位定下来之后,第 1、0 位无论怎么挑都凑不出两个同时含这些位的数,所以答案是 8+4=128 + 4 = 12。第 1 位那一步也说明了为什么不能先试 14:只有 15 一个数含 14,凑不满 m=2m = 2 个,强行保留会让后面的判断全部失去候选。

还有一个看起来很像、实际错误的直觉:直接选数值最大的 mm 个数。取 a=(8,7,7)a = (8, 7, 7)、m=2m = 2,最大的两个是 8 和 7,与出来是 00;而选两个 7 得到 7。按数值排序和按二进制位决策是两件事。

代码

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
 */
// 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;
}

复杂度

  • 时间:3030 个位,每一位扫一遍 nn 个数,O(30n)O(30n)。n=2×105n = 2 \times 10^5 时约 6×1066 \times 10^6 次与运算,本地实测 36 至 49 ms。
  • 空间:存下输入的 nn 个数,O(n)O(n),实测 1.3 至 1.6 MB。
  • 位掩码用 int 就够:题面限定 ai<230a_i < 2^{30},最高只探到第 29 位,1 << 29 在 32 位 int 内不会溢出。如果值域扩到 2312^{31} 以上,这里要改成 1ll << b,否则会碰到有符号整数的溢出。

总结

这题的入手点是把“选 mm 个数”换成“定 30 个二进制位”。

按位与的每一位只关心“够不够 mm 个数”,mm 个数的具体身份只通过“必须是同一批数”这一条约束串起来。层 3 用超集计数把这条约束也解掉,代价是必须枚举值域;正解退回逐位决策,用候选过滤代替整个值域,代价是每位都要扫一遍全部数。两条路的复杂度都是“值域或位宽”乘上另一个量,值域小就走桶,值域大就走扫描。

分档的意义也在这里。固定 mm、性质 A、小值域,都只是某一条约束被额外收紧后的简化版本,抓住收紧的那一条就能拿到对应分数,而正解是把这些约束一起松开的结果。

再往外推一步:目标是最大化一个按位组合出来的值、并且每一位的贡献按指数递减时,都可以从高位到低位贪心,判定一位时只保留满足已定高位的那批候选,看这一位还能不能凑够所需的个数。

相关 rbook 文章

做这题用到的知识点在 rbook 算法书 上有专门章节:

知识点 对应文章 用在哪
位运算基础 《二进制与位运算》 第 kk 位怎么取、权值是 2k2^k;正解里 (a[i] & want) == want 就是在检查若干位
子集枚举 《子集枚举》 值域能开桶那一层,按掩码统计“包含它全部位的数有多少个”
贪心与正确性证明 《贪心算法》 从高位到低位“能保就保”,靠 2b2^b 大于低位之和论证这一步不会破坏全局最优