参考讲解
莫队算法将暴力与分块两个算法合二为一,可以称之为优雅的暴力
莫队是一个必须离线的算法
本质是通过改变查询的顺序来实现降低时间复杂度
样例:求一个区间中每个数出现次数的平方和(多次询问)
我们可以用暴力来做每次枚举L~R,这样的暴力注定不可,我们考虑先一个等级的暴力
一开始指针区间为0->0每一个查询,左指针left和右指针right不断更新
比如Left=2,Right=4,
而题目要求查询L=1,r=5
left就要向前移动一位,也就是left–,而left位置上(也就是L=1这个位置)出现的数的次数自然就要+1(因为原本不在区间内,而现在在了)
同理,right要向后移动一位
添加和删除代码
inline void del(int x)
{
sum[c[x]]--;
ans-=2*sum[c[x]]+1;
}
inline void add(int x)
{
sum[c[x]]++;
ans+=2*sum[c[x]]-1;
}
指针移动代码
for(long long i=1;i<=m;i++)
{
long long ql=q[i].l,qr=q[i].r;
while(l<ql)
{
del(l++);
//l++;
}
while(l>ql)
{
// l--;
add(--l);
}
while(r<qr)
{
// r++;
add(++r);
}
while(r>qr)
{
del(r--);
// r--;
}
anss[q[i].num]=ans;
}
我们来分析一下时间复杂度,每一次询问移动两最大都会是O(N)
总复杂度为O(N*M)
貌似这样并不是很优化
接下来介绍莫队的核心
在本题中询问是多次的
我们按照询问给定的区间,然后对询问进行分块
比如九组询问:
2 3
1 4
4 5
1 6
7 9
8 9
5 8
6 8
n=9,我们可以分成三块,
也就是1 ~ 3 , 4 ~ 6,7 ~ 9
我们以L为准来将询问划分到不同块
在每一个单独的块内,我们让R更小的排在前面
对上面的九组询问可得:
(2,3)/(1,4)/(1,6)
(4,5)/(5,8)/(6,8)
(7,9)/(8,9)
实现代码:
bool cmp(node a,node b)
{
return (a.r/block)==(b.r/block)?a.l<b.l:a.r<b.r;
}
/* //根据奇偶性排序 bool cmp(node a,node b) { return pos[a.l]^pos[b.l]?pos[a.l]<pos[b.l]:pos[a.l]&1?a.r<b.r:a.r>b.r; } */
此时时间复杂度为O((N+M)*√N)
分析过程:
我将不大明白。。。所以直接引用参考题解
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=50005;
long long int c[maxn];
long long int sum[maxn];
struct node{
long long int l,r,num;
}q[maxn];
long long anss[maxn];
long long int block;
long long int ans=0;
bool cmp(node a,node b)
{
return (a.r/block)==(b.r/block)?a.l<b.l:a.r<b.r;
}
/* //根据奇偶性排序 bool cmp(node a,node b) { return pos[a.l]^pos[b.l]?pos[a.l]<pos[b.l]:pos[a.l]&1?a.r<b.r:a.r>b.r; } */
inline void del(int x)
{
sum[c[x]]--;
ans-=2*sum[c[x]]+1;
}
inline void add(int x)
{
sum[c[x]]++;
ans+=2*sum[c[x]]-1;
}
int main()
{
long long int n,m,k;
cin>>n>>m>>k;
block=sqrt(n);
for(long long i=1;i<=n;i++)cin>>c[i];
for(long long i=1;i<=m;i++)
{
cin>>q[i].l>>q[i].r;
q[i].num=i;
}
sort(q+1,q+1+m,cmp);
int l=1,r=0;
for(long long i=1;i<=m;i++)
{
long long ql=q[i].l,qr=q[i].r;
while(l<ql)
{
del(l++);
//l++;
}
while(l>ql)
{
// l--;
add(--l);
}
while(r<qr)
{
// r++;
add(++r);
}
while(r>qr)
{
del(r--);
// r--;
}
anss[q[i].num]=ans;
}
for(long long i=1;i<=m;i++)
printf("%lld\n",anss[i]);
return 0;
}
扩展
带修改莫队
参考博文
加入单点修改操作
将时间也加入询问,这样询问就变成了一个三元组(li,ri,ti)
从询问i移动到询问j的时间复杂度变成O(|li-ri|+|lj-rj|+|ti-tj|)
设m==n(即询问次数等于数的数量)
将序列分块,块的大小是n的三分之二次方,一共有n的三分之一次方个块
。将询问按左端点所在块的编号为第一关键字,右端点所在块的编号为第二关键字,时间为第三关键字排序。
(和之前比就是多个时间排序)
排序条件:
int cmp(const node1 &a,const node1 &b)
{
if (a.l/unit!=b.l/unit) return a.l/unit<b.l/unit;
else if (a.r/unit!=b.r/unit) return a.r/unit<b.r/unit;
else return a.x<b.x;
}
主函数维护答案:
void solve()
{
int l=1,r=0,now=0;
for (int i=1;i<=m;i++)
{
while (r<q[i].r) update(r+1,1),r++;
while (r>q[i].r) update(r,-1),r--;
while (l>q[i].l) update(l-1,1),l--;
while (l<q[i].l) update(l,-1),l++;
while (now<q[i].x) change(now+1,1,l,r),now++;
while (now>q[i].x) change(now,-1,l,r),now--;
ans[q[i].id]=tot;
}
}
修改通过change实现:
void change(int bh,int z,int l,int r)
{
if (ch[bh].x>=l&&ch[bh].x<=r) update(ch[bh].x,-1); //删除从前的影响
swap(c[ch[bh].x],ch[bh].y);
if (ch[bh].x>=l&&ch[bh].x<=r) update(ch[bh].x,1); //添加影响
}
树上莫队
如果例题改成:
给一颗n个点的树,点有点权,m次询问,每次询问xi到yi的路径上有多少个不同的点权
树上路径可以通过树的括号序转化成序列问题
用 first(x) 表示 x 在括号序里第一次出现的位置,则路径 (x,y)的信息就等于括号序上的区间 [first(x),first(y)] 的信息(设 first(x)<first(y) ),当 lca(x,y) 不是 x 也不是 y 时,还需要再加上 lca 的信息。