【小结】莫队

莫队使用了分块思想。

莫队是一个离线的,快速的维护统计区间信息的数据结构。

普通莫队

有这样一个问题:

\(n\) 个数的序列 \(a_1,a_2,a_3,...,a_n\)。有 \(m\) 次询问。

每次询问给定两个正整数 \(l,r\) 表示询问 \([l,r]\) 的数字种类数。

\(n \leq 10^5\)

先考虑暴力:

现在用两个指针 \(x,y\) 移动,\(x\) 表示当前维护信息的左端点。\(y\) 维护右端点。

那么对于询问可以一点点移动,从上一次的状态转移到这一次。

对于每一次移动,令 \(l\) 表示当前询问区间左端点,\(r\) 表示右端点。

\(x>l\),则 \(x\) 往左移,那么会加上 \(x-1\) 这个元素并使得 \(x\)\(1\)

\(y<r\),则 \(y\) 往右移,那么会加上 \(y+1\) 这个元素并使得 \(y\)\(1\)

\(x<l\),则 \(x\) 往右移,会删掉 \(x\) 这个元素并使得 \(x\)\(1\)

\(y>r\),则 \(y\) 往左移,会删掉 \(y\) 这个元素并使得 \(y\) 减 1。

那么如此维护每一次操作即可。

注意:

  1. 开始的初始状态一般为 \(x=1,y=0\)

  2. 最好先加入再删除,要不先删除了有可能会删成 \(-1\),造成不必要的麻烦。

暴力做法到此结束。

离线的目的是减少重复计算,提高复杂度。

既然是离线的,肯定要排序。

那么排序的方法是这样的:

考虑分块,令块长为 \(\sqrt{n}\)

先按左端点所在块的编号从小到大排序,若编号相同,则按右端点排序。

结论:这样排序调整顺序之后再做上述暴力的复杂度是正确的,是 \(O((n+m)\sqrt{n})\) 的。而且常数较小。

我们可以证明这个结论。

时间复杂度分析:

先考虑 \(y\),对于每一个块,\(y\) 会一直前进,所以是 \(O(n)\) 的。然后这一步的复杂度是 \(O(n\sqrt{n})\) 的。然后切换块的时候,\(y\) 大不了把 \(n\) 个元素都跑一遍,这样有 \(O(\sqrt{n})\) 次切换,也是 \(O(n\sqrt{n})\) 的。

然后考虑 \(x\)。对于每一个块,最多有 \(m\) 次操作,也就是 \(m\)\(x\)。然后每一个块内运动是 \(O(\sqrt{n})\) 的。别忘了块的大小是 \(O(\sqrt{n})\) 的。那么这一步的复杂度是 \(O(m\sqrt{n})\) 的。然后对于每一次切换,也大不了跑 \(n\) 次,因为只有 \(O(\sqrt{n})\) 个块,也是 \(O(n\sqrt{n})\) 的。

综上所述,上面的时间复杂度是 \(O((n+m)\sqrt{n})\) 的,排序的 \(O(m\log{m})\) 忽略不记。

那么这样就可以做出常数较小的根号做法。

排序只改变莫队的复杂度,而不改变正确性。

所以莫队超时的时候,首先检查排序有没有错。

P3901 数列找不同

用时 415ms 的 AC 记录

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10,B=320;
int n,q,i,a[N],x,y,l,r,id,cnt[N],res;
bool ans[N];
struct Node{
	int l,r,id;
}op[N];
bool cmp(Node x,Node y)
{
	if (x.l/B!=y.l/B) return x.l/B<y.l/B;
	return x.r<y.r;
}
void add(int x)
{
	cnt[a[x]]++;
	if (cnt[a[x]]==1) res++;
}
void del(int x)
{
	cnt[a[x]]--;
	if (cnt[a[x]]==0) res--;
}
int main()
{
	scanf("%d%d",&n,&q);
	for (i=1;i<=n;i++) scanf("%d",&a[i]);
	for (i=1;i<=q;i++) scanf("%d%d",&op[i].l,&op[i].r),op[i].id=i;
	sort(op+1,op+q+1,cmp);
	x=1;
	y=0;
	for (i=1;i<=q;i++)
	{
		l=op[i].l;
		r=op[i].r;
		id=op[i].id;
		while (x>l)
		{
			x--;
			add(x);
		}
		while (y<r)
		{
			y++;
			add(y);
		}
		while (x<l)
		{
			del(x);
			x++;
		}
		while (y>r)
		{
			del(y);
			y--;
		}
		ans[id]=(res==r-l+1);
	}
	for (i=1;i<=q;i++)
	{
		if (ans[i]) printf("Yes\n");
		else printf("No\n");
	}
	return 0;
}

