「EZEC-4」可乐

采用贡献覆盖视角,把每个元素能接受的异或值区间在 01-Trie 上打懒标记,最后通过 DFS 下传求得最优解。

OJ: luogu

题目 ID: P6824

难度:普及+/提高

标签:01-Trie数位DP异或懒标记差分python

日期: 2026-07-16 19:57

题意

任选非负整数 xx,最大化满足 (aix)k(a_i \oplus x) \le k 的元素数量。

数据范围1n,k,ai1061 \le n, k, a_i \le 10^6

思路

人脑思考路径

一句话本质:将“对选择的 xx 遍历 aia_i 进行合法性判定”转化为“对于每个 aia_i,在 xx 的字典树(超平面)上进行区间/子树贡献覆盖”。

直接枚举 xx 的瓶颈在哪里? 由于 xx 可以是任意非负整数,虽然有用范围在位宽以内(约 2202^{20}),但如果针对每个 xx 都去扫描一遍 nn 个数进行异或判定,总复杂度将高达 O(N220)O(N \cdot 2^{20}),这在 n106n \le 10^6 的数据范围下是无法接受的。我们必须避免“选择一个 xx 再去匹配所有 aia_i”的被动思路。

如果反过来思考,从每一个独立的 aia_i 出发会看到什么? 对于一个确定的 aia_i,能让 (aix)k(a_i \oplus x) \le k 成立的 xx 并不是杂乱无章的。由于大小比较是二进制字典序,这等价于 aixa_i \oplus x 的高位与 kk 相同,且在某个 kk 为 1 的数位上异或结果取 0 使得低位可以任取。如果我们把 xx 视为未知数前缀,那么每个 aia_i 能接受的所有 xx 集合可以分解成若干个“高位确定、低位任取”的前缀区间。在 01-Trie 上,这些区间恰好对应着若干个不相交的整棵子树。

如何在 Trie 树上刻画这些能接受 xx 的子树? 我们可以直接构建 xx 的 01-Trie。对于每个 aia_i,顺着异或字典序的比较过程,在树上查找:

  1. 如果遇到 kk 的当前位为 11,只要 xx 的当前位取 aia_i 的当前位,异或值即为 0,这部分 xx 的子树已经严格小于 kk,我们可以对其根节点打上 +1 懒标记,代表这片区域全部被 aia_i 覆盖;同时为了处理“相等”的情况,指针走向另一条分支(让异或值为 1)继续深入比较。
  2. 如果遇到 kk 的当前位为 00,异或值绝对不能为 1,所以指针必须且只能走向让异或值为 0 的那条分支。
  3. 走到底层时,对于相等的分支也打上 +1 标记。

如何得到最终的答案? 当我们把所有 aia_i 的合法区间在 xx 的 Trie 树上全部打上标记之后,我们只需要从根节点出发,做一次 DFS 把高层的懒标记累加并下传(Push Down)到所有叶子节点。每个叶子节点最终累加得到的覆盖值,就代表该叶子对应的数值 xx 能够喝到的可乐箱数。最后扫描所有叶子,求最大值即可。这就像是在树上做了一次一维区间的“差分与前缀和”。


1. 核心思路:贡献覆盖

要用 01-Trie 来解这道题,我们的核心思路从单纯的“查找”转变为“贡献覆盖”。

我们不再是拿着一个确定的 xx 去树上找有多少个 aia_i 满足条件,而是把每个 aia_i 能够接受的所有 xx 对应的节点,在 01-Trie 上打上 +1 的懒标记(Lazy Tag)。最后通过一次 DFS 把标记下传到叶子节点,找出被覆盖次数最多的叶子就是最佳的 xx

2. 详细遍历与打标规则

