[Luogu 10856]【MX-X2-T5】「Cfz Round 4」Xor-Forces 题解

Xor-Forces

简称释义:MX - 梦熊信息学联盟,Cfz - Coffee_zzz。

场上算错了复杂度,自己把自己的可持久化线段树的正解给叉掉了。

对于复杂度不是很好算的题目,要认真算。像这样也就一百来行的代码,思路又已经证明是正确的,可以把它实现出来,再拿极限数据去测。

问题重述

原题面已足够清晰,抄录如下:

给定一个长度为 n = 2 k n=2^k n=2k 的数组 a a a,下标从 0 0 0 开始,维护 m m m 次操作:

  1. 操作一:给定 x x x,设数列 a ′ a' a 满足 a i ′ = a i ⊕ x a'_i=a_{i\oplus x} ai=aix,将 a a a 修改为 a ′ a' a。其中 ⊕ \oplus 表示按位异或运算。
  2. 操作二:给定 l , r l,r l,r,查询 a a a 的下标在 l , r l,r l,r 之间的子数组有多少颜色段。不保证 l ≤ r \bm {l\le r} lr,若 l > r \bm{l > r} l>r,请自行交换 l , r \bm{l,r} l,r

其中,一个极长的所有数都相等的子数组称为一个颜色段。

对于 T = 1 T=1 T=1 的测试点,要求强制在线。


对于所有测试数据, T ∈ { 0 , 1 } T \in \{ 0, 1 \} T{0,1} 0 ≤ k ≤ 18 0\le k\le 18 0k18 n = 2 k n=2^k n=2k 1 ≤ m ≤ 2 × 1 0 5 1\le m\le 2\times 10^5 1m2×105 1 ≤ a i ≤ n 1\le a_i\le n 1ain o p ∈ { 1 , 2 } \mathit{op} \in \{ 1, 2 \} op{1,2} 0 ≤ x , l , r < n 0\le x,l,r < n 0x,l,r<n

  • Subtask 1(15 pts): T = 1 T=1 T=1 k ≤ 10 k\le 10 k10 m ≤ 1 0 3 m\le 10^3 m103
  • Subtask 2(15 pts): T = 1 T=1 T=1,不存在操作一。
  • Subtask 3(20 pts): T = 1 T=1 T=1,对于所有操作二,要么 l = 0 , r = n − 1 l=0,r=n-1 l=0,r=n1,要么 l = n − 1 , r = 0 l=n-1,r=0 l=n1,r=0
  • Subtask 4(20 pts): T = 0 T=0 T=0
  • Subtask 5(30 pts): T = 1 T=1 T=1

部分分

Subtask 1,暴力计算即可。
Subtask 2,可以使用线段树进行维护。

当然其实显然也可以 O ( n ) O(n) O(n),不过 O ( n ) O(n) O(n) 做法对正解的启发性似乎不大。

线段树节点需要维护的信息以及合并方式如下:

struct Data {
    int l, r, val; // 最左端的值,最右端的值,不同颜色段的数目
    Data(int a=0, int b=0, int c=0): l(a), r(b), val(c) {}
    Data operator+(const Data &x) { // 合并两个线段的值
        return {l, x.r, val + x.val - (r==x.l)};
    }
};

Subtask 3 & 4 可以参考官方题解。

正解:可持久化线段树

考虑可持久化,对每一个 x x x 0 ≤ x < 2 k 0 \le x < 2^k 0x<2k)建立一个主席树的根节点,分别初始下标 xor x x x 得到的新数组的信息。

尝试:对所有的初始下标 xor 同一个数 x x x。当 k = 4 k=4 k=4 x = 10 = ( 1010 ) 2 x = 10 = (1010)_2 x=10=(1010)2 时:

原下标:	0		1		2		3		4		5		6		7
二进制:	0000	0001	0010	0011	0100	0101	0110	0111
新下标:	10		11		8		9		14		15		12		13
二进制:	1010	1011	1000	1001	1110	1111	1100	1101
原下标:	8		9		10		11		12		13		14		15
二进制:	1000	1001	1010	1011	1100	1101	1110	1111
新下标:	2		3		0		1		6		7		4		5
二进制:	0010	0011	0000	0001	0110	0111	0100	0101

如果当前线段左端点为 l l l,右端点为 r r r m i d = ( l + r ) / 2 mid = (l+r)/2 mid=(l+r)/2 l e n = r − l + 1 len = r-l+1 len=rl+1(显然这是一棵满二叉树, l e n len len 2 2 2 的自然数次幂),观察发现:

  • x and ⁡   ( l e n / 2 ) = 0 x \operatorname{and}\ (len/2) = 0 xand (len/2)=0,则左子树对应原来下标的 [ l , m i d ] [l, mid] [l,mid],右子树对应原来下标的 [ m i d + 1 , r ] [mid+1, r] [mid+1,r]
  • x and ⁡   ( l e n / 2 ) = 1 x \operatorname{and}\ (len/2) = 1 xand (len/2)=1,则左子树对应原来下标的 [ m i d + 1 , r ] [mid+1, r] [mid+1,r],右子树对应原来下标的 [ l , m i d ] [l, mid] [l,mid],恰好与前一种情况相反。

因为位运算中,每一位之间独立,所以如果二进制下 x 1 x_1 x1 x 2 x_2 x2 的后 k k k 位相同,那么相应会有 k k k非叶子节点的子树形态相同。那么形态相同的这些部分就可以发挥可持久化的作用。

