前缀异或把子段异或化为两前缀配对,01-Trie 贪心求与历史前缀的最大异或,严格大于更新保最早结尾、叶子覆盖保同值最大下标得到最短子段。
OJ: roj
题目 ID: 2096
难度:普及+/提高-
标签:字典树位运算贪心python
创建: 2026-10-01 08:40
更新: 2026-10-07 13:50
形式化题目
给定长度
- 异或值最大;
- 并列时,结尾
最小(奶牛序号越小社会等级越高,"结尾奶牛社会等级最高"即序号最小); - 仍并列时,子段最短(等价于同
下 最大)。
输出最大异或值及其起点 6 4 5。
正解
思路
朴素做法与瓶颈:枚举全部
第一层优化:前缀异或。 记
于是"选子段"变成"选配对
第二层优化:01-Trie 上的贪心。 把
平局规则怎么落地——本题比一般"最大异或对"多了两层比较,各对应一个实现细节:
- 结尾最早:按
从左到右扫描,候选值严格大于当前答案才更新——并列时先扫到的(最小 )自然保留。 - 同结尾最短:固定
后,能达到最大值 的配对值是唯一的 ,即所有并列的 都指向同一个前缀值 、落在同一个叶子上。叶子在同值重复插入时直接覆盖为更大的 ,起点 随之最大,子段最短。 - 子段非空:先查后插——查询
时树里只有 ,保证 且右端恰为 ;查完再把 插入供更晚的结尾使用。
样例逐结尾推演(
| 结尾 |
树中已有的 |
最优 |
当前答案(最大值 |
||
|---|---|---|---|---|---|
| 1 | 1 | 0 | 1 | 1 1 1 | |
| 2 | 1 | 0 | 1 | 1 1 1(并列,保最早结尾) | |
| 3 | 4 | 2 | 5 | 5 3 3 | |
| 4 | 0 | 3 | 4 | 5 3 3( |
|
| 5 | 2 | 3 | 6 | 6 4 5 |
表里有两处平局细节值得注意:
下图展示
graph TD
R["根"] -->|"0"| A["结点 A"]
R -->|"1:位2 反向存在,走"| C["结点 C"]
A -->|"0"| B["结点 B"]
B -->|"0"| L0["叶:值 0,j=4"]
B -->|"1"| L1["叶:值 1,j=2"]
C -->|"0:位1 反向存在,走"| D["结点 D"]
D -->|"0:位0 反向 1 不存在,顺走"| L4["叶:值 4,j=3"]
三条带文字说明的边构成查询路径,三个决策位依次异或出
代码
Python 版:
#!/usr/bin/env python3
# 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-01 09:01
# update_at: 2026-10-01 09:01
import sys
from array import array
BITS = 21 # 数值范围 0..2^21-1,从第 20 位往下逐位决策
CH = array("i", [0, 0]) # 01 字典树孩子表:节点 u 的 0/1 孩子存在 CH[2u]/CH[2u+1],0 表示空
IDX = array("i", [-1]) # 节点值表:叶子记录该前缀值已插入的最大下标,非叶子恒为 -1
def insert(value: int, j: int) -> None:
"""把前缀值 value 插入 01 字典树,并在叶子登记下标 j。"""
u = 0
for k in range(BITS - 1, -1, -1):
bit = value >> k & 1
nxt = CH[2 * u + bit]
if not nxt: # 该分支还没建,补一个叶子链节点
nxt = len(IDX)
CH.extend((0, 0))
IDX.append(-1)
CH[2 * u + bit] = nxt
u = nxt
# 同一个值后插入的下标更大,直接覆盖即保留"最大下标"→ 同值下子段最短
IDX[u] = j
def query(value: int) -> int:
"""贪心走"异或后该位为 1"的分支,返回与 value 异出最大值的那个前缀下标。"""
u = 0
for k in range(BITS - 1, -1, -1):
bit = value >> k & 1
u = CH[2 * u + (bit ^ 1)] or CH[2 * u + bit] # 对立位存在就走,保证该位异或为 1
return IDX[u]
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
# 前缀异或 b[0]=0 是"空前缀"哨兵:子段 [l,r] 的异或 = b[r] ^ b[l-1]
b = [0] * (n + 1)
for i in range(1, n + 1):
b[i] = b[i - 1] ^ next(data)
insert(0, 0) # 哨兵空前缀:值 0、下标 0(对应起点 1)
best_v, best_l, best_r = -1, 1, 1
for r in range(1, n + 1):
j = query(b[r]) # 只查 j < r,所以先查再插
cand = b[r] ^ b[j]
if cand > best_v: # 严格大于:并列时先保留最早结尾,同结尾由叶子的最大下标保最短
best_v, best_l, best_r = cand, j + 1, r
insert(b[r], r)
print(best_v, best_l, best_r)
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* 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-06 13:28
* update_at: 2026-10-06 13:28
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 100005;
const int BITS = 21; // 数值范围 0..2^21-1,从第 20 位往低位决策
const int MAXNODE = MAXN * (BITS + 1) + 5; // 每个前缀最多新增 BITS+1 个结点
ll n; // 奶牛数量
ll pre[MAXN]; // pre[i] = a[1]^...^a[i];pre[0]=0 是空前缀哨兵,对应起点 1
int child[MAXNODE * 2]; // 01 字典树孩子表:结点 u 的 0/1 孩子是 child[2u]/child[2u+1],0 表示空
ll leaf_id[MAXNODE]; // 叶子登记该前缀值已插入的最大下标,非叶子恒为 -1
int node_cnt; // 已用结点数,结点 0 作根
// 把前缀值 val 插入字典树,并在叶子登记下标 j;同值重复插入时覆盖为更大的 j。
void trie_insert(ll val, ll j) {
int u = 0;
for (int k = BITS - 1; k >= 0; k--) {
ll b = (val >> k) & 1;
int v = child[2 * u + b];
if (v == 0) {
v = node_cnt;
node_cnt++;
child[2 * v] = 0;
child[2 * v + 1] = 0;
leaf_id[v] = -1;
child[2 * u + b] = v;
}
u = v;
}
leaf_id[u] = j; // 后插入的下标更大,直接覆盖即保留最大下标 -> 同值下子段最短
}
// 贪心走“异或后该位为 1”的分支,返回与 val 异或最大的那个前缀下标。
ll trie_query(ll val) {
int u = 0;
for (int k = BITS - 1; k >= 0; k--) {
ll b = (val >> k) & 1;
int other = child[2 * u + (b ^ 1)];
if (other != 0) {
u = other; // 反向位存在就走,该位异或得 1
} else {
u = child[2 * u + b]; // 反向位不存在只能顺走,该位异或得 0
}
}
return leaf_id[u];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
pre[0] = 0;
for (ll i = 1; i <= n; i++) {
ll x;
cin >> x;
pre[i] = pre[i - 1] ^ x;
}
// 建树:清空根,插入空前缀哨兵(值 0、下标 0)。
child[0] = 0;
child[1] = 0;
leaf_id[0] = -1;
node_cnt = 1;
trie_insert(0, 0);
ll best_v = -1; // 最大异或值
ll best_l = 1; // 最优子段起点
ll best_r = 1; // 最优子段终点
for (ll r = 1; r <= n; r++) {
ll j = trie_query(pre[r]); // 先查后插,保证 j < r、子段非空
ll cand = pre[r] ^ pre[j];
if (cand > best_v) { // 严格大于:并列时保留最早结尾
best_v = cand;
best_l = j + 1;
best_r = r;
}
trie_insert(pre[r], r);
}
cout << best_v << ' ' << best_l << ' ' << best_r << '\n';
return 0;
}实现要点:
- Trie 用两个
array('i')平坦存储:CH每结点两个孩子槽(CH[2u]/CH[2u+1]),IDX叶子登记该值最大下标,按需扩容——避免 Python 对象开销撑爆 128MB 内存。 CH[2 * u + (bit ^ 1)] or CH[2 * u + bit]一行完成"反向位存在就走、否则顺走"的贪心。- 叶子
IDX[u] = j覆盖式登记承担"同值取最大→ 最短子段";主循环 cand > best_v严格大于才更新承担"最早结尾";query在insert之前承担"子段非空"。三处对应题目的三层规则。 b(即正文的)以 b[0] = 0作空前缀哨兵,在主循环前单独insert(0, 0),对应起点 1。
复杂度
- 时间:
, 。 个前缀各一次 查询加一次 插入,合计约 层操作( )。与 C++ 正解(前缀异或 + 01-Trie)同阶。 - 空间:
结点上界,实际共享前缀远少于 ; array('i')每结点 8 字节孩子槽加 4 字节下标,实测峰值约 40MB,低于 128MB 限制。 - 实测:数据仓全部 20 组真实数据(
从 1 到 )输出全部一致,单组最大约 0.35s、峰值约 40MB。OJ 限时 1s,Python 在本地较快机器上 0.35s、OJ 上有 TLE 风险,属 python-oj-short 允许范围;算法复杂度与标准 C++ 正解同阶,不是暴力。
总结
- 前缀异或把"选子段"化为"选配对":
,空前缀哨兵 对应起点 1,必须最先插入。 - 01-Trie 贪心用
回答"与历史前缀的最大异或":高位收益 大于所有低位之和,反向位存在就走;先查后插保证 、子段非空。 - 平局三规则各有归宿:从左到右扫描 + 严格大于更新 → 最早结尾;叶子同值覆盖保最大
→ 同结尾最短;插入顺序 → 非空。 - 边界:
时只有哨兵可配对;前缀值可重复(样例 ),靠"叶子保最大下标"正确处理,这正是本题区别于裸"最大异或对"的细节。