XOR Sorting (Hard Version)
XOR Sorting (Hard Version)
二进制分块,线段树
1. 操作的本质
数组下标从 \(0\) 开始。一次操作允许交换 \(i,j\),当且仅当:
其中 \(k\) 是当前允许的最大异或值。
关键结论:任意一次交换,即使 \(i \oplus j\) 不是 \(2\) 的幂,也可以分解为若干次异或值为 \(2^p\) 的交换,并且分解过程中用到的异或值都不超过原来的 \(i \oplus j\)。
因此,判断数组能否用参数 \(k\) 排序,等价于判断能否只用异或值为 \(2^p\) 且 \(2^p \le k\) 的交换来排序。
2. 给定 \(k\) 时的分块结构
设:
即:
对于任意两个下标 \(i,j\),如果它们在二进制第 \(p+1\) 位或更高位上有任何一位不同,则:
因此这样的两个位置永远不能直接交换,也不能通过中间交换跨越这条边界。
于是数组被强制划分为大小为 \(2^{p+1}\) 的连续块:
每个块内部所有下标的高位相同,块间下标高位不同,异或值至少为 \(2^{p+1}>k\)。
所以元素只能在自己所在的块内移动。
块内p+1位必定相同,不同块p+1必定不同,所以块合并后最小的k依然要是2的幂次
3. 块内可以任意排列
在一个大小为 \(2^{p+1}\) 的块内,任意两个下标的异或值都小于 \(2^{p+1}\)。
而我们可以使用步长为:
的交换,这些值都满足:
因此全部合法。这些步长在块内生成的图是连通的,所以块内元素可以任意重排。
4. 排序的充要条件
既然块内可以任意排列,那么整个数组能够排序,当且仅当:
把每个块内部排好序后,整个数组非递减。
等价于检查每一对相邻块:
如果所有相邻块都满足该条件,则整体可以排序;否则不行。
5. 为什么答案一定是 \(0\) 或 \(2^p\)
可行性关于 \(k\) 单调:\(k\) 越大,允许的交换越多,能完成的事情只增不减。
当 \(k\) 落在区间:
时,\(\lfloor \log_2 k \rfloor=p\),块大小始终为 \(2^{p+1}\),块划分和块内可达性都不变。
所以这个区间内所有 \(k\) 的可行性完全相同。
因此只需要检查代表值 \(k=2^p\)。
如果 \(2^p\) 可行,则最小可行 \(k\) 就是 \(2^p\);如果不可行,整个区间都不可行。
所以答案一定形如 \(0\) 或 \(2^p\)。
6. 困难版本:动态维护所有块大小
简单版本可以枚举 \(p\),每次按固定块大小 \(2^{p+1}\) 检查相邻块。
困难版本有 \(q\) 次点更新,需要在每次更新后快速回答最小 \(k\)。
做法是把数组末尾补若干个 \(\infty\),使长度 \(N\) 成为 \(2\) 的幂。
建立线段树,每个节点对应一个长度为 \(2\) 的幂的连续区间。
线段树的每一层,正好对应一种块大小 \(2^{p+1}\)。
7. 线段树节点维护的信息
每个节点维护三个值:
- \(mn\):该区间的最小值;
- \(mx\):该区间的最大值;
- \(need\):仅在该区间内部排序所需的最小 \(k\)。
叶子节点长度为 \(1\):
8. 合并规则与 \(k\) 的推导
设当前节点长度为 \(len\),左儿子为 \(L\),右儿子为 \(R\)。
因为补成了 \(2\) 的幂,所以:
左右儿子长度都是:
先继承子节点的需求:
然后判断是否需要跨过中点交换:
如果成立,说明左半部分的最大值大于右半部分的最小值,左半的大元素必须排到右半小元素的后面,因此必须允许元素跨过左右分界线。
左半和右半的下标在二进制第 \(p\) 位上不同,所以任何跨中点的交换都满足:
因此要允许跨中点交换,最小需要的 \(k\) 是:
所以当 \(L.mx>R.mn\) 时:
注意这里比较的是左儿子的最大值 \(L.mx\) 和右儿子的最小值 \(R.mn\),而不是整个区间的 \(mn,mx\)。
9. 根节点就是答案
根节点覆盖整个数组,它的 \(need\) 就是使整个数组有序所需的最小 \(k\),即:
由于合并过程中每次提升用的都是 \(len/2\),而 \(len\) 是 \(2\) 的幂,所以每个节点的 \(need\) 一定是 \(0\) 或 \(2\) 的幂。
因此根节点的 \(need\) 自然就是最小可行的 \(k\),不需要再单独枚举 \(p\)。
10. 更新操作
每次更新位置 \(pos\) 的值为 \(x\):
- 递归到对应叶子,令 \(mn=mx=x,\ need=0\);
- 回溯时对路径上的每个父节点重新执行合并规则;
- 根节点的 \(need\) 就是新答案。
这就是普通的线段树点更新。
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = 4e18;
struct Node {
ll mn, mx, need;
};
vector<Node> seg;
vector<ll> a;
int N;
Node mergeNode(const Node& L, const Node& R, int len) {
Node res;
res.mn = min(L.mn, R.mn);
res.mx = max(L.mx, R.mx);
res.need = max(L.need, R.need);
if(L.mx > R.mn) {
res.need = max(res.need, (ll)(len / 2));
}
return res;
}
void build(int node, int l, int r) {
if(l == r) {
seg[node] = {a[l], a[l], 0};
return;
}
int mid = (l + r) >> 1;
build(node << 1, l, mid);
build(node << 1 | 1, mid + 1, r);
seg[node] = mergeNode(seg[node << 1], seg[node << 1 | 1], r - l + 1);
}
void update(int node, int l, int r, int pos, ll val) {
if(l == r) {
seg[node] = {val, val, 0};
a[pos] = val;
return;
}
int mid = (l + r) >> 1;
if(pos <= mid) update(node << 1, l, mid, pos, val);
else update(node << 1 | 1, mid + 1, r, pos, val);
seg[node] = mergeNode(seg[node << 1], seg[node << 1 | 1], r - l + 1);
}
void solve() {
int n, q; cin >> n >> q;
N = 1;
while(N < n) N <<= 1;
a.assign(N, INF);
for(int i = 0; i < n; i ++) cin >> a[i];
seg.assign(4 * N, {0, 0, 0});
build(1, 0, N - 1);
cout << seg[1].need << "\n";
for(int i = 0; i < q; i++) {
int pos; ll x; cin >> pos >> x;
update(1, 0, N - 1, pos, x);
cout << seg[1].need << "\n";
}
}
int main() {
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int _ = 1; cin >> _;
while(_--) solve();
return 0;
}

浙公网安备 33010602011771号