然后还有一种优化方式,只用加一行。

但是他有点玄学,有可能不会优化。但当数据随机时,跑的是很快的。

就是每次我们的指针 \(y\) 都要从块中最小的 \(r\) 开始,跑到块中最大的 \(r\) 结束,再跑到另一个块的 \(r\),不断循环,复杂度是正确的,但有点浪费。

怎么办呢?我们只需要不让他那么多来回即可。

也就是在第 \(1\) 块做完之后,我要在返回第 \(2\) 块的过程中把 \(2\) 算完。

所以我们可以将 \(2\) 从大到小排序,从而使得不重复计算。

所以我们将块编号奇数的 \(r\) 从小到大排,将编号偶数的 \(r\) 从大到小排。

这种优化,当且仅当所有询问只在偶数或奇数是无效。

有时优化较大。

用时 324ms 的 AC 记录

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10,B=320;
int n,q,i,a[N],x,y,l,r,id,cnt[N],res;
bool ans[N];
struct Node{
	int l,r,id;
}op[N];
bool cmp(Node x,Node y)
{
	if (x.l/B!=y.l/B) return x.l/B<y.l/B;
	if ((x.l/B)%2==0) return x.r<y.r;
	return x.r>y.r;
}
void add(int x)
{
	cnt[a[x]]++;
	if (cnt[a[x]]==1) res++;
}
void del(int x)
{
	cnt[a[x]]--;
	if (cnt[a[x]]==0) res--;
}
int main()
{
	scanf("%d%d",&n,&q);
	for (i=1;i<=n;i++) scanf("%d",&a[i]);
	for (i=1;i<=q;i++) scanf("%d%d",&op[i].l,&op[i].r),op[i].id=i;
	sort(op+1,op+q+1,cmp);
	x=1,y=0;
	for (i=1;i<=q;i++)
	{
		l=op[i].l,r=op[i].r,id=op[i].id;
		while (x>l) add(--x);
		while (y<r) add(++y);
		while (x<l) del(x++);
		while (y>r) del(y--);
		ans[id]=(res==r-l+1);
	}
	for (i=1;i<=q;i++)
	{
		if (ans[i]) printf("Yes\n");
		else printf("No\n");
	}
	return 0;
}

P2709 小B的询问

考虑平方的贡献,从 \(x-1\) 变成 \(x\) 也就是 \(x^2-(x-1)^2=x^2-x^2+2x-1=2x-1\)。这是加操作。从 \(x+1\) 变成 \(x\) 也就是 \((x+1)^2-x^2=x^2+2x+1-x^2=2x+1\)。这是减少的量。

所以计算贡献即可。

AC记录

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=5e4+10,B=224;
int n,m,k,i,a[N],x,y,l,r,id,cnt[N];
ll ans[N],res;
struct Node{
	int l,r,id;
}op[N];
bool cmp(Node x,Node y)
{
	if (x.l/B!=y.l/B) return x.l/B<y.l/B;
	if ((x.l/B)%2==0) return x.r<y.r;
	return x.r>y.r;
}
void add(int x)
{
	if (a[x]>k) return;
	cnt[a[x]]++;
	res+=2*cnt[a[x]]-1;
}
void del(int x)
{
	if (a[x]>k) return;
	cnt[a[x]]--;
	res-=2*cnt[a[x]]+1;
}
int main()
{
	scanf("%d%d%d",&n,&m,&k);
	for (i=1;i<=n;i++) scanf("%d",&a[i]);
	for (i=1;i<=m;i++) scanf("%d%d",&op[i].l,&op[i].r),op[i].id=i;
	sort(op+1,op+m+1,cmp);
	x=1;
	y=0;
	for (i=1;i<=m;i++)
	{
		l=op[i].l;
		r=op[i].r;
		id=op[i].id;
		while (x>l)
		{
			x--;
			add(x);
		}
		while (y<r)
		{
			y++;
			add(y);
		}
		while (x<l)
		{
			del(x);
			x++;
		}
		while (y>r)
		{
			del(y);
			y--;
		}
		ans[id]=res;
	}
	for (i=1;i<=m;i++) printf("%lld\n",ans[i]);
	return 0;
}

P4396 [AHOI2013] 作业

一种做法是用莫队套树状数组做,直接莫队时按两种要求分别修改两个树状数组,然后查询 \([a,b]\) 即可。我做的是这种。复杂度 \(O(m\sqrt{n}\log{n})\) 卡卡常跑的飞快。

