莫队学习笔记

莫队

是一种离线算法,当我们发现区间结果很难维护,但是添加和删除元素比较简单的时候,就可以考虑莫队。需要对询问排序,处理询问的时候只需要移动端点。可以证明加入/删除元素的复杂度为 \(O(1)\) 的情况下,整体复杂度还是 \(O(n\sqrt{n})\)

常见优化

奇偶性排序
不难发现把询问区间的右端点排成连续的波浪形会比仅仅是块内单调递增的复杂度更好一些,所以

	friend bool operator <(node a,node b)
	{
		return pos[a.x]<pos[b.x]||
		(pos[a.x]==pos[b.x]&&(((pos[a.x]&1)&&a.y<b.y)||((!(pos[a.x]&1))&&a.y>b.y)));
	}

块的大小
通常用均值不等式算出最优的分块大小。听说还有很多玄学的分块大小,不过均值不等式还是太难了。
mn不同阶时分块大小应为\(\frac{n}{\sqrt m}\)

模板(HH的项链)

排序然后移动端点找答案。//但是以下代码并不能通过此题

void upd(int x,int k)//更新x点的颜色数量
{
	if(!num[x])++cnt;
	num[x]+=k;
	if(!num[x])--cnt;
}
int main()
{
	n=read();s=sqrt(n);
	for(int i=1;i<=n;i++)//
	{
		a[i]=read(),pos[i]=(i-1)/s+1;
	}
	m=read();
	for(int i=1;i<=m;i++)b[i].x=read(),b[i].y=read(),b[i].id=i;//读入+分块
	sort(b+1,b+m+1);//排序
	int l=1,r=0;
	for(int i=1;i<=m;i++)
	{
		while(l>b[i].x)upd(a[--l],1);//更新结果
		while(l<b[i].x)upd(a[l++],-1);
		while(r<b[i].y)upd(a[++r],1);
		while(r>b[i].y)upd(a[r--],-1);
		ans[b[i].id]=cnt;
	}
	for(int i=1;i<=m;i++)printf("%d\n",ans[i]);
	return 0;
}

回滚莫队

有些情况下,注意到元素的加入或者删除非常简单,但反过来删除或者加入就很难,所以考虑依然按照左端点在同一块内、右端点单调递增的顺序排序,注意,不能使用奇偶块优化。这样的话每次左端点移动到一个新的块里时,暴力求出答案并保存,求下一个区间答案时把左端点恢复到下一块的右端点,复原答案,右端点一直往后跑就行了。

数列分块入门9
观察得出可以使用只加不删的回滚莫队,如下:

int l=0,r=0,lp=1;
	for(int i=1;i<=tot;++i)
	{
		memset(cnt,0,sizeof cnt);
		r=rt[i];
		tem=-1;
		while(pos[b[lp].x]==i)
		{
			l=rt[i]+1;
			if(b[lp].y-b[lp].x<=s)//同一块内
			{
				ans[b[lp].id]=v[calc(b[lp].x,b[lp].y)];//暴力
				++lp;
				continue;
			}
			while(b[lp].y>r)add(++r);//右端点移动 添加新元素
			long long las=tem;//保存答案
			while(b[lp].x<l)add(--l);//添加新元素
			ans[b[lp].id]=v[tem];
			tem=las;//回复答案
			while(l<=rt[i])--cnt[a[l++]];//恢复桶
			++lp;
		}
	}

带修莫队

如果查询中有修改,但是不强制在线的话,同样可以用莫队做,只需要在结构体里再存一维时间,然后记录下所有的询问,其他部分就和普通莫队差不多一样了。

需要注意一下排序和时间的修改。(例题)

inline bool cmp(que a,que b)
{
  if (pos[a.x]!=pos[b.x])
  return a.x<b.x;
  if (pos[a.y]!=pos[b.y])
  return a.y<b.y;
  return a.z<b.z;//z为时间
}
void addt(int t)//修改时间
{
	swap(a[c[t]],d[t]);//交换一下,因为下一次时间标记经过此处的操作一定是相反的
	if(l<=c[t]&&c[t]<=r)//如果在区间内,则会对当前答案造成影响
	{
		if(!cnt[a[c[t]]])++tmp;	
		++cnt[a[c[t]]];
		--cnt[d[t]];
		if(!cnt[d[t]])--tmp;
	}
}

树上莫队

莫队是在一个序列上做统计,那么如果要让莫队上树就要先把树变成一个序列。这里我们使用括号序,像这样得到括号序:

void dfs(int x,int fa=-1)
{
  kh[++tot]=x;//记录括号序
  st[x]=tot;//记录进入x的位置
  //向下dfs
  kh[++tot]=x;//记录括号序
  en[x]=tot;//记录离开x的位置
}

然后我们需要把每次查询的点的编号转换成括号序列的下标(令st[u]>st[v]):
1.如果vu的子树中,那么区间转换为\([en_{u},st_{v}]\)
2.否则区间转换为\([st_{u},st_{v}]\)
移动端点时,需要记录当前区间中下标出现的次数,若为两次则抵消。统计答案时需要特判lca

例题

