莫队

莫队

优雅的暴力


用法

n n n\sqrt{n} nn 的时间内离线求解一段区间内不同数字的个数

实现

暴力做法:

用一个桶记录每种颜色出现的数量

随后扫描桶,进行统计

显然会超时

我们对询问进行排序,以便利用前一个询问的信息更新下一个询问

我们建立双指针,每次移动指针加入新数

这便是莫队算法的雏形(是暴力,但不优雅)

很容易发现我们刚才的做法仍然是 O ( n ) O(n) O(n)的,并没有得到优化

通过调整查询的顺序,我们可以把复杂度降到 O ( n ) O(\sqrt{n}) O(n )

我们通过排序,可以使得其中一个指针变得单调

另一个指针怎么解决?我们使用分块

我们进行排序时,应按这种方法排

  • 第一关键字:左端点所在分块的编号
  • 第二关键字:右端点的下标

于是我们每块的内部右端点递增

这样在更新的过程中,在每一块内部右端点移动数量不会超过 n n n(右端点在 1 → n 1\to n 1n,且递增),移动总数不会超过 n n n\sqrt{n} nn

左端点: 块 内 块 间 { 块 内 : n 块 间 : 2 n 块内块间\begin{cases}块内:\sqrt{n}\\块间:2\sqrt{n}\end{cases} {:n :2n

这样我们的总时间复杂度就是 O ( n n ) O(n\sqrt{n}) O(nn )

玄学优化

之所以说它玄学,是因为这些优化在某些数据中有奇效,但在其他数据中可能会失效

奇偶性优化

在排序时,第二关键字我们遵循以下原则:
{ 奇 数 块 : 块 内 右 端 点 从 小 到 大 排 偶 数 块 : 块 内 右 端 点 从 大 到 小 排 \begin{cases} 奇数块:块内右端点从小到大排\\ 偶数块:块内右端点从大到小排\\ \end{cases} {::
玄学解释:右端点可以减小反复横跳的次数,滚到右边后可以顺势滚回左端点,而不是回到起点

分块优化

块长取 n 2 m \sqrt{\frac{n^2}{m}} mn2 (具体视题目数据而定)

其他玄学技巧请自行bdfs

例题

AcWing2492. HH的项链

模板题,没什么可说的

/*************************************************************************
    > File Name: p1972[SDOI]HH的项链2.cpp
    > Author: Typedef 
    > Mail: [email protected] 
    > Created Time: 2021/3/8 8:15:05
    > Tags: 莫对,分块,玄学
 ************************************************************************/
#include
#include
#include
#include
using namespace std;
const int N=5e5+10,M=2e5+10,S=1e6+10;
int n,m,len;
int w[N],ans[N];
struct Query{
	int id,l,r;
}q[M];
int cnt[S];
int get(int x){
	return x/len;
}
bool cmp(const Query& a,const Query& b){
	int i=get(a.l),j=get(b.l);
	if(i!=j) return i<j;
	return i&1?a.r<b.r:a.r>b.r;//玄学拉满
}
void add(int x,int& res){
	if(!cnt[x]) res++;
	cnt[x]++;
}
void del(int x,int &res){
	cnt[x]--;
	if(!cnt[x]) res--;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&w[i]);
	scanf("%d",&m);
	len=sqrt((double)n*n/m);//玄学拉满
	for(int i=0;i<m;i++){
		int l,r;
		scanf("%d%d",&l,&r);
		q[i]={i,l,r};
	}
	sort(q,q+m,cmp);
	for(int k=0,i=0,j=1,res=0;k<m;k++){
		int id=q[k].id,l=q[k].l,r=q[k].r;
		while(i<r) add(w[++i],res);
		while(i>r) del(w[i--],res);
		while(j<l) del(w[j++],res);
		while(j>l) add(w[--j],res);
		ans[id]=res;
	}
	for(int i=0;i<m;i++) printf("%d\n",ans[i]);
	system("pause");
	return 0;
}

玄学拉满: 1575 m s 1575ms 1575ms

不使用奇偶性优化: 1511 m s 1511ms 1511ms

不使用分块优化: 1797 m s 1797ms 1797ms

不使用任何玄学: 1737 m s 1737ms 1737ms

在本题中奇偶性优化效果不佳,但分块优化十分有效


带修莫队

谁说莫队不能带修?

不带修改的莫队是一维的,而带修莫队则是二维的,其中第二维是时间

我们假设有 t t t个修改操作,那么我们的莫队就有 t t t

其中的第 i i i层表示经过第 i i i次修改操作的情况

对于在 k k k修改之后, k + 1 k+1 k+1修改之前的操作[L,R],我们给它一个时间为 k k k的时间戳

这样操作都变成了[L,R,K]

我们的指针同样也变成了三个:i,j,t

每次我们需要将i移动到L的位置,j移动到R,t移动到K的位置上

ij我们可以用 O ( 1 ) O(1) O(1)时间维护,问题在于如何维护t

假设我们在k-1行,经过修改操作变成k

在这个过程中,只会有一个数发生了变化
{ 被 修 改 数 不 在 i , j 区 间 中 : 无 需 更 改 c n t 被 修 改 数 在 i , j 区 间 中 : 进 行 一 次 d e l 操 作 和 a d d 操 作 即 可 \begin{cases} 被修改数不在i,j区间中:无需更改cnt\\ 被修改数在i,j区间中:进行一次del操作和add操作即可\\ \end{cases} {i,j:cnti,j:deladd
复杂度 O ( 1 ) O(1) O(1)

t是从 0 → k 0\to k 0k走,并将x修改为x',那么我们在从 k → 0 k\to0 k0走时需将其恢复

有一个取巧的方式,对于修改操作,我们交换xx'

这样向下走时会复原

那么如何排序呢?

同样是让一个指针单调,不能单调的分块

于是我们用三个关键字进行排序
{ 第 一 关 键 字 : l 所 在 块 的 编 号 第 二 关 键 字 : r 所 在 块 的 编 号 第 三 关 键 字 : t ( 时 间 戳 ) \begin{cases} 第一关键字:l所在块的编号\\ 第二关键字:r所在块的编号\\ 第三关键字:t(时间戳)\\ \end{cases} :l:r:t()
很好,那么又进行分块呢?

本题中我们块长取 n t 3 \sqrt[3]{nt} 3nt

最终总的时间复杂度 n 4 t 3 \sqrt[3]{n^4t} 3n4t

t在极端情况下是m

/*************************************************************************
    > File Name: p1903[国家集训队]数颜色.cpp
    > Author: Typedef 
    > Mail: [email protected] 
    > Created Time: 2021/3/8 10:00:12
    > Tags: 莫队,带修莫队
 ************************************************************************/
#include
#include
#include
#include
using namespace std;
const int N=233333,S=1000010;
int n,m,len,mq,mc;
int w[N],cnt[S],ans[N];
int read(){
    int x=0;char r=getchar();
    while(!isdigit(r))r=getchar();
    while(isdigit(r)){x=x*10+r-'0';r=getchar();}
    return x;
}
struct Query{
	int id,l,r,t;
}q[N];
struct Modify{
	int p,c;
}c[N];
int get(int x){
	return x/len;
}
bool cmp(const Query& a,const Query& b){
	int al=get(a.l),ar=get(a.r);
	int bl=get(b.l),br=get(b.r);
	if(al!=bl) return al<bl;
	if(ar!=br) return ar<br;
	return a.t<b.t;
}
void add(int x,int& res){
	if(!cnt[x]) res++;
	cnt[x]++;
}
void del(int x,int& res){
	cnt[x]--;
	if(!cnt[x]) res--;
}
int main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++) w[i]=read();
	for(int i=0;i<m;i++){
		char op[2];
		int a,b;
		cin>>op;
		a=read(),b=read();
		if(*op=='Q') mq++,q[mq]={mq,a,b,mc};
		else c[++mc]={a,b};
	}
	len=cbrt((double)n*mc)+1;
	if(!mc) len=sqrt((double)n)+1;
	sort(q+1,q+mq+1,cmp);
	for(int i=0,j=1,t=0,k=1,res=0;k<=mq;k++){
		int id=q[k].id,l=q[k].l,r=q[k].r,tm=q[k].t;
		while(i<r) add(w[++i],res);
		while(i>r) del(w[i--],res);
		while(j<l) del(w[j++],res);
		while(j>l) add(w[--j],res);
		while(t<tm){
			t++;
			if(c[t].p>=j&&c[t].p<=i){
				del(w[c[t].p],res);
				add(c[t].c,res);
			}
			swap(w[c[t].p],c[t].c);
		}
		while(t>tm){
			if(c[t].p>=j&&c[t].p<=i){
				del(w[c[t].p],res);
				add(c[t].c,res);
			}
			swap(w[c[t].p],c[t].c);
			t--;
		}
		ans[id]=res;
	}
	for(int i=1;i<=mq;i++) printf("%d\n",ans[i]);
	system("pause");
	return 0;
}

回滚莫队

有些情况中,我们很容易进行插入操作,却很难进行删除操作

比如维护集合内最大值

[JOI2013]历史研究

给我们一个序列,每次询问一个区间

我们定义一个数的重要度,为该数的值与该数在区间中出现次数的乘积

每次求区间重要度的最大值

保证所有的数为正

同样双关键字排序
{ 第 一 关 键 字 : l 所 在 块 的 编 号 第 二 关 键 字 : r 右 端 点 \begin{cases} 第一关键字:l所在块的编号\\ 第二关键字:r右端点\\ \end{cases} {:l:r
我们考虑块内:

  • 暴力求块内的所有区间
  • 对于右端点在块外的情况
    • 块内部分可以暴力加
    • 块外部分与正常莫队相似
/*************************************************************************
    > File Name: AT1219历史研究.cpp
    > Author: Typedef 
    > Mail: [email protected] 
    > Created Time: 2021/3/9 9:38:59
    > Tags: 
 ************************************************************************/
#include
#include
#include
#include
#include
#include
using namespace std;
typedef long long ll;
const int N=1e5+7;
int n,m,len;
int w[N],cnt[N];
ll ans[N];
struct Query{
	int id,l,r;
}q[N];
vector<int> nums;
int get(int x){
	return x/len;
}
bool cmp(const Query& a,const Query& b){
	int i=get(a.l),j=get(b.l);
	if(i!=j) return i<j;
	return a.r<b.r;
}
void add(int x,ll& res){
	cnt[x]++;
	res=max(res,(ll)cnt[x]*nums[x]);
}
int main(){
	scanf("%d%d",&n,&m);
	len=sqrt(n);
	for(int i=1;i<=n;i++) scanf("%d",&w[i]),nums.push_back(w[i]);
	sort(nums.begin(),nums.end());
	nums.erase(unique(nums.begin(),nums.end()),nums.end());
	for(int i=1;i<=n;i++)
		w[i]=lower_bound(nums.begin(),nums.end(),w[i])-nums.begin();
	for(int i=0;i<m;i++){
		int l,r;
		scanf("%d%d",&l,&r);
		q[i]={i,l,r};
	}
	sort(q,q+m,cmp);
	for(int x=0;x<m;){
		int y=x;
		while(y<m&&get(q[y].l)==get(q[x].l)) y++;
		int right=get(q[x].l)*len+len-1;//暴力右边界
		while(x<y&&q[x].r<=right){
			ll res=0;
			int id=q[x].id,l=q[x].l,r=q[x].r;
			for(int k=l;k<=r;k++) add(w[k],res);
			ans[id]=res;
			for(int k=l;k<=r;k++) cnt[w[k]]--;
			x++;
		}
		ll res=0;
		int i=right,j=right+1;
		while(x<y){
			int id=q[x].id,l=q[x].l,r=q[x].r;
			while(i<r) add(w[++i],res);
			ll backup=res;
			while(j>l) add(w[--j],res);
			ans[id]=res;
			while(j<right+1) cnt[w[j++]]--;
			res=backup;
			x++;
		}
		memset(cnt,0,sizeof(cnt));
	}
	for(int i=0;i<m;i++) printf("%lld\n",ans[i]);
	system("pause");
	return 0;
}

树上莫队

很好理解,莫队跑树上

SP10707 COT2 - Count on a tree II

树上每一个节点都有一个权值

给定两个点u,v,询问路径上不同权值的个数

**欧拉序:**类似dfs序,不过在回溯的时候要再记一次

**性质:**点u,v,我们用first[u],last[u]分别表示u第一次和最后一次出现的位置

x,y满足first[x]

  • lca(x,y)=x则欧拉序列中,[first[x],first[y]]中只出现一次的点
  • lca(x,y)!=x则对应欧拉序列中[last[x],first[y]]中只出现一次的点

这样我们就把树上路径转化为区间了

你可能感兴趣的:(笔记,题解,数据结构)