用时 932ms,最慢点591ms AC 记录

另一种做法是分块套莫队。块长 \(\sqrt{n}\)。莫队时只用修改全局和块内的种类数以及数值的数量。然后查询的时候也是 \(O(\sqrt{n})\) 的。合起来神奇的是 \(O((n+m)\sqrt{n})\) 的。还没写。

AC 记录

#include<bits/stdc++.h>
using namespace std;
/*
a[i] 表示数列第 i 个数。
cnt[i] 表示 i 数的出现次数。
buc[i] 表示出现次数为 i 的数的个数。
sum[i] 表示对于 buc[i] 的分块块中和。
答案:求出现次数的第 k 小。对于分块直接考虑类似倍增。直接枚举。
对于出现次数为 0 的东西我们不算因为他不算在股票中,可以认为他不存在。
所以只会有 1 以上的东西。
分块的策略就不能用 i/O+1 了,得直接暴力,或者写 (i-1)/O+1 也行。
*/
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
const int N=1e5+10,O=320;
int n,m,k,i,j,a[N],t[N],B;
int x,y,ans[N];
int l,r,id,buc[N],cnt[N];
int sum[N],L[N],R[N],ID[N];
struct Node{
	int l,r,k,id;
}op[N];
void build()
{
	int i,idx;
	for (i=1;i<=n;i+=O)
	{
		idx=i/O+1;
		L[idx]=i;
		R[idx]=min(n,i+O-1);
	}
	for (i=1;i<=n;i++) ID[i]=(i-1)/O+1;
}
bool cmp(Node x,Node y)
{
	if (x.l/B!=y.l/B) return x.l/B<y.l/B;
	if ((x.l/B)%2==0) return x.r<y.r;
	return x.r>y.r;
}
void add(int x)
{
	buc[cnt[a[x]]]--;
	sum[ID[cnt[a[x]]]]--;
	cnt[a[x]]++;
	buc[cnt[a[x]]]++;
	sum[ID[cnt[a[x]]]]++;
}
void del(int x)
{
	buc[cnt[a[x]]]--;
	sum[ID[cnt[a[x]]]]--;
	cnt[a[x]]--;
	buc[cnt[a[x]]]++;
	sum[ID[cnt[a[x]]]]++;
}
int query(int x)
{
	int i,j,id,res=0;
	for (i=1;i<=ID[n];i++)
	{
		if (res+sum[i]<x)
		{
			res+=sum[i];
			continue;
		}
		for (j=L[i];j<=R[i];j++)
		{
			if (res+buc[j]<x) res+=buc[j];
			else return j;
		}
	}
	return -1;
}
int main()
{
	scan>>n>>m;
	B=sqrt(n);
	build();
	for (i=1;i<=n;i++) scan>>a[i],t[i]=a[i];
	sort(t+1,t+n+1);
	k=unique(t+1,t+n+1)-t-1;
	for (i=1;i<=n;i++) a[i]=lower_bound(t+1,t+k+1,a[i])-t;
	for (i=1;i<=m;i++) scan>>op[i].l>>op[i].r>>op[i].k,op[i].id=i;
	sort(op+1,op+m+1,cmp);
	x=1;
	y=0;
	for (i=1;i<=m;i++)
	{
		l=op[i].l;
		r=op[i].r;
		k=op[i].k;
		id=op[i].id;
		while (x>l) add(--x);
		while (y<r) add(++y);
		while (x<l) del(x++);
		while (y>r) del(y--);
		ans[id]=query(k);
	}
	for (i=1;i<=m;i++) printf("%d\n",ans[i]);
	return 0;
}

P3730 曼哈顿交易

和上一题有点像。

首先,这个题大抵是不能用 \(O(n\sqrt{n}logn)\) 的算法过。考虑更优算法。

先离散化,然后值域分块套莫队。

\(cnt_i\) 表示 \(i\) 的出现次数,\(buc_i\) 表示出现次数为 \(i\) 的数的个数。\(sum_i\) 表示对于 \(buc_i\) 的分块,第 \(i\) 块的和。

对于这个东西,每次单点修改对于整个块计算贡献。

查询也是 \(O(\sqrt{n})\) 的。

所以总复杂度是 \(O(n\sqrt{n})\) 的。

通过这两题可以发现:莫队的题目,结合分块有时更优。原因就是分块的单点修改可以做到 \(O(1)\),查询 \(O(\sqrt{n})\)。莫队的题对于修改的复杂度系数是 \(O(n\sqrt{n})\) 的,查询的复杂度系数是 \(O(m)\) 的。所以我们可以牺牲查询的时间,换取修改的速度。就是做到时间上的尽可能平衡。