void add(int x)
{
	cnt[kh[x]]^=1;//
	if(!cnt[kh[x]])//取出节点 
	{
		num[a[kh[x]]]--;
		if(!num[a[kh[x]]])tmp--;
	}
	else//加入节点 
	{
		if(!num[a[kh[x]]])tmp++;
		num[a[kh[x]]]++;
	}
}
int main()
{
	n=read();m=read();s=max(2.0,n/sqrt(n*2));//注意分块的大小 
	lmt=log2(n)+1;
	for(int i=1;i<=n;i++)a[i]=read(),v[i]=a[i];
	sort(v+1,v+n+1); 
	for(int i=1;i<=n;i++)a[i]=lower_bound(v+1,v+n+1,a[i])-v;//离散化 
	for(int i=1;i<n;i++)
	{
		x=read();y=read();insert(x,y);insert(y,x);
	}
	d[1]=1;
	dfs(1);//预处理 
	for(int i=1;i<=lmt;i++)
		for(int j=1;j<=n;j++)f[i][j]=f[i-1][f[i-1][j]];//求lca用 
	for(int i=1;i<=m;i++)
	{
		b[i].c=read();b[i].d=read();b[i].e=lca(b[i].c,b[i].d);//读入端点,求lca 
		if(st[b[i].c]>st[b[i].d])swap(b[i].c,b[i].d);//令c比d先在括号序中出现 
		b[i].x=(b[i].e==b[i].c)?st[b[i].c]:en[b[i].c];//转化区间 
		b[i].y=st[b[i].d];b[i].id=i;
	}
	sort(b+1,b+m+1);//然后就是一般的莫队 
	int l=1,r=0;
	for(int i=1;i<=m;i++)
	{
		while(r<b[i].y)add(++r);
		while(l>b[i].x)add(--l);
		while(r>b[i].y)add(r--);
		while(l<b[i].x)add(l++);
		ans[b[i].id]=tmp;
		if(b[i].c!=b[i].e&&!num[a[b[i].e]])++ans[b[i].id];//特判lca 
	}
	for(int i=1;i<=m;i++)write(ans[i]),puts("");
	return 0;
}

题目

异或序列
简单莫队\(+\)前缀和思想。\(sum_{i}\) 表示 \([1,i]\) 区间内的异或前缀和,问题就转化为每次询问的区间 \([l-1,r]\) 内有多少对 \((i,j)\) 满足 \(sum_{j} \oplus sum_{i}=k\),即\(sum_{i} \oplus k= sum_{j}\)

小Z的袜子
列式子并化简可得,\(ans=\frac{\Sigma C_{cnt_{i}}^2}{C_{len}^2}=\frac{\Sigma cnt_{i}^2-\Sigma cnt_{i}}{len^2-len}\)\(cnt_{i}\)表示当前区间内颜色为 \(i\) 的袜子数量,\(len\)表示当前区间长度,然后就可以得到如何转移答案。

mex
解1:STL大法。用bitset记录当前区间中有哪些数出现过,然后使用内置函数快速求得结果。
解2:回滚莫队。

序列
简单莫队\(+\)推式子。设原本区间长度为\(len\),新向后加入一个元素后对答案的贡献是包含此元素的\(len+1\)个区间的最小值,显然从此元素到向前第一个比此元素小的元素的区间最小值都是此元素,再往前的结果可以预处理得到。

大数
简单莫队\(+\)推式子。设\(sum_{i}\) 表示大数中\([i,n]\)表示的数的大小,那么\([l,r]\)表示的数\(num_{l,r}=\frac{sum_{l}-sum_{r+1}}{10^{n-r}}\)
1.\(10\equiv0\pmod p\)\(p=2,5\)时,统计区间内有多少个以偶数或者\(0/5\)结尾的子序列。
2.\(10 \not\equiv 0\pmod p\)时,有\({sum_{l}-sum_{r+1}}\equiv0\pmod p\),即\(sum_{l}\equiv sum_{r+1}\pmod p\)。每次询问转化为查询区间内有多少模 \(p\) 意义下相等的数。

Abs Sum
推式子\(+\)普通莫队\(+\)树状数组。因为有\(|a-b|=\max(a,b)-\min(a,b)\),那么对于将 \(A\) 上区间\([1,r-1]\)转化为\([1,r]\),最大值对结果的贡献 \(=\) 当前已选 \(B\) 中元素中大于 \(A_{r}\) 的和 \(+\) 当前已选 \(B\) 中元素中小于 \(A_{r}\) 的个数 \(A_{r}\),最小值部分同理,用四个树状数组维护以上。

糖果公园
树上带修莫队。


易错

1.较为无关,但是离散化不要写成 sort(b.begin(),b.begin())

2.莫队移动区间端点时最好先延长再减短,比如:

  for(int i=1;i<=m;i++)
	{
		while(r<b[i].y)tem+=vr(l,++r);
		while(l>b[i].x)tem+=vl(--l,r);
		while(r>b[i].y)tem-=vr(l,r--);
		while(l<b[i].x)tem-=vl(l++,r);
		ans[b[i].id]=tem;
	}

3.回滚莫队不能用奇偶块优化。

4.如果在 mn 不同阶时用了上面说的分块方式,要注意块长不能是 \(0\)

5.忘记 pos 赋初值,忘记排序,弄反数组的值和下标,小心这些行为。

6.注意add函数的参数表示值还是下标。

posted @ 2025-05-16 21:18  baiguifan  阅读(30)  评论(0)    收藏  举报