浅谈莫队

前置介绍

莫队,顾名思义是莫涛队长是一种较为高效的暴力算法,其本质就是减少暴力跳动的过程,并且本算法有个远(chou)近(ming)闻(zhao)名(zhu)的特点,就是特别好写。所以深受广大蒟蒻的喜爱,比如我

当然先看看莫涛队长的原题目吧。

普通莫队

题目传送门:小 Z 的妹子(经过Exp10re魔改)

如果暴力,其实学了循环和数组的蒟蒻们就已经可以写了。(没出现的变量名不管,我懒的删。)

code-暴力

#include<bits/stdc++.h>
#define int long long
#define gcd __gcd
#define ll long long 
using namespace std;
const int N=5e5+50;
struct Q
{
	int l,r;
	ll ans1,ans2;
	int id;
}q[N];
ll pos[N],a[N],cnt[N];
int n,m,ans;
void up(int k)
{
	ans-=cnt[a[k]]*cnt[a[k]];
	cnt[a[k]]++;
	ans+=cnt[a[k]]*cnt[a[k]];
}
void down(int k)
{
	ans-=cnt[a[k]]*cnt[a[k]];
	cnt[a[k]]--;
	ans+=cnt[a[k]]*cnt[a[k]];
}
signed main()
{
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++)	scanf("%lld",&a[i]);
	for(int i=1;i<=m;i++)	scanf("%lld%lld",&q[i].l,&q[i].r);
	int l=1,r=0;
	for(int i=1;i<=m;i++)
	{
		while(r<q[i].r)	up(++r);
		while(r>q[i].r)	down(r--);
		while(l>q[i].l)	up(--l);
		while(l<q[i].l)	down(l++);
		if(q[i].l==q[i].r)//根据题目要求。 
		{
			q[i].ans1=0;q[i].ans2=1;
			continue;
		}
		q[i].ans1=ans-(q[i].r-q[i].l+1);
		q[i].ans2=(q[i].r-q[i].l+1)*(q[i].r-q[i].l);
		ll k=gcd(q[i].ans1,q[i].ans2);//gcd约分。 
		q[i].ans1/=k;
		q[i].ans2/=k;
		cout<<q[i].ans1<<"/"<<q[i].ans2<<endl; 
	}
}

当然我这个是没法过的。

可能与初学莫队的人来说我这个暴力偏复杂,但是为了后面的讲解,还是看看这个暴力。

$ update $:写下更新答案的做法

其实就是组合数

\[ C_{r-l+1}^{2} \]

具体的就是先朴素处理第一个答案,然后朴素的对 \(ans\) 进行增减,储存在答案数组里。

我们发现,l,r的跳动是大大的存在重复跳动的,也就是说我们完全可以减少l,r的跳动,所以,莫队,就粉墨登场了。

刚刚我们分析了这个算法不优秀的原因,所以我们就想办法让它优秀。

考虑一种做法,讲询问进行分块,按某种玄学方式排序后从而得到减少跳动的方法。

下面介绍这个玄学方式,对于每个区间块,以 L 为第一关键字,R 为第二关键字,排序。

下面给出 cmp 的代码

code-cmp

bool cmp(Q a,Q b)
{
	if(pos[a.l]==pos[b.l])	return a.r<b.r;
	else return a.l<b.l;
}

当然,这个排序方式也可以优化,例如进行奇偶优化等,这里就不再介绍。

差点忘记我们分的块的块长了,一般是取 \(n^\frac{1}{2} \),但实际操作 $ n^{\frac{2}{3}
}$, \(n^\frac{3}{4} \)会更优(纯纯是数据问题)。

分块的复杂度是 \(O(\sqrt{n}*\sqrt{n}\log_ {} {\sqrt{n}}+n\log_ {} {n}) =O(n\log_{} {n})\)

下面证明莫队的复杂度。

证明

设每一段中 l 的最大值是 \(max_{x}\)

排序后第一次求解时间复杂度显然是 \(O(n)\)

考虑最坏时间复杂度,即 每一段 r 的最大值是 n。

每一次改变的时间复杂度都是 \(O(max_{i}-max_{i-1})\)的。

最后计算总和时间复杂度为 \(O(\sqrt n*max_{\sqrt n-1})\)

即为 \(O(n\sqrt n)\)

证毕

现在我们的操作就是离线分块,排序,暴力求解。

code-莫队

#include<bits/stdc++.h>
#define int long long
#define gcd __gcd
#define ll long long 
using namespace std;
const int N=5e5+50;
struct Q
{
	int l,r;
	ll ans1,ans2;
	int id;
}q[N];
ll pos[N],a[N],cnt[N];
int n,m,ans;
bool cmp(Q a,Q b)
{
	if(pos[a.l]==pos[b.l])	return a.r<b.r;
	else return a.l<b.l;
}
void up(int k)
{
	ans-=cnt[a[k]]*cnt[a[k]];
	cnt[a[k]]++;
	ans+=cnt[a[k]]*cnt[a[k]];
}
void down(int k)
{
	ans-=cnt[a[k]]*cnt[a[k]];
	cnt[a[k]]--;
	ans+=cnt[a[k]]*cnt[a[k]];
}
bool id(Q a,Q b)
{
	return a.id<b.id;
}
signed main()
{
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++)	scanf("%lld",&a[i]);
	int t=sqrt(n);
	for(int i=1;i<=n;i++)	pos[i]=(i-1)/t+1;//询问分块,块长为sqrt(n)
	for(int i=1;i<=m;i++)
	{
		scanf("%lld%lld",&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(r<q[i].r)	up(++r);
		while(r>q[i].r)	down(r--);
		while(l>q[i].l)	up(--l);
		while(l<q[i].l)	down(l++);
		if(q[i].l==q[i].r)
		{
			q[i].ans1=0;q[i].ans2=1;
			continue;
		}
		q[i].ans1=ans-(q[i].r-q[i].l+1);
		q[i].ans2=(q[i].r-q[i].l+1)*(q[i].r-q[i].l);
		ll k=gcd(q[i].ans1,q[i].ans2);//gcd约分
		q[i].ans1/=k;
		q[i].ans2/=k;
	}
	sort(q+1,q+1+m,id);//还原排序前的询问
	for(int i=1;i<=m;i++)	printf("%lld/%lld\n",q[i].ans1,q[i].ans2);
}

