CF617E XOR and Favorite Number 题解

想了好久才明白zz

来源?:莫队题单


题目大意

给定一个长度为 \(n\) 的序列 \(a\),然后再给一个数字 \(k\),再给出 \(m\) 组询问,每组询问给出一个区间,求这个区间里面有多少个子区间的异或值为 \(k\)

  • \(1 \le n,m \le 10^5\)
  • \(0 \le k,a_i \le 10^6\)
  • \(1 \le l_i,r_i \le n\)

题目分析

读完题后我是一脸懵的qwq

不会异或!!
不会查询!!

好,那就不要异或也不要查询!!(⁠・⁠o⁠・⁠)


简化 1

给定一个长度为 \(n\) 的序列 \(a\),求这个序列的和。

这也太简了

直接算 \(\sum\limits^{n}_{i=1} a_i\) 即可!


简化 2

给定一个长度为 \(n\) 的序列 \(a\),给出 \(m\) 组询问,每组询问给出一个区间,求这个区间的和。

参考:P8218 【深进1.例1】求区间和

蓝题变橙题

首先当然可以直接模拟,每次询问计算 \(\sum\limits_{i=l}^{r} a_i\)。可是时间复杂度 \(O(nm)\),不理想。

那么优化就是...

前缀和!

首先设 \(a_0=0\),因为无修改所以可以开一个数组 \(\text{pf}\) 记录 \(a\) 的前缀和。设

\[\text{pf}_x=\sum^{x}_{i=0} a_i \]

我们有

\[\begin{align*} \sum^{r}_{i=l}a_i&=\sum^{r}_{i=0}a_i-\sum^{l-1}_{i=0}a_i\\ &= \text{pf}_r-\text{pf}_{l-1} \end{align*} \]

时间复杂度:\(O(n)\) 预处理,\(O(1)\) 查询


简化 3

给定一个长度为 \(n\) 的序列 \(a\),给出 \(m\) 组询问,每组询问给出一个区间,求这个区间的异或值。

参考:Leetcode 1310. 子数组异或查询

首先定义 \(\oplus\) 为异或符号,\(\bigoplus\limits_{i=l}^{r}a_i=a_l \oplus a_{l+1} \oplus \dots \oplus a_{r-1} \oplus a_r\)

发现这题与上一题非常相似,考虑是否可以利用同样的思想。

考虑 \(a_0\) 的值。当我们在求和时,我们设 \(a_0=0\)。 为什么?

是因为我们知道对于任意 \(x\)\(x+0=x\) 成立。在这里我们称 \(0\) 为加法的单位元。

同理对于任意 \(x\)\(x \times 1=x\) 成立。所以 \(1\) 为乘法的单位元。

那么异或呢?对于任意 \(x\)\(x \oplus 0=x\) 成立。所以 \(0\) 为异或的单位元。

所以我们同样设 \(a_0=0\)

继续开一个数组 \(\text{pf}\) 记录 \(a\) 的前缀异或。设

\[\text{pf}_x=\bigoplus\limits_{i=0}^{x} a_i \]

要如何得到区间异或呢?同样先从和观察。

\[\begin{align*} \sum_{i=0}^{l-1}a_i + \sum_{i=l}^{r}a_i &= \sum_{i=0}^{r}a_i\\ \sum_{i=l}^{r}a_i &= \sum_{i=0}^{r}a_i - \sum_{i=0}^{l-1}a_i\\ &= \text{pf}_r - \text{pf}_{l-1} \end{align*} \]

注意到第二步我们将 \(+\) 号变成了 \(-\) 号,为什么?

因为减法是加法的逆运算!对于任意 \(u,v,w\) 满足 \(u+v=w\)\(u=w-v\) 成立。

那么异或呢?观察发现异或的逆运算是异或!

也就是对于任意 \(u,v,w\) 满足 \(u \oplus v = w\)\(u = w \oplus v\) 成立。

因为异或满足交换律,所以 \(u = v \oplus w\) 也成立。

回到前缀异或。现在我们有

\[\begin{align*} \bigoplus_{i=0}^{l-1}a_i \oplus \bigoplus_{i=l}^{r}a_i &= \bigoplus_{i=0}^{r}a_i\\ \bigoplus_{i=l}^{r}a_i &= \bigoplus_{i=0}^{r}a_i \oplus \bigoplus_{i=0}^{l-1}a_i\\ &= \text{pf}_r \oplus \text{pf}_{l-1} \end{align*} \]