对于 01-Trie 的遍历,当我们处理 kk 的第 jj 位时:

  1. kj=1k_j = 1:说明 aixa_i \oplus x 的这一位如果取 0,那么整体已经严格小于 kk。此时低位可以任取。我们直接给 xj=ai[j]0x_j = a_i[j] \oplus 0 对应的分支打上 +1 标记;同时,为了处理等于的情况,当前指针必须向 xj=ai[j]1x_j = a_i[j] \oplus 1 的分支继续深入。
  2. kj=0k_j = 0:说明 aixa_i \oplus x 的这一位只能取 0,否则会大于 kk。我们没有“严格小于”的分支可以选,指针只能向 xj=ai[j]0x_j = a_i[j] \oplus 0 的分支深入。

3. 样例 1 全局模拟

已知:

  • k=5k = 5,二进制为 101(低 3 位:位 2, 位 1, 位 0)。
  • 序列 a={2,3,4}a = \{2, 3, 4\},二进制分别为 010, 011, 100
步骤一:处理 a1=2 (0102)a_1 = 2 \ (010_2)

当前路径前缀为空,从根节点出发。

  • 第 2 位 (k2=1,a1[2]=0k_2 = 1, a_1[2] = 0)
    • 严格小于:x2=00=0x_2 = 0 \oplus 0 = 0。给左分支(代表 0xx)打上 +1 标记。
    • 保持相等:指针走向 x2=01=1x_2 = 0 \oplus 1 = 1。目前路径为 1
  • 第 1 位 (k1=0,a1[1]=1k_1 = 0, a_1[1] = 1)
    • 只能保持相等:x1=10=1x_1 = 1 \oplus 0 = 1。指针走向 1。目前路径为 11
  • 第 0 位 (k0=1,a1[0]=0k_0 = 1, a_1[0] = 0)
    • 严格小于:x0=00=0x_0 = 0 \oplus 0 = 0。给节点 110 打上 +1 标记。
    • 保持相等:指针走向 x0=01=1x_0 = 0 \oplus 1 = 1。目前路径为 111
  • 末尾:抵达叶子节点 111,给精确匹配的叶子打上 +1
状态图 1

下图展示了在插入第一个元素 a1=2a_1 = 2 并根据 k=5k=5 的数位决策进行标记后的 01-Trie 状态:

graph TD
    R["Root"] -->|0| N0["0 (+1)"]
    R -->|1| N1["1"]
    N1 -->|1| N11["11"]
    N11 -->|0| N110["110 (+1)"]
    N11 -->|1| N111["111 (+1)"]

从该图中可以看出:

  • 左分支 0 被打上 +1 标记,表示所有以 0 开头的 xx(即 000011)都在第一步被覆盖。
  • 随后指针沿着相等路径走向右分支,最终在 110 打上 +1,并在精确相等的叶子 111 处也打上 +1
步骤二:处理 a2=3 (0112)a_2 = 3 \ (011_2)

当前路径前缀为空,重置到根节点。

  • 第 2 位 (k2=1,a2[2]=0k_2 = 1, a_2[2] = 0)
    • 严格小于:x2=00=0x_2 = 0 \oplus 0 = 0。给左分支 0 打上 +1 标记(此时 0 节点的 tag 累加为 2)。
    • 保持相等:指针走向 x2=01=1x_2 = 0 \oplus 1 = 1。目前路径为 1
  • 第 1 位 (k1=0,a2[1]=1k_1 = 0, a_2[1] = 1)
    • 只能保持相等:x1=10=1x_1 = 1 \oplus 0 = 1。指针走向 1。目前路径为 11
  • 第 0 位 (k0=1,a2[0]=1k_0 = 1, a_2[0] = 1)
    • 严格小于:x0=10=1x_0 = 1 \oplus 0 = 1。给节点 111 加上 +1(注意,这是给子树打标记)。
    • 保持相等:指针走向 x0=11=0x_0 = 1 \oplus 1 = 0。目前路径为 110
  • 末尾:抵达叶子节点 110,给该叶子打上 +1
状态图 2

下图展示了继续插入第二个元素 a2=3a_2 = 3 后的 01-Trie 状态:

graph TD
    R["Root"] -->|0| N0["0 (+2)"]
    R -->|1| N1["1"]
    N1 -->|1| N11["11"]
    N11 -->|0| N110["110 (+2)"]
    N11 -->|1| N111["111 (+2)"]