AC 记录

#include<bits/stdc++.h>
using namespace std;
/*
a[i] 表示数列第 i 个数。
cnt[i] 表示 i 数的出现次数。
buc[i] 表示出现次数为 i 的数的个数。
sum[i] 表示对于 buc[i] 的分块块中和。
答案:求出现次数的第 k 小。对于分块直接考虑类似倍增。直接枚举。
对于出现次数为 0 的东西我们不算因为他不算在股票中,可以认为他不存在。
所以只会有 1 以上的东西。
分块的策略就不能用 i/O+1 了,得直接暴力,或者写 (i-1)/O+1 也行。
*/
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
const int N=1e5+10,O=320;
int n,m,k,i,j,a[N],t[N],B;
int x,y,ans[N];
int l,r,id,buc[N],cnt[N];
int sum[N],L[N],R[N],ID[N];
struct Node{
	int l,r,k,id;
}op[N];
void build()
{
	int i,idx;
	for (i=1;i<=n;i+=O)
	{
		idx=i/O+1;
		L[idx]=i;
		R[idx]=min(n,i+O-1);
	}
	for (i=1;i<=n;i++) ID[i]=(i-1)/O+1;
}
bool cmp(Node x,Node y)
{
	if (x.l/B!=y.l/B) return x.l/B<y.l/B;
	if ((x.l/B)%2==0) return x.r<y.r;
	return x.r>y.r;
}
void add(int x)
{
	buc[cnt[a[x]]]--;
	sum[ID[cnt[a[x]]]]--;
	cnt[a[x]]++;
	buc[cnt[a[x]]]++;
	sum[ID[cnt[a[x]]]]++;
}
void del(int x)
{
	buc[cnt[a[x]]]--;
	sum[ID[cnt[a[x]]]]--;
	cnt[a[x]]--;
	buc[cnt[a[x]]]++;
	sum[ID[cnt[a[x]]]]++;
}
int query(int x)
{
	int i,j,id,res=0;
	for (i=1;i<=ID[n];i++)
	{
		if (res+sum[i]<x)
		{
			res+=sum[i];
			continue;
		}
		for (j=L[i];j<=R[i];j++)
		{
			if (res+buc[j]<x) res+=buc[j];
			else return j;
		}
	}
	return -1;
}
int main()
{
	scan>>n>>m;
	B=sqrt(n);
	build();
	for (i=1;i<=n;i++) scan>>a[i],t[i]=a[i];
	sort(t+1,t+n+1);
	k=unique(t+1,t+n+1)-t-1;
	for (i=1;i<=n;i++) a[i]=lower_bound(t+1,t+k+1,a[i])-t;
	for (i=1;i<=m;i++) scan>>op[i].l>>op[i].r>>op[i].k,op[i].id=i;
	sort(op+1,op+m+1,cmp);
	x=1;
	y=0;
	for (i=1;i<=m;i++)
	{
		l=op[i].l;
		r=op[i].r;
		k=op[i].k;
		id=op[i].id;
		while (x>l) add(--x);
		while (y<r) add(++y);
		while (x<l) del(x++);
		while (y>r) del(y--);
		ans[id]=query(k);
	}
	for (i=1;i<=m;i++) printf("%d\n",ans[i]);
	return 0;
}

P3709 大爷的字符串题

容易发现,题意就是 \(m\) 次询问询问 \([l,r]\) 众数出现次数。

跟上面题套路差不多,多一个 \(buc_i\) 表示出现次数为 \(i\) 的数的个数。然后添加时维护并取最大值,删除时,如果这个数是唯一的最大值,就把 \(ans-1\)