先建立 x = 0 x=0 x=0,即原始数组的线段树。

∀ x > 0 \forall x > 0 x>0,找到 x x x 二进制下非 0 0 0 的最高位,将这一位设为 0 0 0 之后,就能得到我们需要从这里转移的蓝本。

例如 x = ( 01011 ) 2 x = (01011)_2 x=(01011)2,它应从 x ′ = ( 00011 ) 2 x' = (00011)_2 x=(00011)2 转移,因为两者的后 3 3 3 位都是 011 011 011,对应的子树形态相同,应当被重复利用。只有在从后往前第 4 4 4 位开始,才需要发生改变。

转移的时候从原来的根节点向下递归,第一次 x and ⁡   ( l e n / 2 ) = 1 x \operatorname{and}\ (len/2) = 1 xand (len/2)=1 时(此时就是 x x x 的最高位),在此建立新节点,其左子树是原节点的右子树,右子树时原节点的左子树,然后合并左右子信息,返回当前的新节点。

建立可持久化线段树之后,查询就变得十分简单了。

复杂度分析

查询 O ( m log ⁡ n ) O(m \log n) O(mlogn) 显然。

建立可持久化线段树的时候,如果线段长度为 2 x 2^x 2x,那么有 2 k / 2 x = 2 k − x 2^k/2^x = 2^{k-x} 2k/2x=2kx 个这么长的线段。同时经过 xor 操作,区间 [ l , l + 2 x − 1 ] [l, l+2^x-1] [l,l+2x1] 会有 2 x 2^x 2x 种不同可能的情况,那么当前层一共有 2 k − x × 2 x = 2 k 2^{k-x} \times 2^x = 2^k 2kx×2x=2k 个维护不同信息的线段。包括叶子节点,一共有 ( k + 1 ) (k+1) (k+1) 层,所以建立主席树的过程,时间和空间复杂度均为 O ( n log ⁡ n ) O(n \log n) O(nlogn),总复杂度 O ( ( n + m ) log ⁡ n ) O((n+m) \log n) O((n+m)logn),可以通过此题。

代码

// persistent segment tree
#include <cstdio>
#include <algorithm>
#include <cmath>

using namespace std;

const int MAXN = (1<<18)+5; // 一定要加括号

struct Data {
    int l, r, val;
    Data(int a=0, int b=0, int c=0): l(a), r(b), val(c) {}
    Data operator+(const Data &x) {
        return {l, x.r, val + x.val - (r==x.l)};
    }
};

int n, m, k, a[MAXN];



class PSTree { // 数据结构封装成类,也可以不写
    private:
    Data s[MAXN<<5];
    int lc[MAXN<<5], rc[MAXN<<5], tot;
    int root[MAXN];

    int newNode(Data x) {
        s[++tot] = x;
        lc[tot] = rc[tot] = 0;
        return tot;
    }

    int build(int a[], int l, int r) {
        if (l == r) {
            int cur = newNode({a[l], a[r], 1});
            return cur;
        }
        int mid = (l + r) >> 1;
        int cur = newNode(Data());
        lc[cur] = build(a, l, mid);
        rc[cur] = build(a, mid+1, r);
        s[cur] = s[lc[cur]] + s[rc[cur]]; // 不要丢 pushup!
        return cur;
    }

    int update(int last, int l, int r, int x) {
        int mid = (l + r) >> 1, len = r - l + 1;
        int cur = newNode(Data());
        if (x & (len >> 1)) { // 在这里可持久化
            lc[cur] = rc[last], rc[cur] = lc[last];
        } else {
            lc[cur] = update(lc[last], l, mid, x);
            rc[cur] = update(rc[last], mid+1, r, x);
        }
        s[cur] = s[lc[cur]] + s[rc[cur]]; // 不要丢 pushup!
        return cur;
    }

    Data query(int cur, int l, int r, int L, int R) {
        if (L <= l && R >= r) return s[cur];
        int mid = (l + r) >> 1;
        if (R <= mid) {
            return query(lc[cur], l, mid, L, R);
        } else if (L > mid) {
            return query(rc[cur], mid+1, r, L, R);
        } else {
            return query(lc[cur], l, mid, L, R) + query(rc[cur], mid+1, r, L, R);
        }
    }

    public:
    void init(int a[]) {
        tot = 0;
        root[0] = build(a, 0, n-1);
        for (int i = 1; i < n; ++i) {
            int k = log(i) / log(2); // 强制转换的时候向下取整,刚好符合要求
            root[i] = update(root[i^(1<<k)], 0, n-1, i);
        }
    }

    Data query(int L, int R, int x) {
        return query(root[x], 0, n-1, L, R);
    }
} segTree;

int main() {
    int T;
    scanf("%d%d%d", &T, &k, &m);
    n = 1<<k;
    for (int i = 0; i < n; ++i) {
        scanf("%d", &a[i]);
    }
    segTree.init(a);
    int v = 0, lst = 0;
    while (m--) {
        int op;
        scanf("%d", &op);
        if (op == 1) {
            int x;
            scanf("%d", &x);
            x ^= T * lst;
            v ^= x; // 利用了 xor 的结合律
        } else {
            int l, r;
            scanf("%d%d", &l, &r);
            l ^= T * lst, r ^= T * lst;
            if (l > r) swap(l, r);
            lst = segTree.query(l, r, v).val;
            printf("%d\n", lst);
        }
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值