从该图中可以看出:

  • 由于 a2a_2 在高位与 a1a_1 相同,左分支 0 再次被覆盖,标记值累加到了 +2
  • 右侧子树 11 分支下,节点 110111 各自再次得到 +1 标记,标记值都累计到了 +2
步骤三:处理 a3=4 (1002)a_3 = 4 \ (100_2)

回到根节点。

  • 第 2 位 (k2=1,a3[2]=1k_2 = 1, a_3[2] = 1)
    • 严格小于:x2=10=1x_2 = 1 \oplus 0 = 1。给右分支 1 打上 +1 标记。
    • 保持相等:指针走向 x2=11=0x_2 = 1 \oplus 1 = 0。目前路径为 0
  • 第 1 位 (k1=0,a3[1]=0k_1 = 0, a_3[1] = 0)
    • 只能保持相等:x1=00=0x_1 = 0 \oplus 0 = 0。指针走向 0。目前路径为 00
  • 第 0 位 (k0=1,a3[0]=0k_0 = 1, a_3[0] = 0)
    • 严格小于:x0=00=0x_0 = 0 \oplus 0 = 0。给节点 000 打上 +1
    • 保持相等:指针走向 x0=01=1x_0 = 0 \oplus 1 = 1。目前路径为 001
  • 末尾:抵达叶子 001,打上 +1
状态图 3

下图展示了插入第三个元素 a3=4a_3 = 4 后的最终 01-Trie 标记分布形态:

graph TD
    R["Root"] -->|0| N0["0 (+2)"]
    R -->|1| N1["1 (+1)"]
    N0 -->|0| N00["00"]
    N00 -->|0| N000["000 (+1)"]
    N00 -->|1| N001["001 (+1)"]
    N1 -->|1| N11["11"]
    N11 -->|0| N110["110 (+2)"]
    N11 -->|1| N111["111 (+2)"]

从该图中可以看出:

  • 插入 a3a_3 时,右分支 1 被打上 +1 标记。
  • 在左侧分支下,节点 00 被创建,叶子 000001 各自被标记,形成了整棵树的最终覆盖状态。
步骤四:标记下传 (Push Down) 找答案

现在我们要把高层的 Tag 像瀑布一样流到叶子节点,计算每个叶子最终受到的覆盖总和。

  • 根节点的左孩子 0 原本有 +2 的 Tag,它会将这 +2 继承给它的所有子孙(即 000, 001, 010, 011)。
    • 叶子 000:继承 +2,自身有 +1 -> 总计 3
    • 叶子 001:继承 +2,自身有 +1 -> 总计 3
    • 叶子 010011:只继承 +2 -> 总计 2
  • 根节点的右孩子 1+1 的 Tag,同样下传(给 100111)。
    • 叶子 100101:只继承 +1 -> 总计 1
    • 叶子 110:继承 +1,加上之前累计的 +2 -> 总计 3
    • 叶子 111:继承 +1,加上之前累计的 +2 -> 总计 3

扫描所有叶子节点,最大覆盖数为 3(当 x=000,001,110,111x = 000, 001, 110, 111x{0,1,6,7}x \in \{0, 1, 6, 7\} 时,都能喝到 3 箱可乐),这与样例 1 的输出完全吻合!


4. 概念模型与数学本质

集合重叠(最大覆盖)本质

对于每一个 aia_i,满足条件 aixka_i \oplus x \le kxx 构成了一个个独立的集合 Si={xaixk}S_i = \{x \mid a_i \oplus x \le k\}。题目所求的“使得喝到可乐箱数最大”的 xx,其本质就是寻找一个元素 xx,使得包含 xx 的集合 SiS_i 的数量最大。 01-Trie 在这里扮演了集合的高效表示与区间求和工具角色。每个 SiS_ixx 的 01-Trie 上都表现为至多 logV\log V 个子树的并集。我们在 Trie 上给这些对应的子树打上 +1 懒标记,最后通过 DFS 累加下传,本质上就是在求所有集合在具体元素(叶子 xx)上的覆盖重叠度,以此高效定位出重叠度最高的数字。