AC 记录

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int n,m,i,a[N],B,c[N],cnt[N],x,y,l,r,id,ans[N],res,t[N],k;
struct Node{
	int l,r,id;
}op[N];
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
bool cmp(Node x,Node y)
{
	if (x.l/B!=y.l/B) return x.l/B<y.l/B;
	if ((x.l/B)%2==0) return x.r<y.r;
	return x.r>y.r;
}
void add(int x)
{
	cnt[c[a[x]]]--;
	c[a[x]]++;
	cnt[c[a[x]]]++;
	res=max(res,c[a[x]]);
}
void del(int x)
{
	if (cnt[c[a[x]]]==1&&res==c[a[x]]) res--;
	cnt[c[a[x]]]--;
	c[a[x]]--;
	cnt[c[a[x]]]++;
}
int main()
{
	scan>>n>>m;
	B=sqrt(n);
	for (i=1;i<=n;i++) scan>>a[i],t[i]=a[i];
	sort(t+1,t+n+1);
	k=unique(t+1,t+n+1)-t-1;
	for (i=1;i<=n;i++) a[i]=lower_bound(t+1,t+k+1,a[i])-t;
	for (i=1;i<=m;i++) scan>>op[i].l>>op[i].r,op[i].id=i;
	sort(op+1,op+m+1,cmp);
	x=1;
	y=0;
	for (i=1;i<=m;i++)
	{
		l=op[i].l;
		r=op[i].r;
		id=op[i].id;
		while (x>l) add(--x);
		while (y<r) add(++y);
		while (x<l) del(x++);
		while (y>r) del(y--);
		ans[id]=res;
	}
	for (i=1;i<=m;i++) printf("%d\n",-ans[i]);
	return 0;
}

P5268 [SNOI2017] 一个简单的询问

思维挺难,建议升紫。

这题的你难点在于乘法是联系起多个变量的桥梁。所以整体思路就是要让乘法里的变量变少。

怎么变少?考虑前缀和把起点变为相同的。

下面是推式子。

\(\sum_{x=0}^{+\infty} get(l_1,r_1,x) \times get(l_2,r_2,x)\)

\(=\sum_{x=0}^{+\infty} (get(1,r_1,x)-get(1,l_1-1,x))\times(get(1,r_2,x)-get(1,l_2-1,x))\)

\(g(i,x)=get(1,i,x)\)

\(=\sum_{x=0}^{+\infty} g(r_1,x)\times g(r_2,x)- g(r_1,x)\times g(l_2-1,x)-g(l_1-1,x)\times g(r_2,x)+g(l_1-1,x)\times g(l_2-1,x)\)

现在乘法中的变量个数变为两个。

对于每组询问拆成四组询问来做,然后按照权值标记并累加即可。

AC 记录

#include<bits/stdc++.h>
using namespace std;
/*

*/
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
typedef long long ll;
const int N=5e4+10;
int n,q,i,a[N],k,l1,r1,l2,r2,B,x,y;
int l,r,id;
ll ans[N],cnt[N],buc[N],res;
struct Node{
	int l,r,id;
	ll val;
}op[N<<2];
bool cmp(Node x,Node y)
{
	if (x.l/B!=y.l/B) return x.l/B<y.l/B;
	if ((x.l/B)%2==0) return x.r<y.r;
	return x.r>y.r;
}
void add1(int x)
{
	cnt[a[x]]++;
	res+=buc[a[x]];
}
void add2(int x)
{
	buc[a[x]]++;
	res+=cnt[a[x]];
}
void del1(int x)
{
	cnt[a[x]]--;
	res-=buc[a[x]];
}
void del2(int x)
{
	buc[a[x]]--;
	res-=cnt[a[x]];
}
int main()
{
	scan>>n;
	B=sqrt(n);
	for (i=1;i<=n;i++) scan>>a[i];
	scan>>q;
	for (i=1;i<=q;i++)
	{
		scan>>l1>>r1>>l2>>r2;
		op[++k]={r1,r2,i,1};
		op[++k]={l1-1,r2,i,-1};
		op[++k]={r1,l2-1,i,-1};
		op[++k]={l1-1,l2-1,i,1};
	}
	sort(op+1,op+k+1,cmp);
	x=0;
	y=0;
	for (i=1;i<=k;i++)
	{
		l=op[i].l;
		r=op[i].r;
		id=op[i].id;
		while (x<l) add1(++x);
		while (y<r) add2(++y);
		while (x>l) del1(x--);
		while (y>r) del2(y--);
		ans[id]+=op[i].val*res;
	}
	for (i=1;i<=q;i++) printf("%lld\n",ans[i]);
	return 0;
}

CF617E XOR and Favorite Number

前缀和,查询异或和相同的点。

注意点:

  1. 查询 \([l,r]\)\(l\)\(1\)
  2. 不要把自己计入。
  3. 指针初始为 \(x=0,y=-1\)