这就得到一个优秀的暴力算法。

习题:U311773 CF86D SP3267
UVA12345

其实可以写我的私题,就是第一道,可以练习卡常技巧,和莫队常见优化技巧。(最后一道是福利,暴力都可以过)

to be continue.(肯定要到晚上了,电脑被某人征用了,没办法,太宠ta了)

带修莫队

鲁迅先生曾说过(鲁迅:对,我说过

其实莫队本不支持修改,修改的人多了,也就支持修改。

所以我们找个例题,看看带修改的莫队是怎么样的。

题目传送门: 数颜色 / 维护队列

先来复习一下不带修改的莫队。

这个 up,down函数(即我之前代码的修改函数),超级好写,所以不单独列出来了。

这里我使用了 \(n^{\frac{3}{4}}\) 的块长,在本题效率较高。

再看看带修改的莫队。

这里引进一个概念:时间轴。

时间轴的定义就是操作的时间。
就是我们维护的是l,r,time。
可以达到的最优时间复杂度是 \(O(n^\frac{2}{3}m^\frac{2}{3}t^\frac{1}{3})\)

笔者的证明似乎有点不好,等到有个比较完美的在给证明。

现在谈谈具体的修改方法。
这个是单点修改,我们可以轻松的转换。具体就是如果当前修改操作次数相较于下一次修改操作次数进行增减。
那么怎么进行这个增减呢。

修改:我们首先判断当前是否在修改区间内。如果是的话,我们等于是从区间中删掉颜色 \(a\),加上颜色 \(b\),并且当前项的颜色改成 \(b\)。如果不在修改区间内的话,我们就直接修改当为颜色 \(b\)

还原:等于加上一个当前项,把颜色 \(b\) 改成颜色 \(a\) 的修改。

这里笔者用了一个技巧就是修改变色,我们直接交换修改色和当前色,以便还原。

那么代码也就不难写了。

code-带修莫队

#include<bits/stdc++.h>
#define int long long
#define ll long long 
using namespace std;
const int N=1.4e6+50;
ll a[N],cnt[N],pos[N],ans,ans_[N];
char opt[15];
int cnt1,cnt2;
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
	while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
void print(int x)
{
    if(x<0)
	{
        putchar('-');
        x=-x;
    }
    if(x>9) print(x/10);
    putchar(x%10+'0');
}
struct Q
{
	int l,r;
	int t;
	int id;
}b[N];
struct Q_
{
	ll pos,k;
}c[N];
int n,m;
inline bool cmp(Q a,Q b)
{
    if(pos[a.l]!=pos[b.l]) return pos[a.l]<pos[b.l];
    if(pos[a.r]!=pos[b.r]) return pos[a.r]<pos[b.r];
    return a.t<b.t;
}
inline void up(int s)
{
	cnt[a[s]]++;
	if(cnt[a[s]]==1)	ans++;
}
inline void down(int s)
{
	cnt[a[s]]--;
	if(cnt[a[s]]==0)	ans--;
}
inline void st(int now,int i)
{
    if(c[now].pos>=b[i].l&&c[now].pos<=b[i].r) 
    {
        if(--cnt[a[c[now].pos]]==0) ans--;
        if(++cnt[c[now].k]==1)      ans++; 
    }//只有在区间,我们才改变答案。
    swap(c[now].k,a[c[now].pos]);//刚刚提到的技巧
}
signed main()
{
	n=read(),m=read();
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=1;i<=m;i++)
	{ 
		scanf("%s",opt);
		if(opt[0]=='Q')
		{
			cnt1++;
			b[cnt1].l=read();
			b[cnt1].r=read();
			b[cnt1].t=cnt2;
			b[cnt1].id=cnt1;//保存前一次修改的时间。
		}
		else	c[++cnt2].pos=read(),c[cnt2].k=read();
	}//记录操作时间。 
	
	int tot=pow(n,0.75);//块长n^(3/4)
	
	for(int i=1;i<=n;i++)	pos[i]=(i-1)/tot+1;
	sort(b+1,b+1+cnt1,cmp);
	int l=1,r=0,now=0;
	for(int i=1;i<=cnt1;i++)
	{
		while(l<b[i].l)	down(l++);
		while(l>b[i].l)	up(--l);
		while(r>b[i].r) down(r--);
		while(r<b[i].r)	up(++r);//普通莫队
		while(now<b[i].t) st(++now,i);//时间轴
      while(now>b[i].t) st(now--,i);
		ans_[b[i].id]=ans;
	}
	for(int i=1;i<=cnt1;i++)	print(ans_[i]),putchar('\n');
}

有一个要注意的地方,就是我们是最后动时间轴的,其实一开始动也可以,但是会添加不必要的麻烦(这个麻烦读者可以自己看看是哪里),便于操作,我们最后动时间轴的。

于是我们就完成了带修莫队。

to be continue.

树上莫队

posted @ 2023-09-26 17:07  奈绪  阅读(160)  评论(0)    收藏  举报