算子映射

在 01-Trie 的拓扑结构中,从根到叶子的深度递增,映射了布尔空间维度的坍缩。条件 aixka_i \oplus x \le k 中的 kk 作为截断条件,本质上是对超立方体进行的“半空间切割”。那些严格小于的分支,就是在高维空间中被完全包含的子立方体,因此可以对该子树对应的根节点进行一次 O(1)O(1) 的标量赋值(Lazy Tag)。

思维模板:Trie 树懒标记

在求解“最大化满足条件的元素个数”时,如果判定条件是前缀强相关的位运算,将其转换为“对所有合法前缀打 Tag,最后 DFS 聚合”。这等价于在一维空间里进行区间差分。

快速识别
  • 指纹特征:给定数组 AA,求使得 [AiXK]\sum [A_i \oplus X \le K] 最大的 XX
  • 结构等价:这种全局询问的问题,如果用 01-Trie 做,实际上就是“树上差分 / 懒标记下推”;如果降维到一维数组,就是“区间差分 + 前缀和”。两者数学本质完全同构。
数学推导

设 Trie 树上节点 uu 代表前缀 PuP_u。令 Tag(u)Tag(u) 为该节点获得的标记值。 对于任意叶子节点 vv(对应特定的整数 xx),其最终覆盖度 C(v)C(v) 等于根到该叶子路径上所有标记的和:

C(v)=uPath(Rootv)Tag(u)C(v) = \sum_{u \in Path(Root \to v)} Tag(u)

这完美避免了枚举 xx,将 O(VlogV)O(V \log V) 的复杂度降维到 O(NlogV)O(N \log V) 建树 + O(V)O(V) 遍历。

5. 避坑指南

  • 标记位置的选择:在处理严格小于分支时,容易搞错是要把标记打在“当前节点”还是“下一个状态的节点”上。一定要记住,标记必须打在xx 的二进制位对应的边走向 of 子节点上。
  • DFS 剪枝与遍历优化:如果一个分支从没被创建过,说明没有 aia_i 在那个分支上有比当前更深的贡献。我们在 DFS 遍历时,不需要访问不存在的空分支,直接遍历已有分支即可,因为未建出的隐式分支的最大贡献必然不超过其非空祖先。

代码

python
import sys

# Increase recursion depth just in case, though 25 is enough
sys.setrecursionlimit(2000)

def main():
    input = sys.stdin.read
    data = input().split()
    if not data:
        return
    n = int(data[0])
    k = int(data[1])
    a = [int(x) for x in data[2:]]
    
    m = max(k, max(a))
    top_bit = m.bit_length() - 1 if m > 0 else 0
    
    # Trie lists: ch0[u], ch1[u], tag[u]. Root is 1.
    ch0 = [0, 0]
    ch1 = [0, 0]
    tag = [0, 0]
    cnt = 1
    
    for a_val in a:
        u = 1
        for bit in range(top_bit, -1, -1):
            k_bit = (k >> bit) & 1
            a_bit = (a_val >> bit) & 1
            if k_bit == 1:
                # 1. 严格小于分支: x_bit = a_bit
                x_bit_less = a_bit
                if x_bit_less == 0:
                    if not ch0[u]:
                        cnt += 1
                        ch0[u] = cnt
                        ch0.append(0)
                        ch1.append(0)
                        tag.append(0)
                    tag[ch0[u]] += 1
                else:
                    if not ch1[u]:
                        cnt += 1
                        ch1[u] = cnt
                        ch0.append(0)
                        ch1.append(0)
                        tag.append(0)
                    tag[ch1[u]] += 1
                
                # 2. 保持相等分支: x_bit = a_bit ^ 1
                x_bit_eq = a_bit ^ 1
                if x_bit_eq == 0:
                    if not ch0[u]:
                        cnt += 1
                        ch0[u] = cnt
                        ch0.append(0)
                        ch1.append(0)
                        tag.append(0)
                    u = ch0[u]
                else:
                    if not ch1[u]:
                        cnt += 1
                        ch1[u] = cnt
                        ch0.append(0)
                        ch1.append(0)
                        tag.append(0)
                    u = ch1[u]
            else:
                # 只能保持相等分支: x_bit = a_bit
                x_bit_eq = a_bit
                if x_bit_eq == 0:
                    if not ch0[u]:
                        cnt += 1
                        ch0[u] = cnt
                        ch0.append(0)
                        ch1.append(0)
                        tag.append(0)
                    u = ch0[u]
                else:
                    if not ch1[u]:
                        cnt += 1
                        ch1[u] = cnt
                        ch0.append(0)
                        ch1.append(0)
                        tag.append(0)
                    u = ch1[u]
        tag[u] += 1

    max_cola = 0
    
    # DFS traversal
    def dfs(u, current_sum):
        nonlocal max_cola
        if not u:
            return
        current_sum += tag[u]
        if not ch0[u] and not ch1[u]:
            if current_sum > max_cola:
                max_cola = current_sum
            return
        if ch0[u]:
            dfs(ch0[u], current_sum)
        if ch1[u]:
            dfs(ch1[u], current_sum)

    dfs(1, 0)
    print(max_cola)