AC 记录

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10,M=2e6+10;
int n,m,k,i,a[N],x,y,l,r,id,cnt[M],B;
ll ans[N],res;
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
struct Node{
	int l,r,id;
}op[N];
bool cmp(Node x,Node y)
{
	if (x.l/B!=y.l/B) return x.l/B<y.l/B;
	if ((x.l/B)%2==0) return x.r<y.r;
	return x.r>y.r;
}
void add(int x)
{ 
	res+=cnt[k^a[x]];
	cnt[a[x]]++;
}
void del(int x)
{
	cnt[a[x]]--;
	res-=cnt[k^a[x]];
}
int main()
{
	scan>>n>>m>>k;
	B=sqrt(n);
	for (i=1;i<=n;i++) scan>>a[i],a[i]^=a[i-1];
	for (i=1;i<=m;i++) scan>>op[i].l>>op[i].r,op[i].id=i,op[i].l--;
	sort(op+1,op+m+1,cmp);
	x=0;
	y=-1;
	for (i=1;i<=m;i++)
	{
		l=op[i].l;
		r=op[i].r;
		id=op[i].id;
		while (x>l)
		{
			x--;
			add(x);
		}
		while (y<r)
		{
			y++;
			add(y);
		}
		while (x<l)
		{
			del(x);
			x++;
		}
		while (y>r)
		{
			del(y);
			y--;
		}
		ans[id]=res;
	}
	for (i=1;i<=m;i++) printf("%lld\n",ans[i]);
	return 0;
}

P3245 [HNOI2016] 大数

绝世好题。

讨论 \(p\) 是否与 \(10\) 互质。

\(p=2\)\(5\) 的时候,即 \(p\) 不和 \(10\) 互质。

那么与末尾数字直接有关。

\(p=2\) 时,当且仅当末尾数字为 \(0,2,4,6,8\) 时合法。

\(p=5\) 时,当且仅当末尾数字为 \(0,5\) 时合法。

所以对于这两个数字,特判并计入。

\(\sum_{i=l}^{r} pos-l+1(a[pos]\equiv 0 (\mod p))\)

可以维护两个前缀和 \(s_{i,j}\) 表示前 \(i\) 个东西,数字为 \(j\) 的合法个数,\(sum_{i,j}\) 表示前 \(i\) 个东西,数字为 \(j\)\(\sum_{k=1}^{i} k\)

然后这种情况就出来了。

考虑 \(p \neq 2\)\(p \neq 5\) 时。令 \(suf_i\) 表示 \(i\) 后缀的进制和。

递推式:\(suf_i=suf_{i+1}+10^{n-i}\times a_i\)

这样一段 \([l,r]\) 的和就是 \(\frac{suf_l-suf_{r+1}}{10^{n-r}}\)

因为 \(10\)\(p\) 互质,所以 \(suf_l-suf_{r+1}\equiv 0(\mod p)\)。所以 \(suf_l\equiv suf_{r+1}(\mod p)\)

然后对于区间询问,按照上一题的套路莫队去找同余的。

\(p\) 很大,数组开不下,需要离散化。

AC 记录

#include<bits/stdc++.h>
using namespace std;
/*

*/
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
typedef long long ll;
const int N=2e5+10;
int p,i,n,m,l,r,B,id,cnt[N];
char a[N];
ll s[N][10],sum[N][10];
ll suf[N],b[N],k,res,ans[N];
struct Node{
	int l,r,id;
}op[N];
void work1()
{
	int i,j,l,r;
	ll res;
	for (i=1;i<=n;i++)
	{
		for (j=0;j<=9;j++)
		{
			s[i][j]=s[i-1][j]+(a[i]==j+'0');
			sum[i][j]=sum[i-1][j]+(a[i]==j+'0')*i;
		}
	}
	scan>>m;
	while (m--)
	{
		scan>>l>>r;
		res=0;
		for (j=0;j<=9;j+=p) res+=sum[r][j]-sum[l-1][j]-(l-1)*(s[r][j]-s[l-1][j]);
		printf("%lld\n",res);
	}
}
bool cmp(Node x,Node y)
{
	if (x.l/B!=y.l/B) return x.l/B<y.l/B;
	if ((x.l/B)%2==0) return x.r<y.r;
	return x.r>y.r;
}
void add(int x)
{
	res+=cnt[suf[x]];
	cnt[suf[x]]++;
}
void del(int x)
{
	cnt[suf[x]]--;
	res-=cnt[suf[x]];
}
void work2()
{
	int i,j;
	scan>>m;
	for (i=1;i<=m;i++)
	{
		scan>>op[i].l>>op[i].r;
		op[i].id=i;
		op[i].r++;//这里别忘加!
	}
	sort(op+1,op+m+1,cmp);
	ll t=1;
	for (i=n;i>=1;i--)
	{
		suf[i]=(suf[i+1]+t*(a[i]-'0')%p)%p;
		t=t*10%p;
		b[++k]=suf[i];
	}
	sort(b+1,b+k+2);
	k=unique(b+1,b+k+2)-b-1;
	for (i=1;i<=n+1;i++) suf[i]=lower_bound(b+1,b+k+2,suf[i])-b;
	int x=1,y=0;
	for (i=1;i<=m;i++)
	{
		l=op[i].l;
		r=op[i].r;
		id=op[i].id;
		while (x>l) add(--x);
		while (y<r) add(++y);
		while (x<l) del(x++);
		while (y>r) del(y--);
		ans[id]=res;
	}
	for (i=1;i<=m;i++) printf("%lld\n",ans[i]);
}
int main()
{
	scan>>p;
	scanf("%s",a+1);
	n=strlen(a+1);
	B=sqrt(n);
	if (p==2||p==5) work1();
	else work2();
	return 0;
}

