莫队
补一下莫队。
莫队
对于序列上的询问问题,如果 \([l,r]\) 的答案能 \(O(1)\) 扩展到 \([l-1,r],[l+1,r],[l,r-1],[l,r+1]\) ,那么我们就可以用莫队来以 \(O(n\sqrt n)\) 的复杂度离线解决。
实现实际上非常简单,离线所有询问然后排序,对序列分块,以 \(l\) 所在块为第一关键字, \(r\) 为第二关键字排序。
例题:小Z的袜子
这个概率可以直接变成数区间相同元素个数。我们知道如果有 \(n\) 只相同颜色袜子那么贡献就是 \(\frac {n(n-1)}2\) 。实际上莫队扫到每只袜子的时候加上前面所有袜子的数量(就是这只袜子能配多少对)就行了。
一个小优化:奇偶化排序。奇数块 \(r\) 从小到大,偶数块 \(r\) 从大到小。
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
int sq;
struct node{
int l,r,id;
bool operator<(const node& s)const{
if(l/sq!=s.l/sq)return l<s.l;
if((l/sq)&1)return r<s.r;
return r>s.r;
}
}q[50010];
int num[50010],a[50010],n,m,sum;
int ans1[50010],ans2[50010];
void add(int k){
sum+=num[k];num[k]++;
}
void del(int k){
num[k]--;sum-=num[k];
}
int gcd(int a,int b){
if(b==0)return a;else return gcd(b,a%b);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=m;i++){
scanf("%d%d",&q[i].l,&q[i].r);
q[i].id=i;
}sq=sqrt(n);
sort(q+1,q+m+1);
for(int i=1,l=1,r=0;i<=m;i++){
if(q[i].l==q[i].r){
ans2[q[i].id]=1;continue;
}
while(l>q[i].l)add(a[--l]);
while(r<q[i].r)add(a[++r]);
while(l<q[i].l)del(a[l++]);
while(r>q[i].r)del(a[r--]);
ans1[q[i].id]=sum;
ans2[q[i].id]=1ll*(r-l+1)*(r-l)/2;
}
for(int i=1;i<=m;i++){
if(ans1[i]==0)printf("0/1\n");
else{
int d=gcd(ans1[i],ans2[i]);
printf("%d/%d\n",ans1[i]/d,ans2[i]/d);
}
}
return 0;
}
带修莫队
众所周知普通的莫队不支持修改。所以我们给它加上一个时间维。于是每次就有三个关键字可以加减。
实际上设莫队的维数是 \(k\) ,那么莫队的最坏复杂度是 \(O(n^{2-\frac 1k})\) 的。块长是 \(n^{1-\frac 1k}\) 的时候复杂度是最优的。当然某些毒瘤题里常数块长可能跑的更快。
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
int a[2000010],n,m,sq,ans[2000010];
int col[2000010],sum;
struct ques{
int l,r,t,id;
bool operator<(const ques& s)const{
if(l/sq!=s.l/sq)return l<s.l;
if(r/sq!=s.r/sq)return r<s.r;
return t<s.t;
}
}q[2000010];
struct node{
int pos,num;
}rep[2000010];
inline void add(int x){
if(col[x]==0)sum++;col[x]++;
}
inline void del(int x){
col[x]--;if(col[x]==0)sum--;
}
inline void update(int l,int r,int t){
if(l<=rep[t].pos&&rep[t].pos<=r){
del(a[rep[t].pos]);add(rep[t].num);
}swap(a[rep[t].pos],rep[t].num);
}
int main(){
scanf("%d%d",&n,&m);
sq=pow(n,0.666);int cnt=0,ti=0;
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=m;i++){
char ch[5];scanf("%s",ch);
if(ch[0]=='Q'){
cnt++;scanf("%d%d",&q[cnt].l,&q[cnt].r);
q[cnt].t=ti;q[cnt].id=cnt;
}
else{
ti++;scanf("%d%d",&rep[ti].pos,&rep[ti].num);
}
}sort(q+1,q+cnt+1);
for(int i=1,l=1,r=0,t=0;i<=cnt;i++){
while(l>q[i].l)add(a[--l]);
while(r<q[i].r)add(a[++r]);
while(l<q[i].l)del(a[l++]);
while(r>q[i].r)del(a[r--]);
while(t<q[i].t)update(l,r,++t);
while(t>q[i].t)update(l,r,t--);
ans[q[i].id]=sum;
}
for(int i=1;i<=cnt;i++)printf("%d\n",ans[i]);
return 0;
}
回滚莫队
如果一个题看上去很可以莫队,但是删除不太好搞,就可以使用回滚莫队。
回滚莫队,即不删除(当然你也可以不加入是吧)莫队,它的大体步骤是:
- 对序列分块,对询问排序(不能奇偶化)。
- 按顺序处理询问:如果左端点所属块与上一个询问不同,将左端点设为块的右端点 \(+1\) ,右端点设为块的右端点(即初始的空区间)。
- 如果左右端点在同一块,那么直接暴力扫。
- 如果不在同一块,先扩展右端点,然后记录一下当前的状态,再扩展左端点,最后直接还原刚才记录的状态并还原左端点。
复杂度还是 \(O(n\sqrt n)\) 的。
例题:AT1219 歴史の研究
#include <algorithm>
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#define int long long
using namespace std;
int n,m,sq,sum,a[100010],lsh[100010],L[510],R[510];
int ans[100010],cnt[100010],belong[100010];
struct node{
int l,r,id;
bool operator<(const node &s)const{
return belong[l]==belong[s.l]?r<s.r:l<s.l;
}
}q[100010];
int ret[100010];
int solve(int l,int r){
int ans=0;
for(int i=l;i<=r;i++)ret[a[i]]++;
for(int i=l;i<=r;i++)ans=max(ans,lsh[a[i]]*ret[a[i]]);
for(int i=l;i<=r;i++)ret[a[i]]--;
return ans;
}
void add(int x){
cnt[a[x]]++;
sum=max(sum,cnt[a[x]]*lsh[a[x]]);
}
void del(int x){
cnt[a[x]]--;
}
signed main(){
scanf("%lld%lld",&n,&m);sq=sqrt(n);
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);lsh[i]=a[i];
}
sort(lsh+1,lsh+n+1);
int num=unique(lsh+1,lsh+n+1)-lsh-1;
for(int i=1;i<=n;i++)a[i]=lower_bound(lsh+1,lsh+num+1,a[i])-lsh;
for(int i=1;i<=m;i++){
scanf("%lld%lld",&q[i].l,&q[i].r);q[i].id=i;
}
int bl=n/sq;
for(int i=1;i<=bl;i++){
L[i]=(i-1)*sq+1;R[i]=i*sq;
}
R[bl]=n;
for(int i=1;i<=bl;i++){
for(int j=L[i];j<=R[i];j++)belong[j]=i;
}
sort(q+1,q+m+1);
for(int x=1,i=1,l=1,r=0;x<=bl;x++){
memset(cnt,0,sizeof(cnt));
l=R[x]+1;r=R[x];sum=0;
while(belong[q[i].l]==x){
if(belong[q[i].l]==belong[q[i].r])ans[q[i].id]=solve(q[i].l,q[i].r);
//同一块内直接暴力扫
else{
while(q[i].r>r)add(++r);//扩展r
int ret=sum;//记录
while(l>q[i].l)add(--l);//扩展l
ans[q[i].id]=sum;sum=ret;
while(l<=R[x])del(l++);//更新答案并还原状态
}
i++;
}
}
for(int i=1;i<=m;i++)printf("%lld\n",ans[i]);
}
二次离线莫队
普通莫队的复杂度保证在于扩展区间的复杂度很小。但是如果这个复杂度比较大那就寄了。
二次离线莫队通过把扩展区间的过程也离线下来来把复杂度从 \(n\sqrt mf(n)\) 变成 \(nf(n)+n\sqrt m\)。要求信息可减。
对于一次扩展(以扩展右端点为例,左端点是一样的过程):新计算的贡献是 \(r+1\) 对 \([l,r]\) 的贡献,不妨记作 \(f(r+1,[l,r])\)。那么差分一下,就是 \(f(r+1,[1,r])-f(r+1,[1,l-1])\)。第一部分是一个前缀后边第一个点对前缀的贡献,可以 \(O(nf(n))\) 预处理得到。然后模拟莫队的移动端点过程,并把所有形如 \(f(r+1,[1,l-1])\) 的询问挂在 \(l-1\) 上,并在最后扫描线更新答案。为了方便处理,可以差分贡献最后加起来。
例题:P4887 【模板】莫队二次离线(第十四分块(前体)):
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <vector>
#include <cmath>
using namespace std;
int n,sq,m,k,sum,a[100010];
int L[510],R[510],belong[100010];
long long ans[100010];
struct node{
int l,r,id;
bool operator<(const node&s)const{
if(belong[l]==belong[s.l])return r<s.r;
return l<s.l;
}
}q[100010];
vector<int>v;
vector<node>ques[100010];
int cnt[1<<14],pre[100010];
int main(){
scanf("%d%d%d",&n,&m,&k);sq=sqrt(n);
if(k>14){
while(m--)puts("0");return 0;
}
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=m;i++)scanf("%d%d",&q[i].l,&q[i].r),q[i].id=i;
int bl=n/sq;
for(int i=1;i<=bl;i++)L[i]=R[i-1]+1,R[i]=i*sq;R[bl]=n;
for(int i=1;i<=bl;i++){
for(int j=L[i];j<=R[i];j++)belong[j]=i;
}
sort(q+1,q+m+1);
for(int i=0;i<(1<<14);i++)if(__builtin_popcount(i)==k)v.push_back(i);
for(int i=1;i<=n;i++){
for(int x:v)cnt[a[i]^x]++;
pre[i]=cnt[a[i+1]];
}
for(int i=1,l=1,r=0;i<=m;i++){
if(l>q[i].l)ques[r].push_back({q[i].l,l-1,q[i].id});
while(l>q[i].l)l--,ans[q[i].id]-=pre[l-1];
if(r<q[i].r)ques[l-1].push_back({r+1,q[i].r,-q[i].id});
while(r<q[i].r)ans[q[i].id]+=pre[r],r++;
if(l<q[i].l)ques[r].push_back({l,q[i].l-1,-q[i].id});
while(l<q[i].l)ans[q[i].id]+=pre[l-1],l++;
if(r>q[i].r)ques[l-1].push_back({q[i].r+1,r,q[i].id});
while(r>q[i].r)r--,ans[q[i].id]-=pre[r];
}
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++){
for(int x:v)cnt[a[i]^x]++;
for(node q:ques[i]){
for(int j=q.l;j<=q.r;j++){
int tmp=cnt[a[j]];
if(j<=i&&!k)tmp--;
if(q.id<0)ans[-q.id]-=tmp;
else ans[q.id]+=tmp;
}
}
}
for(int i=1;i<=m;i++)ans[q[i].id]+=ans[q[i-1].id];
for(int i=1;i<=m;i++)printf("%lld\n",ans[i]);
return 0;
}
树上莫队
把树拍到括号序上跑莫队。进出的时候都在序列末尾加上 \(x\),并记录每个点的进出时间 \(st_x,ed_x\)。一个区间中如果 \(x\) 出现两次那么是没有贡献的,可以开个 \(vis\) 数组存出现了多少次。然后直接跑莫队即可。对于把链变成区间,分类讨论一下:(假设 \(st_x<st_y\))
- 如果有祖先后代关系(设 \(x\) 为 \(y\) 祖先),则区间为 \([st_x,st_y]\)。
- 否则,区间为 \(ed_x,st_y\),但 \(\text{lca}(x,y)\) 并不在区间里,所以查询的时候要加上 \(\lca{x,y}\) 的贡献。
校内模拟赛的一份丑陋的代码:
/*数据结构很好玩?*/
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
#include <vector>
using namespace std;
int n,m,sq;
struct node{
int v,next;
}edge[1000010];
int t,head[500010],a[500010];
void add(int u,int v){
edge[++t].v=v;edge[t].next=head[u];head[u]=t;
}
int num,dfn[1000010],st[500010],ed[500010];
void dfs(int x,int f){
dfn[++num]=x;st[x]=num;
for(int i=head[x];i;i=edge[i].next){
if(edge[i].v!=f){
dfs(edge[i].v,x);
}
}
dfn[++num]=x;ed[x]=num;
}
namespace LCA{
int num,dfn[500010],size[500010],fa[500010],top[500010],son[500010],dep[500010];
void dfs1(int x,int f){
size[x]=1;fa[x]=f;dep[x]=dep[f]+1;
for(int i=head[x];i;i=edge[i].next){
if(edge[i].v!=f){
dfs1(edge[i].v,x);
if(size[son[x]]<size[edge[i].v])son[x]=edge[i].v;
}
}
}
void dfs2(int x,int f,int tp){
dfn[x]=++num;top[x]=tp;
if(son[x])dfs2(son[x],x,tp);
for(int i=head[x];i;i=edge[i].next){
if(edge[i].v!=f&&edge[i].v!=son[x])dfs2(edge[i].v,x,edge[i].v);
}
}
void pre(){
dfs1(1,0);dfs2(1,0,1);
}
int lca(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
x=fa[top[x]];
}
if(dep[x]>dep[y])swap(x,y);
return x;
}
}
using LCA::lca;
struct Ques{
int l,r,id;
bool operator<(const Ques&s)const{
if(l/sq==s.l/sq){
if((l/sq)&1)return r>s.r;
return r<s.r;
}
return l<s.l;
}
}q[500010];
int ans[500010];
struct Blo{
int sq,cnt[500010],l[1010],r[1010],belong[500010],size[1010];
void build(){
sq=sqrt(n);
for(int i=1;i<=sq;i++){
l[i]=r[i-1]+1;r[i]=l[i]+sq-1;
}
r[sq]=n;
for(int i=1;i<=sq;i++){
for(int j=l[i];j<=r[i];j++)belong[j]=i;
}
}
void add(int x){
cnt[x]++;
if(cnt[x]==1)size[belong[x]]++;
}
void del(int x){
cnt[x]--;
if(cnt[x]==0)size[belong[x]]--;
}
int query(){
int i;
for(i=1;i<=sq;i++){
if(size[i]!=r[i]-l[i]+1)break;
}
for(int j=l[i];j<=r[i];j++)if(!cnt[j])return j;
return n+1;
}
}blo;
bool vis[500010];
void update(int x){
vis[x]=!vis[x];
if(vis[x])blo.add(a[x]);
else blo.del(a[x]);
}
int read(){
int x=0;char ch=getchar();
while(!isdigit(ch))ch=getchar();
while(isdigit(ch))x=10*x+ch-'0',ch=getchar();
return x;
}
void print(int x){
if(x>=10)print(x/10);
putchar(x%10+'0');
}
int main(){
n=read();m=read();sq=2*n/sqrt(m);
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<n;i++){
int u=read(),v=read();
add(u,v);add(v,u);
}
dfs(1,0);LCA::pre();blo.build();
for(int i=1;i<=m;i++){
int u=read(),v=read();
if(st[u]>st[v])swap(u,v);
q[i]={lca(u,v)==u?st[u]:ed[u],st[v],i};
}
sort(q+1,q+m+1);
for(int i=1,l=1,r=0;i<=m;i++){
while(l>q[i].l)update(dfn[--l]);
while(r<q[i].r)update(dfn[++r]);
while(l<q[i].l)update(dfn[l++]);
while(r>q[i].r)update(dfn[r--]);
int lc=lca(dfn[q[i].l],dfn[q[i].r]);
if(lc!=dfn[q[i].l]&&lc!=dfn[q[i].r]){
update(lc);
ans[q[i].id]=blo.query();
update(lc);
}
else ans[q[i].id]=blo.query();
}
for(int i=1;i<=m;i++)print(ans[i]),putchar('\n');
return 0;
}