if __name__ == '__main__':
    main()
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-08-02
 * update_at: 2026-08-02
 */

/* P6824 「EZEC-4」可乐 */
/* 思路(与 index.md 样例图的"贡献覆盖"一致):在"x 的 01-Trie"上为每个 a_i
 * 打 +1 懒标记——k 位 = 1 时,异或位取 0 的整棵子树打标(严格小于,低位任取),
 * 深入异或位取 1 的分支;k 位 = 0 时只能深入异或位取 0 的分支;走到底的叶子 +1。
 * 最后 DFS 把标记下传,覆盖数最大的叶子就是最佳 x。
 * 01-Trie 复用 rbook 文章《Trie 字典树》模板的结构(tree/Node 为公开成员),
 * 打标与下传都写在模板外部。 */

#include <bits/stdc++.h>
using namespace std;

// 字典树 Trie 模板:插入字符串、判断是否存在、统计前缀出现次数
// 模板参数:ALPHA 字符集大小,OFFSET 字符起点(如 'a')
// 节点维护 pass(经过次数) 和 end(单词结尾次数)
// 用法:Trie<26,'a'> tr; tr.insert("abc"); tr.contains("abc"); tr.count_prefix("ab");
template <int ALPHA = 26, char OFFSET = 'a'>
struct Trie {
    struct Node {
        array<int, ALPHA> ch{}; // ch[c] 子节点编号,0 为空(根也是 0)
        int pass = 0;           // 经过该节点的字符串个数
        int end = 0;            // 以该节点结尾的完整字符串个数
    };

    vector<Node> tree;          // tree[0] 为根

    Trie() { tree.push_back(Node()); }

    // 插入 s
    void insert(const string &s) {
        int u = 0;
        tree[u].pass++;
        for (char cc : s) {
            int c = cc - OFFSET;
            if (tree[u].ch[c] == 0) {       // 无子节点则新建
                tree[u].ch[c] = (int)tree.size();
                tree.push_back(Node());
            }
            u = tree[u].ch[c];
            tree[u].pass++;
        }
        tree[u].end++;
    }

    // 判断 s 是否完整插入过
    bool contains(const string &s) const {
        int u = 0;
        for (char cc : s) {
            int c = cc - OFFSET;
            if (tree[u].ch[c] == 0) return false;
            u = tree[u].ch[c];
        }
        return tree[u].end > 0;  // 必须作为完整单词结尾
    }

    // 统计以 prefix 为前缀的字符串个数
    int count_prefix(const string &prefix) const {
        int u = 0;
        for (char cc : prefix) {
            int c = cc - OFFSET;
            if (tree[u].ch[c] == 0) return 0;
            u = tree[u].ch[c];
        }
        return tree[u].pass;
    }
};

const int MAXN = 100000 + 5;