带修莫队

有一个序列 \(a_1,a_2,\dots,a_n\)。给定 \(m\) 组询问,每次询问有两种形式:

\(n,m\leq 10^5\)

  1. \(1\) \(l\) \(r\) 表示查询 \([l,r]\) 区间内元素的种类数。
  2. \(2\) \(x\) \(y\) 表示把 \(a_x\) 修改为 \(y\)

首先考虑暴力。三个指针 \(x,y,z\) 每次移动到对应的需要上。记录每组询问对应的修改个数,并移动。

按照上述方法可以做到 \(O(nm)\)

然后考虑排序。

规则:

\(\lfloor\frac{x.l}{B}\rfloor\neq \lfloor\frac{y.l}{B}\rfloor\),则返回 \(\lfloor\frac{x.l}{B}\rfloor<\lfloor\frac{y.l}{B}\rfloor\)

否则,若 \(\lfloor\frac{x.r}{B}\rfloor\neq \lfloor\frac{y.r}{B}\rfloor\),则返回 \(\lfloor\frac{x.r}{B}\rfloor<\lfloor\frac{y.r}{B}\rfloor\)

否则,返回 \(x.t<y.t\)

然后这样的复杂度为 \(O(n^\frac{5}{3})\)。可以通过。

证明:我也不知道。

移动 \(z\) 的时候有妙处。

首先,如果和此次操作有关,那么预先处理并还原。

真正的修改在最后一句,把这两个互换,就不用考虑顺序问题。把它交换,下一次一定从相反方向过来,还原即可。

P1903 [国家集训队] 数颜色 / 维护队列

AC 记录

#include<bits/stdc++.h>
using namespace std;
/*

*/
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
const int N=1e6+10;
int n,m,i,a[N],B,x,y,z,res,k,p;
int l,r,id,t,ans[N],c[N];
char ch;
struct U{
	int x,y;
}b[N];
struct Q{
	int l,r,id,t;
}op[N];
bool cmp(Q x,Q y)
{
	if (x.l/B!=y.l/B) return x.l/B<y.l/B;
	if (x.r/B!=y.r/B) return x.r/B<y.r/B;
	return x.t<y.t;
}
void add(int x)
{
	if (!c[a[x]]) res++;
	c[a[x]]++;
}
void del(int x)
{
	c[a[x]]--;
	if (!c[a[x]]) res--;
}
void motify(int id,int t)
{
	if (b[t].x>=op[id].l&&b[t].x<=op[id].r)
	{
		int tmp=a[b[t].x];
		del(b[t].x);
		a[b[t].x]=b[t].y;
		add(b[t].x);
		a[b[t].x]=tmp;
	}
	swap(b[t].y,a[b[t].x]);
}
int main()
{
	scan>>n>>m;
	B=pow(n,0.6666);
	for (i=1;i<=n;i++) scan>>a[i];
	for (i=1;i<=m;i++)
	{
		scanf("%s",&ch);
		if (ch=='Q')
		{
			k++;
			scan>>op[k].l>>op[k].r;
			op[k].id=k;
			op[k].t=p;
		}
		else
		{
			p++;
			scan>>b[p].x>>b[p].y;
		}
	}
	sort(op+1,op+k+1,cmp);
	x=1;
	y=0;
	z=0;
	for (i=1;i<=k;i++)
	{
		l=op[i].l;
		r=op[i].r;
		id=op[i].id;
		t=op[i].t;
		while (x>l) add(--x);
		while (y<r) add(++y);
		while (x<l) del(x++);
		while (y>r) del(y--);
		while (z<t) motify(i,++z);
		while (z>t) motify(i,z--);
		ans[id]=res;
	}
	for (i=1;i<=k;i++) printf("%d\n",ans[i]);
	return 0;
}

