XOR Sorting (Hard Version)

XOR Sorting (Hard Version)

二进制分块,线段树

1. 操作的本质

数组下标从 \(0\) 开始。一次操作允许交换 \(i,j\),当且仅当:

\[i \oplus j \le k \]

其中 \(k\) 是当前允许的最大异或值。

关键结论:任意一次交换,即使 \(i \oplus j\) 不是 \(2\) 的幂,也可以分解为若干次异或值为 \(2^p\) 的交换,并且分解过程中用到的异或值都不超过原来的 \(i \oplus j\)。

因此,判断数组能否用参数 \(k\) 排序,等价于判断能否只用异或值为 \(2^p\) 且 \(2^p \le k\) 的交换来排序。


2. 给定 \(k\) 时的分块结构

设:

\[p = \lfloor \log_2 k \rfloor \]

即:

\[2^p \le k < 2^{p+1} \]

对于任意两个下标 \(i,j\),如果它们在二进制第 \(p+1\) 位或更高位上有任何一位不同,则:

\[i \oplus j \ge 2^{p+1} > k \]

因此这样的两个位置永远不能直接交换,也不能通过中间交换跨越这条边界。

于是数组被强制划分为大小为 \(2^{p+1}\) 的连续块:

\[[0,2^{p+1}-1],\ [2^{p+1},2\cdot 2^{p+1}-1],\ \dots \]

每个块内部所有下标的高位相同,块间下标高位不同,异或值至少为 \(2^{p+1}>k\)。

所以元素只能在自己所在的块内移动。

块内p+1位必定相同,不同块p+1必定不同,所以块合并后最小的k依然要是2的幂次


3. 块内可以任意排列

在一个大小为 \(2^{p+1}\) 的块内,任意两个下标的异或值都小于 \(2^{p+1}\)。
而我们可以使用步长为:

\[2^0,2^1,\dots,2^p \]

的交换,这些值都满足:

\[2^p \le k \]

因此全部合法。这些步长在块内生成的图是连通的,所以块内元素可以任意重排。


4. 排序的充要条件

既然块内可以任意排列,那么整个数组能够排序,当且仅当:

把每个块内部排好序后,整个数组非递减。

等价于检查每一对相邻块:

\[\max(\text{左块}) \le \min(\text{右块}) \]

如果所有相邻块都满足该条件,则整体可以排序;否则不行。


5. 为什么答案一定是 \(0\) 或 \(2^p\)

可行性关于 \(k\) 单调:\(k\) 越大,允许的交换越多,能完成的事情只增不减。

当 \(k\) 落在区间:

\[[2^p,2^{p+1}-1] \]

时,\(\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\):

\[mn=mx=a_i,\quad need=0 \]


8. 合并规则与 \(k\) 的推导

设当前节点长度为 \(len\),左儿子为 \(L\),右儿子为 \(R\)。
因为补成了 \(2\) 的幂,所以:

\[len=2^{p+1} \]

左右儿子长度都是:

\[\frac{len}{2}=2^p \]

先继承子节点的需求:

\[mn=\min(L.mn,R.mn) \]

\[mx=\max(L.mx,R.mx) \]

\[need=\max(L.need,R.need) \]

然后判断是否需要跨过中点交换:

\[L.mx > R.mn \]

如果成立,说明左半部分的最大值大于右半部分的最小值,左半的大元素必须排到右半小元素的后面,因此必须允许元素跨过左右分界线。

左半和右半的下标在二进制第 \(p\) 位上不同,所以任何跨中点的交换都满足:

\[i \oplus j \ge 2^p \]

因此要允许跨中点交换,最小需要的 \(k\) 是:

\[2^p=\frac{len}{2} \]

所以当 \(L.mx>R.mn\) 时:

\[need=\max(need,\frac{len}{2}) \]

注意这里比较的是左儿子的最大值 \(L.mx\) 和右儿子的最小值 \(R.mn\),而不是整个区间的 \(mn,mx\)。


9. 根节点就是答案

根节点覆盖整个数组,它的 \(need\) 就是使整个数组有序所需的最小 \(k\),即:

\[f(a)=seg[1].need \]

由于合并过程中每次提升用的都是 \(len/2\),而 \(len\) 是 \(2\) 的幂,所以每个节点的 \(need\) 一定是 \(0\) 或 \(2\) 的幂。
因此根节点的 \(need\) 自然就是最小可行的 \(k\),不需要再单独枚举 \(p\)。


10. 更新操作

每次更新位置 \(pos\) 的值为 \(x\):

  1. 递归到对应叶子,令 \(mn=mx=x,\ need=0\);
  2. 回溯时对路径上的每个父节点重新执行合并规则;
  3. 根节点的 \(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;
}
posted @ 2026-11-03 21:30  thenrify  阅读(3)  评论(0)    收藏  举报