int n, k;                   // n 箱可乐,上限 k
int a[MAXN];
int top_bit;                // 最高有效位(k 与所有 a_i 的最大位数 - 1)

Trie<2, '0'> trie;          // x 的 01-Trie:每条根到叶子的路径就是一个可能的 x
vector<int> tag(1, 0);      // tag[u]:节点 u 的懒标记,与 trie.tree 下标对齐

// 确保 u 存在 c 儿子;不存在则新建节点并返回其编号。
// 注意:被打标记的分支即使没有 a_i 走过也要显式建出来,
// 否则 DFS 下传会漏掉只靠祖先标记覆盖的隐式叶子。
int ensure_child(int u, int c) {
    if (trie.tree[u].ch[c] == 0) {
        trie.tree[u].ch[c] = (int)trie.tree.size();
        trie.tree.push_back(Trie<2, '0'>::Node());
        tag.push_back(0);
    }
    return trie.tree[u].ch[c];
}

// 为 a_i 打标记:把能接受 a_i 的所有 x 在 x-Trie 上打 +1
void mark(int x) {
    int u = 0;
    for (int bit = top_bit; bit >= 0; bit--) {
        int ab = (x >> bit) & 1; // a_i 的第 bit 位
        if ((k >> bit) & 1) {
            // k 位 = 1(分水岭):异或位取 0 的分支(x 位 = ab)整棵严格小于 k → +1
            tag[ensure_child(u, ab)] += 1;
            // 异或位取 1 的分支(x 位 = ab^1)等于 k → 继续深入
            u = ensure_child(u, ab ^ 1);
        } else {
            // k 位 = 0(关卡):异或位只能取 0(x 位 = ab),只能深入
            u = ensure_child(u, ab);
        }
    }
    tag[u] += 1; // 走到底:异或结果恰好等于 k 的 x 打 +1
}

int best = 0;

// DFS 下传:acc = 从根到 u 的标记累计(每个隐式叶子继承祖先标记)。
// 标记只会增加,所以任意节点 u 的 acc 都是其子树内叶子的覆盖数下界,
// 全局最大 acc 就是最大覆盖数。
void push_down(int u, int acc) {
    acc += tag[u];
    best = max(best, acc);
    if (trie.tree[u].ch[0])
        push_down(trie.tree[u].ch[0], acc);
    if (trie.tree[u].ch[1])
        push_down(trie.tree[u].ch[1], acc);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> k;
    for (int i = 1; i <= n; i++)
        cin >> a[i];

    int m = max(k, *max_element(a + 1, a + n + 1));
    top_bit = 31 - __builtin_clz(m); // m >= 1,最高有效位 = floor(log2(m))

    for (int i = 1; i <= n; i++)
        mark(a[i]);

    push_down(0, 0);
    cout << best << '\n';
    return 0;
}

静态数组版(与模板版同思路:打标记规则相同,DFS 下传时只结算实际分配出来的叶子):

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-08-02
 * update_at: 2026-08-02
 */

/* P6824 「EZEC-4」可乐 —— 静态数组版(对应 index.md 样例图的"贡献覆盖"思路) */
/* 与 main.cpp(rbook 模板版)是同一算法的两种实现:
 *   打标记规则完全相同(k 位=1:异或位取 0 的子树 +1、深入异或位取 1 的分支;
 *   k 位=0:只深入异或位取 0 的分支;走到底的叶子 +1);
 *   区别:这里 DFS 下传时只结算"实际分配出来的叶子"的覆盖度。
 * 节点用 struct 打包:ch[2](儿子编号)与 tag(懒标记)放在一起。 */

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100000 + 5;
const int MAX_BIT = 20;             // a, k <= 1e6 < 2^20,留一位余量
const int MAX_NODE = MAXN * 22 + 5; // 每个数最多新增 21 个节点,2.2M 足够

// 01-Trie 节点:ch[2] 儿子编号(0 为空),tag 懒标记
struct Node {
    int ch[2];
    int tag;
} trie[MAX_NODE];

int node_cnt = 1; // 根节点始终为 1,0 号留作"空儿子"哨兵

