莫队算法:
莫队算法的用处是,对于一个区间内的查询,当我们已经知道了 [ L , R ] 的答案的时候,有莫队算法可以在很短的时间内得到 [ L - 1 , R ] 或者是 [ L ,R + 1 ]的答案,前提是可以离线处理。
步骤:
1.输入 N 个点的时候,对每个点的进行分块 ,一般 N 个点分成 sqrt(n) 块 ,用一个pos[]数组记录分块
int s = sqrt(n);
for(int i = 1;i <= n;i++){
scanf("%d",&a[i]);
a[i] ^= a[i - 1];
pos[i] = i / s; //记录所在分块
}
2.输入m个询问,将它保存下来之后,对它进行排序,按照询问的左端点所在块排序,同块则按右端点排序,都是从小到大
bool cmp(node a,node b){
if(pos[a.l] != pos[b.l]) //两个询问的左极限如果在不同块,那么按分块顺序从小到大,否则按右极限
return pos[a.l] < pos[b.l];
return a.r < b.r;
}
3.开始处理询问,从前往后,一次挪动处理,由于有了前面的排序,后面的移到次数被大大减少了,所以时间很短
然后一边记录答案,最后输出就好。
最简单的例题 : Codeforces 617E
每次询问给出一个区间范围,问区间范围内连续异或值为 k 的子区间有多少个
思路:区间问题,并且可以离线处理,所以莫队是最好选择。
首先要考虑异或的特性 x ^ k = 0 表示x = k ,所以我们可以把区间里的异或值拿去 异或 k,结果为 0 就符和条件
还有就是 如果前面出现异或结果为 y ,而你当前的x ^ k = y 的话,那么表示 x ^ y = k,也满足条件。
还有就是题目要求的是连续的子区间,所以我们得做前缀和才方便处理 . 前缀数组 a[]
开始处理询问,对于每个询问,用 add() 函数 和 dele() 函数去移动到对应的位置
void add(int x){
Ans += flag[a[x] ^ k];
flag[a[x]]++;
}
Ans记录符合子区间个数,flag[x] 存的是已有异或值为x 的区间的个数 ,flag[0] = 1 ,因为x ^ k = 0 表示 x = k
然后 flag 记数 异或值区间 ,当你下一次的异或值 X去 异或 K = Y,而flag[Y]又有值的时候,表示有 flag[Y] 个区间去异或 X 可以等于 K ,表示 [1,a] ^ [1,b] = k ,也就是说 [b,a] = k,又有 flag[Y] 个区间是满足条件,所以计数可以增加flag[y]个。
void dele(int x){
flag[a[x]]--;
Ans -= flag[a[x] ^ k];
}
dele数组就比较简单了,减去它的贡献就好了。
a[x] ^ k = z ,所以 a[x] ^ z = k,减去了 a[x],那么原本 有 flag[z]个区间是满足的,得把它减掉
注意补充:在对左指针进行操作的时候,比如我左指针要右移一位,那么就是要删除当前所在位置的数的贡献值
但是,我们并不能 dele(l++) 而是 dele(l - 1) , l++;
原因是,我们是做了一个 异或前缀和 a[ l ] = x1 ^ x2 ^ ... ^xl ,那么 如果要单找 xl 的话,不是直接 a[ l ] 可以得到的
a[ l ] = a[ l ] ^ a[ l - 1 ] ,也就是说 xl 的贡献是由 a[ l - 1 ] 得来的,所以当我们要删去 x l 的贡献的时候,我们需要dele 的是 a[ l - 1 ] 。
同理,左移的时候要先 l -- 移动到 位置,然后 add(l - 1) 才能把 l 的贡献增加上去
AC代码:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
#define maxn 2000006
#define mem(a,x) memset(a,x,sizeof(a))
#define ll long long
struct node{ //记录询问,离线处理
int l,r,id;
}q[maxn];
int a[maxn],pos[maxn]; // a是前缀数组,pos用于分块
ll ans[maxn],sum[maxn],flag[maxn];
int n,m,k;
ll Ans = 0;
bool cmp(node a,node b){
if(pos[a.l] != pos[b.l]) //两个询问的左极限如果在不同块,那么按分块顺序从小到大,否则按右极限
return pos[a.l] < pos[b.l];
return a.r < b.r;
}
void add(int x){
Ans += flag[a[x] ^ k];
flag[a[x]]++;
}
void dele(int x){
flag[a[x]]--;
Ans -= flag[a[x] ^ k];
}
int main(){
int i;
while(scanf("%d %d %d",&n,&m,&k) != EOF){
Ans = 0;
mem(flag,0);
int s = sqrt(n); //有n个点,分成 sqrt(n)个块
flag[0] = 1; // x 异或 k 之后等于0表示 x 等于 k,记录一个贡献
for(int i = 1;i <= n;i++){
scanf("%d",&a[i]);
a[i] ^= a[i - 1]; //记录异或前缀和
pos[i] = i / s; //记录所在分块
}
for(int i = 1;i <= m;i++){
scanf("%d %d",&q[i].l,&q[i].r);
q[i].id = i;
}
sort(q + 1,q + 1 + m,cmp);
int l = 1,r = 0;
for(int i = 1;i <= m;i++){ //对于每个查询
while(q[i].l < l){ //把左右极限移到对应位置
l--;
add(l - 1);
}
while(q[i].l > l){
dele(l - 1);
l++;
}
while(q[i].r < r){
dele(r);
r--;
}
while(q[i].r > r){
r++;
add(r);
}
ans[q[i].id] = Ans;
}
for(int i = 1;i <= m;i++)
printf("%lld\n",ans[i]);
}
return 0;
}