时间复杂度:\(O(n)\) 预处理,\(O(1)\) 查询


简化 4

给定一个长度为 \(n\) 的序列 \(a\),然后再给一个数字 \(k\),求这个序列里面有多少个子区间的和为 \(k\)

参考:Leetcode 560. 和为 K 的子数组

这里开始就有一点难度了 主要是我菜

发现可以转化题目为

给定一个长度为 \(n\) 的序列 \(a\),然后再给一个数字 \(k\),求有多少个 \(l,r\) 满足 \(\sum\limits_{i=l}^{r} a_i=k\)

直接遍历可得 \(O(n^3)\) 解法

int ans=0;
for(int len=1;len<=n;len++){
    for(int l=0;l<=n-len;l++){
        int r=l+len-1,sum=0;
        for(int i=l;i<=r;i++){
            sum+=a[i];
        }
        if(sum==k) ans++;
    }
}
时间复杂度证明 $$ \begin{align*} \sum_{L=1}^{n}\sum_{l=0}^{n-L}\sum_{i=l}^{l+L-1} 1 &= \sum_{L=1}^{n}\sum_{l=0}^{n-L} L\\ &= \sum_{L=1}^{n} (L(n-L+1))\\ &= \sum_{L=1}^{n} (nL-L^2+L)\\ &= \sum_{L=1}^{n} nL - \sum_{L=1}^{n} L^2 + \sum_{L=1}^{n} L\\ &= n \left(\sum_{L=1}^{n} L\right) + \sum_{L=1}^{n} L - \sum_{L=1}^{n} L^2\\ &= (n+1)\left(\sum_{L=1}^{n} L\right) - \sum_{L=1}^{n} L^2\\ &= (n+1)\left(\dfrac{n(n+1)}{2}\right) - \dfrac{n(n+1)(2n+1)}{6}\\ &= \dfrac{1}{2}\left(n^3+2n^2+n\right) - \dfrac{1}{6}\left(2n^3+3n^2+n\right)\\ &= \dfrac{1}{6}\left(3n^3+6n^2+3n\right) - \dfrac{1}{6}\left(2n^3+3n^2+n\right)\\ &= \dfrac{1}{6}\left(n^3+9n^2+4n\right) \end{align*} $$ 可得 $$O\left(\dfrac{1}{6}\left(n^3+9n^2+4n\right)\right) = O(n^3)$$

利用前缀和可以优化成 \(O(n^2)\)

能继续优化吗?

观察我们可以利用简化 2 的思想得到

给定一个长度为 \(n\) 的序列 \(a\),然后再给一个数字 \(k\),求有多少个 \(l,r\) 满足 \(\text{pf}_r-\text{pf}_{l-1}=k\)

\(l \le r\) 必定成立。

遍历 \(r=[1,n]\),发现区间 \([l,r]\) 要满足 \(\text{pf}_r-\text{pf}_{l-1}=k\),必定有 \(\text{pf}_{l-1}=\text{pf}_r-k\)

定义 \(cnt_x\)\(x\) 出现的次数,初始化 \(cnt_x=0\)。因为 \(l \le r\) 成立,所以对于任意 \(r\)\(l-1\) 的取值为 \([0,r-1]\)。因此仅需在查询 \(cnt_{\text{pf}_r-k}\) 后将 \(cnt_{\text{pf}_r}\) 的值加上 \(1\) 即可。

可得答案为

\[\sum_{r=0}^{n}cnt_{\text{pf}_r-k} \]

需要注意的是 \(a_i\) 的大小。当

  • \(1 \le n \le 10^5\)
  • \(0 \le a_i \le 10^6\)

\(\text{pf}_i\) 的最大值为 \(10^5 \times 10^6 = 10^{11}\),会 MLE。可以用 STL map 来维护,单次查询 \(O(\log n)\)

为什么不用 unordered_map 有机会被卡成单次 $O(n)$,总 $O(n^2)$

详情见 Blowing up unordered_map, and how to stop getting hacked on it

总时间复杂度:\(O(n \log n)\)


简化 5

