「EZEC-4」可乐
采用贡献覆盖视角,把每个元素能接受的异或值区间在 01-Trie 上打懒标记,最后通过 DFS 下传求得最优解。
OJ: luogu
题目 ID: P6824
难度:普及+/提高
标签:01-Trie数位DP异或懒标记差分python
日期: 2026-07-16 19:57
题意
任选非负整数
数据范围:
思路
人脑思考路径
一句话本质:将“对选择的
直接枚举
如果反过来思考,从每一个独立的
如何在 Trie 树上刻画这些能接受
- 如果遇到
的当前位为 ,只要 的当前位取 的当前位,异或值即为 0,这部分 的子树已经严格小于 ,我们可以对其根节点打上 +1懒标记,代表这片区域全部被覆盖;同时为了处理“相等”的情况,指针走向另一条分支(让异或值为 1)继续深入比较。 - 如果遇到
的当前位为 ,异或值绝对不能为 1,所以指针必须且只能走向让异或值为 0 的那条分支。 - 走到底层时,对于相等的分支也打上
+1标记。
如何得到最终的答案?
当我们把所有
1. 核心思路:贡献覆盖
要用 01-Trie 来解这道题,我们的核心思路从单纯的“查找”转变为“贡献覆盖”。
我们不再是拿着一个确定的 +1 的懒标记(Lazy Tag)。最后通过一次 DFS 把标记下传到叶子节点,找出被覆盖次数最多的叶子就是最佳的
2. 详细遍历与打标规则
对于 01-Trie 的遍历,当我们处理
- 若
:说明 的这一位如果取 0,那么整体已经严格小于。此时低位可以任取。我们直接给 对应的分支打上 +1标记;同时,为了处理等于的情况,当前指针必须向的分支继续深入。 - 若
:说明 的这一位只能取 0,否则会大于。我们没有“严格小于”的分支可以选,指针只能向 的分支深入。
3. 样例 1 全局模拟
已知:
,二进制为 101(低 3 位:位 2, 位 1, 位 0)。- 序列
,二进制分别为 010,011,100。
步骤一:处理
当前路径前缀为空,从根节点出发。
- 第 2 位 (
): - 严格小于:
。给左分支(代表 0xx)打上+1标记。 - 保持相等:指针走向
。目前路径为 1。
- 严格小于:
- 第 1 位 (
): - 只能保持相等:
。指针走向 1。目前路径为11。
- 只能保持相等:
- 第 0 位 (
): - 严格小于:
。给节点 110打上+1标记。 - 保持相等:指针走向
。目前路径为 111。
- 严格小于:
- 末尾:抵达叶子节点
111,给精确匹配的叶子打上+1。
状态图 1
下图展示了在插入第一个元素
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开头的(即 000到011)都在第一步被覆盖。 - 随后指针沿着相等路径走向右分支,最终在
110打上+1,并在精确相等的叶子111处也打上+1。
步骤二:处理
当前路径前缀为空,重置到根节点。
- 第 2 位 (
): - 严格小于:
。给左分支 0打上+1标记(此时0节点的 tag 累加为 2)。 - 保持相等:指针走向
。目前路径为 1。
- 严格小于:
- 第 1 位 (
): - 只能保持相等:
。指针走向 1。目前路径为11。
- 只能保持相等:
- 第 0 位 (
): - 严格小于:
。给节点 111加上+1(注意,这是给子树打标记)。 - 保持相等:指针走向
。目前路径为 110。
- 严格小于:
- 末尾:抵达叶子节点
110,给该叶子打上+1。
状态图 2
下图展示了继续插入第二个元素
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)"]
从该图中可以看出:
- 由于
在高位与 相同,左分支 0再次被覆盖,标记值累加到了+2。 - 右侧子树
11分支下,节点110和111各自再次得到+1标记,标记值都累计到了+2。
步骤三:处理
回到根节点。
- 第 2 位 (
): - 严格小于:
。给右分支 1打上+1标记。 - 保持相等:指针走向
。目前路径为 0。
- 严格小于:
- 第 1 位 (
): - 只能保持相等:
。指针走向 0。目前路径为00。
- 只能保持相等:
- 第 0 位 (
): - 严格小于:
。给节点 000打上+1。 - 保持相等:指针走向
。目前路径为 001。
- 严格小于:
- 末尾:抵达叶子
001,打上+1。
状态图 3
下图展示了插入第三个元素
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)"]
从该图中可以看出:
- 插入
时,右分支 1被打上+1标记。 - 在左侧分支下,节点
00被创建,叶子000和001各自被标记,形成了整棵树的最终覆盖状态。
步骤四:标记下传 (Push Down) 找答案
现在我们要把高层的 Tag 像瀑布一样流到叶子节点,计算每个叶子最终受到的覆盖总和。
- 根节点的左孩子
0原本有+2的 Tag,它会将这+2继承给它的所有子孙(即000, 001, 010, 011)。- 叶子
000:继承+2,自身有+1-> 总计 3 - 叶子
001:继承+2,自身有+1-> 总计 3 - 叶子
010和011:只继承+2-> 总计 2
- 叶子
- 根节点的右孩子
1有+1的 Tag,同样下传(给100到111)。- 叶子
100和101:只继承+1-> 总计 1 - 叶子
110:继承+1,加上之前累计的+2-> 总计 3 - 叶子
111:继承+1,加上之前累计的+2-> 总计 3
- 叶子
扫描所有叶子节点,最大覆盖数为 3(当
4. 概念模型与数学本质
集合重叠(最大覆盖)本质
对于每一个 +1 懒标记,最后通过 DFS 累加下传,本质上就是在求所有集合在具体元素(叶子
算子映射
在 01-Trie 的拓扑结构中,从根到叶子的深度递增,映射了布尔空间维度的坍缩。条件
思维模板:Trie 树懒标记
在求解“最大化满足条件的元素个数”时,如果判定条件是前缀强相关的位运算,将其转换为“对所有合法前缀打 Tag,最后 DFS 聚合”。这等价于在一维空间里进行区间差分。
快速识别
- 指纹特征:给定数组
,求使得 最大的 。 - 结构等价:这种全局询问的问题,如果用 01-Trie 做,实际上就是“树上差分 / 懒标记下推”;如果降维到一维数组,就是“区间差分 + 前缀和”。两者数学本质完全同构。
数学推导
设 Trie 树上节点
这完美避免了枚举
5. 避坑指南
- 标记位置的选择:在处理严格小于分支时,容易搞错是要把标记打在“当前节点”还是“下一个状态的节点”上。一定要记住,标记必须打在与
的二进制位对应的边走向 of 子节点上。 - DFS 剪枝与遍历优化:如果一个分支从没被创建过,说明没有
在那个分支上有比当前更深的贡献。我们在 DFS 遍历时,不需要访问不存在的空分支,直接遍历已有分支即可,因为未建出的隐式分支的最大贡献必然不超过其非空祖先。
代码
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()/**
* 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 下传时只结算实际分配出来的叶子):
/**
* 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;
}复杂度
设值域位数为 MAX_BIT=20 留一位余量)。打标记为
为什么 DFS 不会超时:下传只遍历"实际分配出来的节点"——每个 insert 沿
为什么隐式叶子不用访问(正确性):一个已分配叶子
总结
本题通过将