对于莫队的时间复杂度更加深入的理解

首先的想法是按左端点排序。但这样左端点移动次数是 \(\mathcal O(n)\) 的,右端点是 \(\mathcal O(n^2)\) 的。不太平衡。

考虑按左端点分块。设块长为 \(B\)。那么一共有 \(\frac{n}{B}\) 个块,对于每一个块,按右端点排序,每一个块右端点移动次数是 \(\mathcal O(\frac{n^2}{B})\) 的。对于左端点,对于相邻的两个,差不超过 \(B\),移动 \(\mathcal O(mB)\) 次。对于不同的相邻的块,总移动次数为 \(\mathcal O(\frac{n^2}{B})\) 次。然后总复杂度为 \(\mathcal O(\frac{n^2}{B}+mB)\)

如何使这个值最小?均值不等式。\(\frac{n^2}{B}+mB\geq \sqrt{n^2m}=n\sqrt{m}\),仅当 \(\frac{n^2}{B}=mB\)\(B=\frac{n}{\sqrt{m}}\) 时取等。

最终结论:取 \(B=\frac{n}{\sqrt{m}}\) 最优,时间复杂度为 \(\mathcal O(n\sqrt{m})\)。加上排序就是 \(\mathcal O(n\sqrt{m}+m\log m)\)

如此,奇偶优化本质上只是加速了切换块的过程,每次换块少遍历一次,大概快 \(33\%\)

例题:U709167 【模板】普通莫队&莫队时间测试

回滚莫队

对于有些问题,我们是不好进行删除操作的。此时就需要用到回滚莫队。

回滚莫队通过每次重置左端指针,每次询问重新遍历一遍左半部分,而右半部分有序,从而做到指针永远不会回退。

P5906 【模板】回滚莫队&不删除莫队

#include<bits/stdc++.h>
using namespace std;
/*
11
4 2 2 1 6 10 2 5 1 3 6 
6
6 8
1 5
3 11
9 10
7 11
5 10
*/
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
const int N=2e5+10;
int n,m,B,i,j,k,a[N],b[N],l,r;
int ans[N],last[N],idx[N],cnt;
int ma[N],mi[N],clear[N],c,res,tmp;
int R;
struct Node{
	int l,r,id;
}op[N];
bool cmp(Node x,Node y)
{
	if (idx[x.l]!=idx[y.l]) return idx[x.l]<idx[y.l];
	return x.r<y.r;
}
int calc(int l,int r)
{
	int i,res=0;
	for (i=l;i<=r;i++) last[a[i]]=0;
	for (i=l;i<=r;i++)
	{
		if (!last[a[i]]) last[a[i]]=i;
		else res=max(res,i-last[a[i]]);
	}
	return res;
}
int main()
{
	scan>>n;
	B=sqrt(n)+1;
	for (i=1;i<=n;i++) scan>>a[i],b[i]=a[i],idx[i]=(i-1)/B+1;
	cnt=idx[n];
	sort(b+1,b+n+1);
	k=unique(b+1,b+n+1)-b-1;
	for (i=1;i<=n;i++) a[i]=lower_bound(b+1,b+k+1,a[i])-b;
	scan>>m;
	for (i=1;i<=m;i++) scan>>op[i].l>>op[i].r,op[i].id=i;
	sort(op+1,op+m+1,cmp);
	for (i=1,j=1;j<=cnt;j++)
	{
		R=min(j*B,n);l=R+1;r=l-1;
		c=0;
		res=0;
		for (;idx[op[i].l]==j;i++)
		{
			if (idx[op[i].r]==j)
			{
				ans[op[i].id]=calc(op[i].l,op[i].r);
				continue;
			}
			while (r<op[i].r)
			{
				r++;
				ma[a[r]]=r;
				if (!mi[a[r]]) mi[a[r]]=r,clear[++c]=a[r];
				res=max(res,r-mi[a[r]]);
			}
			tmp=res;
			while (l>op[i].l)
			{
				l--;
				if (ma[a[l]]) res=max(res,ma[a[l]]-l);
				else ma[a[l]]=l;
			}
			ans[op[i].id]=res;
			while (l<=R)
			{
				if (ma[a[l]]==l) ma[a[l]]=0;
				l++;
			}
			res=tmp;
		}
		for (k=1;k<=c;k++) ma[clear[k]]=mi[clear[k]]=0;
	}
	for (i=1;i<=m;i++) printf("%d\n",ans[i]);
	return 0;
}
posted @ 2026-08-21 16:37  GUO120822  阅读(3)  评论(0)    收藏  举报