给定一个长度为 \(n\) 的序列 \(a\),然后再给一个数字 \(k\),求这个序列里面有多少个子区间的异或值为 \(k\)

合并简化 3 和简化 4 的结论,答案就是

\[\sum_{r=0}^{n}cnt_{\text{pf}_{r} \oplus k} \]

如果明白可以跳过这个 利用简化 3 转化题目为

给定一个长度为 \(n\) 的序列 \(a\),然后再给一个数字 \(k\),求有多少个 \(l,r\) 满足 \(\text{pf}_r \oplus \text{pf}_{l-1}=k\)

\(l \le r\) 必定成立。

遍历 \(r=[1,n]\),发现区间 \([l,r]\) 要满足 \(\text{pf}_r \oplus \text{pf}_{l-1}=k\),必定有 \(\text{pf}_{l-1}=\text{pf}_r \oplus k\)

定义 \(cnt_x\)\(x\) 出现的次数,初始化 \(cnt_x=0\)。因为 \(l \le r\) 成立,所以对于任意 \(r\)\(l-1\) 的取值为 \([0,r-1]\)。因此仅需在查询 \(cnt_{\text{pf}_r \oplus k}\) 后将 \(cnt_{\text{pf}_r}\) 的值加上 \(1\) 即可。

可得答案为

\[\sum_{r=0}^{n}cnt_{\text{pf}_r \oplus k} \]


题目解法

经过多轮的简化已经看得出原题了!贴回原题:

给定一个长度为 \(n\) 的序列 \(a\),然后再给一个数字 \(k\),再给出 \(m\) 组询问,每组询问给出一个区间,求这个区间里面有多少个子区间的异或值为 \(k\)

最后要处理的是回答 \(m\) 组询问。终于要请出今天的主角:莫队算法!

不过经费不足所以腰斩了有机会再更

莫队算法可以参考这位dalao的博客:莫队算法——从入门到黑题

利用简化 5 的结论维护数组 \(cnt\)。在移动 \(l,r\) 指针时更新 \(cnt\)。对于区间 \([l,r]\),答案为

\[\sum_{i=l-1}^{r}cnt_{\text{pf}_i \oplus k} \]

注意:因为 \(a_i \le 10^6\),如果用数组维护 \(cnt\),需要将容量设为 \(2^{\lceil\log_2 10^6\rceil}=2^{20}=\) 1<<20


代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll n,m,k;
ll a[100020],pf[100020];
ll sq,bnum;
ll blg[100020],cnt[1<<20];

struct Mo{
    ll l,r,id;
    bool operator <(const Mo &rhs) const{
        return blg[l]==blg[rhs.l] ? r<rhs.r : l<rhs.l;
    }
};

Mo query[100020];
ll ans=0;

void add(ll x){
    ans+=cnt[pf[x]^k];
    cnt[pf[x]]++;
}

void del(ll x){
    cnt[pf[x]]--;
    ans-=cnt[pf[x]^k];
}

int main() {
    cin>>n>>m>>k;
    a[0]=0;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    pf[0]=a[0];
    for(int i=1;i<=n;i++){
        pf[i]=pf[i-1]^a[i];
    }
    sq=sqrt(n);bnum=ceil((double)n/sq);
    for(int i=0;i<=bnum;i++){
        for(int j=0;j<sq;j++){
            if(i*sq+j+1>n)break;
            blg[i*sq+j+1]=i;
        }
    }

    for(int i=0;i<m;i++){
        ll x,y;cin>>x>>y;
        query[i].l=--x;
        query[i].r=y;
        query[i].id=i;
    }
    sort(query,query+m);
    memset(cnt,0,sizeof(cnt));
    vector<ll> output(m);
    ll L=0,R=-1;
    for(int i=0;i<m;i++){
        ll l=query[i].l,r=query[i].r,id=query[i].id;
        while(L<l) del(L++);
        while(L>l) add(--L);
        while(R<r) add(++R);
        while(R>r) del(R--);
        output[id]=ans;
    }

    for(ll x:output)cout<<x<<'\n';
}


后记

是道好题,题解写了好久qwq

如果有任何疑问或错误漏缺欢迎在评论告诉我!

~~~ 完结撒花 ✿✿ヽ(°▽°)ノ✿ ~~~

posted @ 2024-04-11 16:01  lumid  阅读(56)  评论(4)    收藏  举报