// 将 a 针对条件 a ^ x <= k 插入并打懒标记
void insert(int a, int k) {
    int u = 1;
    for (int i = MAX_BIT; i >= 0; --i) {
        int bit_a = (a >> i) & 1;
        int bit_k = (k >> i) & 1;

        if (bit_k == 1) {
            // 1. x 使得异或结果当前位为 0:整棵子树严格小于 k,低位任取,打 tag
            // x 位 = a 位 ^ 异或位(0);^0 是恒等运算(任何数 ^ 0 不变),
            // 写成 ^0 是为了与下面的 ^1 保持公式对称:x_j = a_i[j] ^ (异或位)
            int branch_less = bit_a ^ 0;
            if (!trie[u].ch[branch_less]) {
                trie[u].ch[branch_less] = ++node_cnt;
            }
            trie[trie[u].ch[branch_less]].tag++;

            // 2. x 使得异或结果当前位为 1:保持前缀相等,继续深入
            int branch_eq = bit_a ^ 1;
            if (!trie[u].ch[branch_eq]) {
                trie[u].ch[branch_eq] = ++node_cnt;
            }
            u = trie[u].ch[branch_eq];
        } else {
            // bit_k == 0:异或结果必须为 0(相等),否则会大于 k
            int branch_eq = bit_a ^ 0;
            if (!trie[u].ch[branch_eq]) {
                trie[u].ch[branch_eq] = ++node_cnt;
            }
            u = trie[u].ch[branch_eq];
        }
    }
    // 叶子节点:精确匹配 a ^ x == k 的情况
    trie[u].tag++;
}

int max_cola = 0;

// DFS 把上层的懒标记瀑布式下推到叶子
void dfs(int u, int current_sum) {
    if (!u) return;

    // 累加当前路径上的 tag
    current_sum += trie[u].tag;

    // 走到实际分配出来的叶子,结算该区域的覆盖度
    if (!trie[u].ch[0] && !trie[u].ch[1]) {
        max_cola = max(max_cola, current_sum);
        return;
    }

    // 继续向下传递
    if (trie[u].ch[0]) dfs(trie[u].ch[0], current_sum);
    if (trie[u].ch[1]) dfs(trie[u].ch[1], current_sum);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, k;
    cin >> n >> k;
    for (int i = 0; i < n; ++i) {
        int a;
        cin >> a;
        insert(a, k);
    }

    dfs(1, 0);
    cout << max_cola << '\n';
    return 0;
}

复杂度

设值域位数为 L21L \le 21a,k106<220a,k \le 10^6 < 2^{20},实际最高位 ≤ 19,代码 MAX_BIT=20 留一位余量)。打标记为 O(nL)O(nL),DFS 下传只遍历已建节点,整体时间与空间均为 O(nL)O(nL)

为什么 DFS 不会超时:下传只遍历"实际分配出来的节点"——每个 insert 沿 LL 位走一遍、每层最多新建 1 个节点(共享前缀复用),所以节点数 nL+12.1×106\le nL + 1 \approx 2.1 \times 10^6n=105n = 10^5),每个节点 O(1)O(1) 结算。对比理论满二叉树:LL 位对应 2L2^L 个叶子(即可能的 xx 个数)、满树节点 2L+112^{L+1}-1——就算遍历满树也很快,但完全没有必要。

为什么隐式叶子不用访问(正确性):一个已分配叶子 uu 的累计值 acc(u)acc(u) 恰好等于它下面所有隐式 xx 的覆盖数——接受性只由根到 uu 路径上的标记决定,uu 之下没有更深的标记,整片区域共享同一个 accacc。所以"结算已分配叶子取最大" = “全体 2L2^Lxx 的最大覆盖”,一个不漏。

总结

本题通过将 (aix)k(a_i \oplus x) \le k 的判定转化为 xx-Trie 上的区间标记覆盖,成功将异或前缀关系映射到树上。利用“区间差分”的思想打懒标记并进行 DFS 一次性下传,极大地优化了暴力枚举的复杂度。