【小结】莫队
莫队使用了分块思想。
莫队是一个离线的,快速的维护统计区间信息的数据结构。
普通莫队
有这样一个问题:
有 \(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。
那么如此维护每一次操作即可。
注意:
-
开始的初始状态一般为 \(x=1,y=0\)。
-
最好先加入再删除,要不先删除了有可能会删成 \(-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 数列找不同
#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\) 从大到小排。
这种优化,当且仅当所有询问只在偶数或奇数是无效。
有时优化较大。
#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\)。这是减少的量。
所以计算贡献即可。
#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})\) 卡卡常跑的飞快。
另一种做法是分块套莫队。块长 \(\sqrt{n}\)。莫队时只用修改全局和块内的种类数以及数值的数量。然后查询的时候也是 \(O(\sqrt{n})\) 的。合起来神奇的是 \(O((n+m)\sqrt{n})\) 的。还没写。
#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)\) 的。所以我们可以牺牲查询的时间,换取修改的速度。就是做到时间上的尽可能平衡。
#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\)。
#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)\)。
现在乘法中的变量个数变为两个。
对于每组询问拆成四组询问来做,然后按照权值标记并累加即可。
#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
前缀和,查询异或和相同的点。
注意点:
- 查询 \([l,r]\) 时 \(l\) 减 \(1\)。
- 不要把自己计入。
- 指针初始为 \(x=0,y=-1\)。
#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\) 很大,数组开不下,需要离散化。
#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\) \(l\) \(r\) 表示查询 \([l,r]\) 区间内元素的种类数。
- \(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 [国家集训队] 数颜色 / 维护队列
#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\%\)。
回滚莫队
对于有些问题,我们是不好进行删除操作的。此时就需要用到回滚莫队。
回滚莫队通过每次重置左端指针,每次询问重新遍历一遍左半部分,而右半部分有序,从而做到指针永远不会回退。
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;
}

浙公网安备 33010602011771号