优雅的暴力——莫队算法学习笔记(超详细)
博客园食用更佳。
一、问题引入
当有一道题每次问你一个区间的某某东西,而这个东西不可合并(不可线段树)不可差分(不可树状数组)分块后不好合并/无法合并(不可分块),但是支持离线并且任意一个区间能通过扩张和收缩得到其它区间的答案,那么这道题就可以莫队。
二、什么是莫队
首先,如果有一道区间可扩张收缩的题(不需要支持离线),你是不是可以先求出第一个区间的答案,然后通过左右端点的扩张和收缩求出其它区间的答案,但是你会发现最坏时间复杂度跟暴力的
三、如何证明莫队时间复杂度
首先你会发现左指针单次移动如果是在同一个块,那么单次移动时间复杂度为
四、莫队板子
#include<bits/stdc++.h>
using namespace std;
const int N = 5e4+5;//可自由变动
struct node
{
int l;
int r;
int id;
}b[N];
int a[N];
int len;
int cmp(node x,node y)
{
int idx = (x.l-1)/len+1,idy = (y.l-1)/len+1;
return idx == idy?x.r<y.r:idx<idy;
}
int sum;
void add(int x)
{
//扩张区间
}
void del(int x)
{
//收缩区间
}
int ans[N];
signed main()
{
int n,_;
scanf("%d %d",&n,&_);
len = sqrt(n);
for(int i = 1;i<=n;i++)
{
scanf("%d",&a[i]);
}
for(int i = 1;i<=_;i++)
{
scanf("%d %d",&b[i].l,&b[i].r);
b[i].id = i;
}
sort(b+1,b+_+1,cmp);
for(int i = 1,l = 1,r = 0;i<=_;i++)//至于这里为什么l = 1,r = 0是因为老师说这样可以防止迷之错误
{
while(l>b[i].l)//老师还说一定要先扩张再收缩,可以防止l>r的情况
{
add(--l);
}
while(r<b[i].r)
{
add(++r);
}
while(l<b[i].l)
{
del(l++);
}
while(r>b[i].r)
{
del(r--);
}
ans[b[i].id] = sum;
}
return 0;
}
注意:这只是一个板子,里面的内容可以随机应变。
五、莫队习题讲解
P3901 数列找不同
很基础的一个莫队,把莫队板子套过来填上
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
struct node
{
int l;
int r;
int id;
}b[N];
int a[N];
int tong[N];
int len;
int cmp(node x,node y)
{
int idx = (x.l-1)/len+1,idy = (y.l-1)/len+1;
return idx == idy?x.r<y.r:idx<idy;
}
int sum;
void add(int x)
{
if(tong[a[x]] == 1)
{
sum++;
}
tong[a[x]]++;
}
void del(int x)
{
tong[a[x]]--;
if(tong[a[x]] == 1)
{
sum--;
}
}
int ans[N];
signed main()
{
int n,_;
scanf("%d %d",&n,&_);
len = sqrt(n);
for(int i = 1;i<=n;i++)
{
scanf("%d",&a[i]);
}
for(int i = 1;i<=_;i++)
{
scanf("%d %d",&b[i].l,&b[i].r);
b[i].id = i;
}
sort(b+1,b+_+1,cmp);
for(int i = 1,l = 1,r = 0;i<=_;i++)
{
while(l>b[i].l)
{
add(--l);
}
while(r<b[i].r)
{
add(++r);
}
while(l<b[i].l)
{
del(l++);
}
while(r>b[i].r)
{
del(r--);
}
ans[b[i].id] = sum;
}
for(int i = 1;i<=_;i++)
{
printf("%s\n",ans[i]?"No":"Yes");
}
return 0;
}
AT_abc293_g [ABC293G] Triple Index
比上一道题稍难一点,我们考虑
十年 OI 一场空,不开 long long 见祖宗!
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e5+5;
struct node
{
int l;
int r;
int id;
}b[N];
int a[N];
int tong[N];
int len;
int cmp(node x,node y)
{
int idx = (x.l-1)/len+1,idy = (y.l-1)/len+1;
return idx == idy?x.r<y.r:idx<idy;
}
int sum;
void add(int x)
{
sum+=tong[a[x]]*(tong[a[x]]-1)/2;
tong[a[x]]++;
}
void del(int x)
{
tong[a[x]]--;
sum-=tong[a[x]]*(tong[a[x]]-1)/2;
}
int ans[N];
signed main()
{
int n,_;
scanf("%lld %lld",&n,&_);
len = sqrt(n);
for(int i = 1;i<=n;i++)
{
scanf("%lld",&a[i]);
}
for(int i = 1;i<=_;i++)
{
scanf("%lld %lld",&b[i].l,&b[i].r);
b[i].id = i;
}
sort(b+1,b+_+1,cmp);
for(int i = 1,l = 1,r = 0;i<=_;i++)
{
while(l>b[i].l)
{
add(--l);
}
while(r<b[i].r)
{
add(++r);
}
while(l<b[i].l)
{
del(l++);
}
while(r>b[i].r)
{
del(r--);
}
ans[b[i].id] = sum;
}
for(int i = 1;i<=_;i++)
{
printf("%lld\n",ans[i]);
}
return 0;
}
P1494 [国家集训队] 小 Z 的袜子
上一道题的弱化版,似乎没什么好说的,就是三元组换成了二元组。 依旧是……十年 OI 一场空,不开 long long 见祖宗!
我们是要拿总数除以可能搭配的二元组数量,所以如果你依旧只使用 不然就会像我一样调一小时……
千万不要忘了题目中的
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5e4+5;
struct node
{
int l;
int r;
int id;
}b[N];
int a[N];
int tong[N];
int len;
int cmp(node x,node y)
{
int idx = (x.l-1)/len+1,idy = (y.l-1)/len+1;
return idx == idy?x.r<y.r:idx<idy;
}
int sum;
void add(int x)
{
sum+=tong[a[x]];
tong[a[x]]++;
}
void del(int x)
{
tong[a[x]]--;
sum-=tong[a[x]];
}
int ans[N];
int num[N];
signed main()
{
int n,_;
scanf("%lld %lld",&n,&_);
len = sqrt(n);
for(int i = 1;i<=n;i++)
{
scanf("%lld",&a[i]);
}
for(int i = 1;i<=_;i++)
{
scanf("%lld %lld",&b[i].l,&b[i].r);
b[i].id = i;
}
sort(b+1,b+_+1,cmp);
for(int i = 1,l = 1,r = 0;i<=_;i++)
{
while(l>b[i].l)
{
add(--l);
}
while(r<b[i].r)
{
add(++r);
}
while(l<b[i].l)
{
del(l++);
}
while(r>b[i].r)
{
del(r--);
}
ans[b[i].id] = sum;
num[b[i].id] = i;
}
for(int i = 1;i<=_;i++)
{
if(b[num[i]].l == b[num[i]].r)//记得这里很重要,不然就10分
{
printf("0/1\n");
continue;
}
int yue = __gcd(ans[i],(b[num[i]].r-b[num[i]].l+1)*(b[num[i]].r-b[num[i]].l)/2);
printf("%lld/%lld\n",ans[i]/yue,(b[num[i]].r-b[num[i]].l+1)*(b[num[i]].r-b[num[i]].l)/2/yue);
}
return 0;
}
P2709 小B的询问
套路都差不多,依旧设
十年 OI 一场空,不开 long long 见祖宗!
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5e4+5;
struct node
{
int l;
int r;
int id;
}b[N];
int a[N];
int tong[N];
int len;
int cmp(node x,node y)
{
int idx = (x.l-1)/len+1,idy = (y.l-1)/len+1;
return idx == idy?x.r<y.r:idx<idy;
}
int sum;
void add(int x)
{
sum+=2*tong[a[x]]+1;
tong[a[x]]++;
}
void del(int x)
{
tong[a[x]]--;
sum-=2*tong[a[x]]+1;
}
int ans[N];
signed main()
{
int n,_,__;
scanf("%lld %lld %lld",&n,&_,&__);
len = sqrt(n);
for(int i = 1;i<=n;i++)
{
scanf("%lld",&a[i]);
}
for(int i = 1;i<=_;i++)
{
scanf("%lld %lld",&b[i].l,&b[i].r);
b[i].id = i;
}
sort(b+1,b+_+1,cmp);
for(int i = 1,l = 1,r = 0;i<=_;i++)
{
while(l>b[i].l)
{
add(--l);
}
while(r<b[i].r)
{
add(++r);
}
while(l<b[i].l)
{
del(l++);
}
while(r>b[i].r)
{
del(r--);
}
ans[b[i].id] = sum;
}
for(int i = 1;i<=_;i++)
{
printf("%lld\n",ans[i]);
}
return 0;
}
CF86D Powerful array
你会发现跟上一题没啥区别,就多了个
十年 OI 一场空,不开 long long 见祖宗!
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5e4+5;
struct node
{
int l;
int r;
int id;
}b[N];
int a[N];
int tong[N];
int len;
int cmp(node x,node y)
{
int idx = (x.l-1)/len+1,idy = (y.l-1)/len+1;
return idx == idy?x.r<y.r:idx<idy;
}
int sum;
void add(int x)
{
sum+=2*tong[a[x]]+1;
tong[a[x]]++;
}
void del(int x)
{
tong[a[x]]--;
sum-=2*tong[a[x]]+1;
}
int ans[N];
signed main()
{
int n,_,__;
scanf("%lld %lld %lld",&n,&_,&__);
len = sqrt(n);
for(int i = 1;i<=n;i++)
{
scanf("%lld",&a[i]);
}
for(int i = 1;i<=_;i++)
{
scanf("%lld %lld",&b[i].l,&b[i].r);
b[i].id = i;
}
sort(b+1,b+_+1,cmp);
for(int i = 1,l = 1,r = 0;i<=_;i++)
{
while(l>b[i].l)
{
add(--l);
}
while(r<b[i].r)
{
add(++r);
}
while(l<b[i].l)
{
del(l++);
}
while(r>b[i].r)
{
del(r--);
}
ans[b[i].id] = sum;
}
for(int i = 1;i<=_;i++)
{
printf("%lld\n",ans[i]);
}
return 0;
}
CF617E XOR and Favorite Number
终于有稍微难那么一点的啦!首先你要知道异或的一个基础性质:如果
有几个注意点:
- 由于
\oplus_{i = l}^r a_i = sum_r \oplus sum_{l-1} ,所以我们读入的区间需要将左端点减1 。 - 十年 OI 一场空,不开 long long 见祖宗!
- 由于是异或,我们一开始需要初始化
num_0 = 1 ,因为我们存在一个sum_0 = 0 。
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e6+5;
struct node
{
int l;
int r;
int id;
}b[N];
int a[N];
int tong[N];
int len;
int k;
int cmp(node x,node y)
{
int idx = (x.l-1)/len+1,idy = (y.l-1)/len+1;
return idx == idy?x.r<y.r:idx<idy;
}
int sum;
void add(int x)
{
sum+=tong[a[x]^k];
tong[a[x]]++;
}
void del(int x)
{
tong[a[x]]--;
sum-=tong[a[x]^k];
}
int ans[N];
signed main()
{
int n,_;
scanf("%lld %lld %lld",&n,&_,&k);
len = sqrt(n);
for(int i = 1;i<=n;i++)
{
scanf("%lld",&a[i]);
a[i]^=a[i-1];
}
for(int i = 1;i<=_;i++)
{
scanf("%lld %lld",&b[i].l,&b[i].r);
b[i].l--;
b[i].id = i;
}
sort(b+1,b+_+1,cmp);
tong[0] = 1;
for(int i = 1,l = 0,r = 0;i<=_;i++)
{
while(l>b[i].l)
{
add(--l);
}
while(r<b[i].r)
{
add(++r);
}
while(l<b[i].l)
{
del(l++);
}
while(r>b[i].r)
{
del(r--);
}
ans[b[i].id] = sum;
}
for(int i = 1;i<=_;i++)
{
printf("%lld\n",ans[i]);
}
return 0;
}
P5268 [SNOI2017] 一个简单的询问
目前最难的一道题,首先题目给了两个区间,我们知道莫队是只能处理一个区间的,但是我们可以将这两个区间拆开:
然后设:
那么答案就是:
我们只需要将询问强行拆成这四种,每种只有两个数,于是就可以莫队了!不过你在处理的时候要注意:看似你是在使用
十年 OI 一场空,不开 long long 见祖宗!
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e5+5;
struct node
{
int l;
int r;
int id;//询问编号
int idd;//在这个询问的几个要处理的变量
}b[N];
int a[N];
int tong1[N];
int tong2[N];
int len;
int cmp(node x,node y)
{
int idx = (x.l-1)/len+1,idy = (y.l-1)/len+1;
return idx == idy?x.r<y.r:idx<idy;
}
int sum;
void add(int x,int opt)//opt用来记录你是左端点扩充还是右端点扩充
{
if(!opt)
{
sum-=tong2[a[x+1]];
tong1[a[x+1]]--;
}
else
{
sum+=tong1[a[x]];
tong2[a[x]]++;
}
}
void del(int x,int opt)//opt用来记录你是左端点扩充还是右端点扩充
{
if(!opt)
{
sum+=tong2[a[x+1]];
tong1[a[x+1]]++;
}
else
{
sum-=tong1[a[x]];
tong2[a[x]]--;
}
}
int ans[4][N];
signed main()
{
int n,_;
scanf("%lld",&n);
len = sqrt(n);
for(int i = 1;i<=n;i++)
{
scanf("%lld",&a[i]);
}
scanf("%lld",&_);
int cnt = 0;
for(int i = 1;i<=_;i++)
{
int l1,r1,l2,r2;
scanf("%lld %lld %lld %lld",&l1,&r1,&l2,&r2);
b[++cnt] = {r1,r2,i,0};
b[++cnt] = {r1,l2-1,i,1};
b[++cnt] = {l1-1,r2,i,2};
b[++cnt] = {l1-1,l2-1,i,3};
}
sort(b+1,b+cnt+1,cmp);
for(int i = 1,l = 1,r = 0;i<=cnt;i++)
{
while(l>b[i].l)
{
add(--l,0);
}
while(r<b[i].r)
{
add(++r,1);
}
while(l<b[i].l)
{
del(l++,0);
}
while(r>b[i].r)
{
del(r--,1);
}
ans[b[i].idd][b[i].id] = sum;
}
for(int i = 1;i<=_;i++)
{
printf("%lld\n",ans[0][i]-ans[1][i]-ans[2][i]+ans[3][i]);
}
return 0;
}
六、带修莫队的引入
你会发现莫队在单点修改的时候会显得无能为力,这个时候就要用到带修莫队了!!
带修莫队和普通莫队差不多,只是需要多维护一个时间戳
七、带修莫队时间复杂度
块长取 证明?不会……
八、带修莫队板子
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+5;
struct node
{
int l;
int r;
int t;
int id;
}b[N];
int a[N];
int tong[N];
int len;
int p[N];
int c[N];
int tmp[N];
int cmp(node x,node y)
{
int idlx = (x.l-1)/len+1,idly = (y.l-1)/len+1;
if(idlx!=idly)
{
return idlx<idly;
}
int idrx = (x.r-1)/len+1,idry = (y.r-1)/len+1;
if(idrx!=idry)
{
return idrx<idry;
}
return x.t<y.t;
}
int sum;
void add(int x)
{
//自己填
}
void del(int x)
{
//自己填
}
void addt(int l,int r,int id)
{
if(p[id]<l||p[id]>r)
{
tmp[id] = a[p[id]];
a[p[id]] = c[id];
}
else
{
del(p[id]);
tmp[id] = a[p[id]];
a[p[id]] = c[id];
add(p[id]);
}
}
void delt(int l,int r,int id)
{
if(p[id]<l||p[id]>r)
{
a[p[id]] = tmp[id];
}
else
{
del(p[id]);
a[p[id]] = tmp[id];
add(p[id]);
}
}
int ans[N];
signed main()
{
int n,_,tot = 0,cnt = 0;
scanf("%d %d",&n,&_);
len = pow(n,2.0/3);
for(int i = 1;i<=n;i++)
{
scanf("%d",&a[i]);
}
for(int i = 1;i<=_;i++)
{
char opt;
scanf(" %c",&opt);
if(opt == 'R')
{
tot++;
scanf("%d %d",&p[tot],&c[tot]);
}
else
{
cnt++;
scanf("%d %d",&b[cnt].l,&b[cnt].r);
b[cnt].t = tot;
b[cnt].id = cnt;
}
}
sort(b+1,b+cnt+1,cmp);
for(int i = 1,l = 1,r = 0,t = 0;i<=_;i++)
{
while(l>b[i].l)
{
add(--l);
}
while(r<b[i].r)
{
add(++r);
}
while(l<b[i].l)
{
del(l++);
}
while(r>b[i].r)
{
del(r--);
}
while(t<b[i].t)
{
addt(l,r,++t);
}
while(t>b[i].t)
{
delt(l,r,t--);
}
ans[b[i].id] = sum;
}
for(int i = 1;i<=cnt;i++)
{
printf("%d\n",ans[i]);
}
return 0;
}
注意:这只是板子,应用时请随机应变。
九、带修莫队练习题
P1903 [国家集训队] 数颜色 / 维护队列
这题很毒瘤!!!请注意常数!!!! 就是带修莫队板子,套进去就好了。 代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+5;
struct node
{
int l;
int r;
int t;
int id;
}b[N];
int a[N];
int tong[N];
int len;
int p[N];
int c[N];
int tmp[N];
int cmp(node x,node y)
{
int idlx = (x.l-1)/len+1,idly = (y.l-1)/len+1;
if(idlx!=idly)
{
return idlx<idly;
}
int idrx = (x.r-1)/len+1,idry = (y.r-1)/len+1;
if(idrx!=idry)
{
return idrx<idry;
}
return x.t<y.t;
}
int sum;
void add(int x)
{
sum+=!tong[a[x]];
tong[a[x]]++;
}
void del(int x)
{
tong[a[x]]--;
sum-=!tong[a[x]];
}
void addt(int l,int r,int id)
{
if(p[id]<l||p[id]>r)
{
tmp[id] = a[p[id]];
a[p[id]] = c[id];
}
else
{
del(p[id]);
tmp[id] = a[p[id]];
a[p[id]] = c[id];
add(p[id]);
}
}
void delt(int l,int r,int id)
{
if(p[id]<l||p[id]>r)
{
a[p[id]] = tmp[id];
}
else
{
del(p[id]);
a[p[id]] = tmp[id];
add(p[id]);
}
}
int ans[N];
signed main()
{
int n,_,tot = 0,cnt = 0;
scanf("%d %d",&n,&_);
len = pow(n,2.0/3);
for(int i = 1;i<=n;i++)
{
scanf("%d",&a[i]);
}
for(int i = 1;i<=_;i++)
{
char opt;
scanf(" %c",&opt);
if(opt == 'R')
{
tot++;
scanf("%d %d",&p[tot],&c[tot]);
}
else
{
cnt++;
scanf("%d %d",&b[cnt].l,&b[cnt].r);
b[cnt].t = tot;
b[cnt].id = cnt;
}
}
sort(b+1,b+cnt+1,cmp);
for(int i = 1,l = 1,r = 0,t = 0;i<=_;i++)
{
while(l>b[i].l)
{
add(--l);
}
while(r<b[i].r)
{
add(++r);
}
while(l<b[i].l)
{
del(l++);
}
while(r>b[i].r)
{
del(r--);
}
while(t<b[i].t)
{
addt(l,r,++t);
}
while(t>b[i].t)
{
delt(l,r,t--);
}
ans[b[i].id] = sum;
}
for(int i = 1;i<=cnt;i++)
{
printf("%d\n",ans[i]);
}
return 0;
}
之后可能会更新更多习题和回滚莫队,敬请期待!