莫队学习笔记

其实大部分是暑假就学过的了…… 但是笔记是手写的 这次做个笔记补档,加些题目,然后学个二次离线。

普通莫队

用途是喜闻乐见的区间查询。

其实思想还是分块,不过暴力得很巧妙罢了。

大致思路:

  • 首先对整个序列分块,记录每个位置的所属块
  • 将查询离线下来并排序
  • 按块查询,使用左右指针完成统计

复杂度就是 \(\mathcal{O}(n\sqrt n)\) ,分块复杂度。

例题 SP3267 D-query

给定一个长度为 \(n\) 的序列,询问区间中不同数字个数。

双倍经验: 小B的询问 求区间出现次数平方和。


预处理:将查询离线,对此进行排序,按左端点所在块的编号升序,再按右端点升序。

然后就可以处理询问了。遍历询问序列,建立指针 \(l,r\) 表示当前所在区间, \(cnt[i]\) 记录当前区间每个数的出现次数,\(num\) 表示出现的不同数的总数。初始时 \(l=1,r=0\) .

每次对于一个新的询问,将左右指针 \(l,r\) 移动到和当前查询区间重合,即得到了当前询问的答案。

由于左端点在同一个块的时候,右端点最坏会遍历整个序列,总共有 \(\sqrt n\) 个块,所以复杂度是 \(\mathcal{O}(n\sqrt n)\)

看起来很暴力,但是是真的很优秀(

一个美妙的优化 :当左端点在同一奇数块的时候,右端点升序;如果在同一偶数块则降序。这样可以让右指针在奇数块询问跳完之后,回来的路上顺便跳完偶数块,实际优化还是挺大的。

//Author:RingweEH
//SP3267 DQUERY - D-query
const int N=3e5,Q=2e5+10,C=1e6+10;
int n,a[N],cnt[C],num,bel[N],siz,bnum,ans[Q];

struct Node
{
	int l,r,id;
	bool operator < ( Node tmp ) const  
	{ return (bel[l]^bel[tmp.l]) ? (bel[l]<bel[tmp.l]) : ((bel[l]&1) ? r<tmp.r : r>tmp.r);  }
}Que[Q];

void Init()
{
	siz=sqrt(n); bnum=ceil((double)n/siz);
	for ( int i=1; i<=bnum; i++ )
		for ( int j=(i-1)*siz+1; j<=i*siz; j++ )
			bel[j]=i;
}

void Del( int x ) { cnt[a[x]]--; num-=(cnt[a[x]]==0); }
void Add( int x ) { cnt[a[x]]++; num+=(cnt[a[x]]==1); }

int main()
{
	n=read(); Init();
	for ( int i=1; i<=n; i++ ) a[i]=read();
	int q=read();
	for ( int i=1; i<=q; i++ )
		Que[i].l=read(),Que[i].r=read(),Que[i].id=i;

	sort( Que+1,Que+1+q );
	int l=1,r=0;
	for ( int i=1; i<=q; i++ )
	{
		int ql=Que[i].l,qr=Que[i].r;
		while ( l<ql ) Del(l),l++;
		while ( l>ql ) l--,Add(l);
		while ( r<qr ) r++,Add(r);
		while ( r>qr ) Del(r),r--;
		ans[Que[i].id]=num;
	}

	for ( int i=1; i<=q; i++ )
		printf( "%d\n",ans[i] );

	return 0;
}

习题

小Z的袜子

询问在区间 \([l,r]\) 随机抽取两个数,相同的概率。(分数表示)


简单推一推式子,发现问题可以转化为平方和,然后再平方差公式一下就好了。注意约分。

void Modify( int x,int num )
{
	tmp+=2*sum[c[x]]*num+1; sum[c[x]]+=num;
}

	for ( int i=1; i<=q; i++ )
	{
		while ( l<Que[i].l ) Modify( l++,-1 );
		while ( l>Que[i].l ) Modify( --l,1 );
		while ( r>Que[i].r ) Modify( r--,-1 );
		while ( r<Que[i].r) Modify( ++r,1 );
		ll a=tmp-( Que[i].r-Que[i].l+1 );
		ll b=(ll)( Que[i].r-Que[i].l+1 )*( Que[i].r-Que[i].l );
		ll ggcd=gcd( a,b );
        if ( ggcd==0 )
        {
            ansa[Que[i].id]=0; ansb[Que[i].id]=1; continue;
        }
		ansa[Que[i].id]=a/ggcd; ansb[Que[i].id]=b/ggcd;
	}

CF617E XOR and Favorite Number

求区间中,序列异或和为 \(k\) 的序列个数。


由于实在是太没代码含量了就口胡一下。

显然可以异或前缀,转化为区间有多少数对异或和为 \(k\) 。然后发现就是对于左端点,统计有多少 \(x\oplus k\) ,就是裸题了。

大爷的字符串题

给定字符串,询问区间贡献。

定义贡献:

  • 每次从区间中取出一个严格上升的序列,最少取的次数

题意杀我。

由于是严格上升,所以只和相同的数个数有关,那么最少取的次数就是区间出现次数最大的数的出现次数。

于是就没了。

void Add( int pos )
{
    t[cnt[x[pos]]]--; t[++cnt[x[pos]]]++; res=max( res,cnt[x[pos]] ); 
}

void Del( int pos )
{
    t[cnt[x[pos]]]--; 
    if ( cnt[x[pos]]==res && !t[cnt[x[pos]]] ) res--;
    t[--cnt[x[pos]]]++;
}

带修莫队

其实就是再加一维时间的指针在修改操作上面乱跳(

排序优先级:

  • 左端点所在块标号
  • 右端点
  • 时间

修改:

  • 其实不需要搞什么复杂的操作。把操作和原来的值 swap 即可,改回来同理。

分块:

  • 听说大小设 \(\mathcal{O}(n^{\frac{2}{3}})\) 比较好。

例题 数颜色

区间数颜色,单点修改。


板子题。

尝试了一下毒瘤写法,觉得很离谱

//Author:RingweEH
for ( int i=1; i<=cntq; i++ )
{
    int ql=Que[i].l,qr=Que[i].r,qt=Que[i].tim;
    while ( l<ql ) res-=!--cnt[a[l++]];
    while ( l>ql ) res+=!cnt[a[--l]]++;
    while ( r<qr ) res+=!cnt[a[++r]]++;
    while ( r>qr ) res-=!--cnt[a[r--]];
    while ( tim<qt )
    {
        ++tim;
        if ( ql<=c[tim].pos && c[tim].pos<=qr ) 
            res-=!--cnt[a[c[tim].pos]] - !cnt[c[tim].col]++;
        swap( a[c[tim].pos],c[tim].col );
    }
    while ( tim>qt )
    {
        if ( ql<=c[tim].pos && c[tim].pos<=qr ) 
            res-=!--cnt[a[c[tim].pos]] - !cnt[c[tim].col]++;
        swap( a[c[tim].pos],c[tim].col );
        --tim;
    }
    ans[Que[i].id]=res;
}

习题 CF940F Machine Learning

单点修改,区间查 \(mex\) .


值域很大,有 \(1e9\) ,数组是开不下了,离散化就好。

剩下的事情和区间数颜色没什么区别,直接做就好了。不会求 \(mex\) ?统计颜色个数 \(cnt\) ,再对此统计每个出现次数的出现次数 \(tot\) 什么东西 ,然后对于每个 \(ans\) ,直接暴力找就好了。

为什么能暴力求?考虑答案是 \(x\) ,对于 \(x\) 之前的数,注意是 出现次数 ,仔细想想就会发现不会超过 \(\sqrt n\) ,和莫队复杂度是同级的。


void Add( int x ) { --tot[cnt[x]]; ++tot[++cnt[x]]; }
void Del( int x ) { --tot[cnt[x]]; ++tot[--cnt[x]]; }
void Modify( int t1,int t2 )
{
	if ( c[t1].pos>=q[t2].l && c[t1].pos<=q[t2].r ) Del ( a[c[t1].pos] ),Add( c[t1].x );
	swap( c[t1].x,a[c[t1].pos] );
}

int l=1,r=0;
for ( int i=1; i<=qcnt; 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 ( now<q[i].tim ) Modify( ++now,i );
	while ( now>q[i].tim ) Modify( now--,i );
	for ( ans[q[i].id]=1; tot[ans[q[i].id]]>0; ++ans[q[i].id] );
}

树上莫队

子树统计

算出DFS序即可转化成序列问题。为什么不直接传标记呢

路径统计

还是要转化成序列,不过是括号序( wiki 上说 DFS序 是只记录一次,欧拉序 是每条边都记录一次),也就是进入DFS记一次,出去再记一次。

这样做完之后就可以统计路径了。首先,显然出现两次的数并不在这条路径上,直接忽略。我们设 \(fir[x]\) 为一个点第一次出现在序列中的位置,\(las[x]\) 表示最后一次出现的位置。

不难发现,对于路径 \(x\to y\) ,设 \(fir[x]\leq fir[y]\) ,在序列上的对应位置有如下规律:

  • 如果 \(\text{lca}(x,y)=x\) ,那么对应区间为 \([fir[x],fir[y]]\)
  • 否则,对应区间为 \([las[x],fir[y]]+\text{lca}(x,y)\)

注意每个点都出现了两次,空间要开够。

例题:Count on a tree II

//Author:RingweEH
//SPOJ10707 Count on a tree II
const int N=2e5+1000;
int a[N],cnt[N],fir[N],las[N],bel[N],tmp[N],vis[N],ncnt,ans[N],nw=0;
int ord[N],val[N],head[N],dep[N],fa[N][30],tot=0,n,m;
struct Queries
{
    int l,r,lca,id;
    bool operator < ( Queries tt )
    { return (bel[l]^bel[tt.l]) ? bel[l]<bel[tt.l] : (bel[l]&1) ? r<tt.r : r>tt.r; }
}q[N];

void Addel( int pos )
{
	vis[pos] ? nw-=!--cnt[val[pos]] : nw+=!cnt[val[pos]]++;
    vis[pos]^=1;
}

void Init()
{
    sort( tmp+1,tmp+n+1 );
    int tot1=unique( tmp+1,tmp+1+n )-tmp-1;
    for ( int i=1; i<=n; i++ )
        val[i]=lower_bound( tmp+1,tmp+tot1+1,val[i] )-tmp;
}

int main()
{
    for ( int i=1,l,r,lca; i<=m; i++ )
    {
        l=read(); r=read(); lca=Get_LCA( l,r );
        if ( fir[l]>fir[r] ) swap( l,r );
        if ( l==lca ) q[i].l=fir[l],q[i].r=fir[r];
        else q[i].l=las[l],q[i].r=fir[r],q[i].lca=lca;
        q[i].id=i;
    }
    int l=1,r=0; sort( q+1,q+1+m );
    for ( int i=1; i<=m; i++ )
    {
        int ql=q[i].l,qr=q[i].r,lca=q[i].lca;
        while ( l<ql ) Addel( ord[l++] );
        while ( l>ql ) Addel( ord[--l] );
        while ( r<qr ) Addel( ord[++r] );
        while ( r>qr ) Addel( ord[r--] );
        if ( lca ) Addel( lca );
        ans[q[i].id]=nw;
        if ( lca ) Addel( lca );
    }
}

习题

这是我自己的发明

给定一棵 \(n\) 点树,有点权,初始根为 \(1\) ,支持:

  • 换根
  • 求两个子树中点权相等的点对数

没有想到做第一道 Ynoi 竟然是因为莫队补档……

其实这个换根是假的,在DFS序上,

  • 如果根是 \(u\) ,子树是整个序列
  • 如果根在 \(u\) 在初始时的子树内,那么子树就是整个序列去掉 \(u\) 原来那个子树
  • 否则,子树显然不变。

也就是说,询问要么是一个区间,要么是整个序列去掉某个区间,那么直接容斥,把一个询问拆成四个就能很容易地求解了。

for ( int i=1,opt,rt=1,u,v; i<=m; i++ )
{
    opt=read();
    if ( opt&1 ) { rt=read(); i--; m--; continue; }
    u=read(),v=read();
    int tx=(l[u]<=l[rt] && r[rt]<=r[u]),ty=(l[v]<=l[rt] && r[rt]<=r[v]);
    if ( u==rt ) u=1,tx=0;  //为根直接把区间设为整棵树
    if ( v==rt ) v=1,ty=0;
    if ( tx ) u=mp[u].lower_bound(l[rt])->second;
    if ( ty ) v=mp[v].lower_bound(l[rt])->second;
    int lx=l[u]-1,ly=l[v]-1,rx=r[u],ry=r[v];
    if ( tx && ty )  ans[i]=pre[n];  
    if ( tx ) ans[i]+=(pre[ry]-pre[ly])*(tx==ty ? -1 : 1);
    if ( ty ) ans[i]+=(pre[rx]-pre[lx])*(tx==ty ? -1 : 1); 
	//计算重合区间
    Que[++cntq]=Queries(rx,ry,rx/B,tx==ty ? i : -i);
    Que[++cntq]=Queries(rx,ly,rx/B,tx==ty ? -i : i);
    Que[++cntq]=Queries(lx,ry,lx/B,tx==ty ? -i : i);
    Que[++cntq]=Queries(lx,ly,lx/B,tx==ty ? i : -i);
}
sort( Que+1,Que+1+cntq ); nw=0; memset( c1,0,sizeof(c1) );
int l=1,r=0,ql,qr,qid;
for ( int i=1; i<=cntq; i++ )
{
    ql=Que[i].l; qr=Que[i].r; qid=Que[i].id;
    while ( l<ql ) l++,c1[a[l]]++,nw+=c2[a[l]];
    while ( l>ql ) c1[a[l]]--,nw-=c2[a[l]],l--;
    while ( r<qr ) r++,c2[a[r]]++,nw+=c1[a[r]];
	while ( r>qr ) c2[a[r]]--,nw-=c1[a[r]],r--;
    qid>0 ? ans[qid]+=nw : ans[-qid]-=nw;
}

回滚莫队

主要用途:解决区间端点扩张容易,缩小难维护的情况。

思想就是通过改变维护方式使得区间只增不减。分两类:

  • 左右端点在同一块,直接暴力
  • 左右端点不在同一块,那么显然可以让右端点单调递增,这样右端点就没有删除操作了;对于左端点,在每次开始时直接移到块尾+1,做每个询问时暴力移动到需要的位置,做完之后重新归位到块尾+1即可。

注意写的时候不要把暴力的计数数组和分块的计数数组混起来。

例题:AT1219 经典模板。

//Author:RingweEH
//AT JOISC 2014 C - 歴史の研究
Init();	int pos=1,l,r,ql,qr,qid; ll nw=0,tt;
for ( int k=1; k<=bnum; k++ )
{
	l=rb[k]+1; r=rb[k]; nw=0; memset( cnt,0,sizeof(cnt) );
	for ( ; bel[q[pos].l]==k; pos++ )
	{
		ql=q[pos].l; qr=q[pos].r; qid=q[pos].id;
		if ( bel[ql]==bel[qr] )
		{
			tt=0;
			for ( int j=ql; j<=qr; j++ ) cnt2[b[j]]=0;
			for ( int j=ql; j<=qr; j++ )
				cnt2[b[j]]++,bmax( tt,1ll*cnt2[b[j]]*a[j] );
			ans[qid]=tt; continue;
		}
		while ( r<qr ) ++cnt[b[++r]],bmax(nw,1ll*cnt[b[r]]*a[r]);
		tt=nw;
		while ( l>ql ) ++cnt[b[--l]],bmax(nw,1ll*cnt[b[l]]*a[l]);
		ans[qid]=nw;
		while ( l<rb[k]+1 ) --cnt[b[l++]];
		nw=tt;
	}
}

二次离线莫队

适用范围:

  • 可以莫队
  • 更新答案时间不是 \(\mathcal{O}(1)\) ,一个数的贡献和区间中别的数有关

大体思路:将更新答案的过程再次离线,降低复杂度。假设更新的暴力复杂度为 \(\mathcal{O}(k)\) ,那么和普通莫队相比,是从 \(\mathcal{O}(nk\sqrt n)\)\(\mathcal{O}(nk+n\sqrt n)\) .

\(f(x,[l,r])\) 表示数 \(x\) 对区间 \([l,r]\) 的贡献。考虑端点变化所产生的影响,设从 \([l,r]\to [l,r+k]\) ,也就是说:

\[\forall x\in[r+1,r+k] , f(x,[l,x-1]). \]

差分之后就是 \(f(x,[l,x-1])=f(x,[1,x-1])-f(x,[1,l-1])\) ,转化为一个数对一个前缀的贡献。保存所有询问,从左到右扫描计算即可。但是这样常数巨大,而且空间巨大,非常的不行。

注意到贡献分成两类:

  • \(f(x,[1,x-1])\) 的贡献永远是一个前缀和它后面的一个数的贡献,可以直接预处理
  • \(f(x,[1,l-1])\) 对于一次询问的 \(x\) 都是不变的,那么打标记就可以只标记左右端点,然后最后再扫一遍暴力处理。

这样就能大大优化时空复杂度,具体可以参考模板题的代码理解。

模板题

查询区间内满足 \(count(a_i\oplus a_j)=k\) 的无序数对个数。其中 \(count(x)\) 表示 \(x\) 二进制下 \(1\) 的个数。


利用异或的优良性质,开个桶记录当前前缀中与 \(i\) 异或有 \(k\) 个数位为 \(1\) 的数的个数。每次加入一个数 \(a[i]\),就对所有 \(count(x)=k\)\(x\) 计入贡献,\(++cnt[a[i]\oplus x]\) 即可。

注意你做的是差量,所以最后对答案要来一遍前缀和。

//Author:RingweEH
const int N=1e5+10;
int bel[N],a[N],n,m,k,cnt[N],pre[N];
struct Queries
{
    int l,r,id; ll ans;
    bool operator < ( Queries tmp ) { return (bel[l]^bel[tmp.l]) ? l<tmp.l : r<tmp.r; }
}q[N];
struct Node 
{ 
    int x,y,pos; 
    Node ( int _x=0,int _y=0,int _pos=0 ) : x(_x),y(_y),pos(_pos) {}
};
vector<Node> opt[N];
vector<int> buc;
ll ans[N];

#define lowbit(x) ((x)&(-x))
int Count( int x ) { int res=0; for ( ; x; x-=lowbit(x) ) res++; return res; }
void Init()
{
    for ( int i=0; i<16384; i++ ) 
        if ( Count(i)==k ) buc.push_back(i);
    int siz=sqrt(n);
    for ( int i=1; i<=n; i++ ) bel[i]=(i-1)/siz+1;
    sort( q+1,q+1+m ); memset( cnt,0,sizeof(cnt) );
    for ( int i=1; i<=n; i++ )
    {
        for ( auto x : buc ) cnt[a[i]^x]++;
        pre[i]=cnt[a[i+1]]; //记住你计算的前缀多了1!
    }
}

int main()
{
    n=read(); m=read(); k=read();
    if ( k>14 ) { for ( int i=1; i<=m; i++ ) printf( "0\n" ); return 0; }
    for ( int i=1; i<=n; i++ ) a[i]=read();
    for ( int i=1; i<=m; i++ ) q[i].l=read(),q[i].r=read(),q[i].id=i;

    Init(); int l=1,r=0,ql,qr,qid; memset( cnt,0,sizeof(cnt) );
    for ( int i=1; i<=m; i++ )
    {
        ql=q[i].l; qr=q[i].r;
        //相当于是r不动,计算固定右端点对区间变化的贡献,l同理
        if ( l<ql ) opt[r].push_back( Node(l,ql-1,-i) );  //记录第二类贡献的标记
        while ( l<ql ) q[i].ans+=pre[l-1],l++;    //累计第一类的贡献
        if ( l>ql ) opt[r].push_back( Node(ql,l-1,i) );
        while ( l>ql ) q[i].ans-=pre[l-2],l--;
        if ( r<qr ) opt[l-1].push_back( Node(r+1,qr,-i) );
        while ( r<qr ) q[i].ans+=pre[r],r++;
        if ( r>qr ) opt[l-1].push_back( Node(qr+1,r,i) );
        while ( r>qr ) q[i].ans-=pre[r-1],r--;
    }
    for ( int i=1; i<=n; i++ )      //计算 a[1~i] 对所有后面的区间的贡献,暴力统计
    {
        for ( auto x : buc ) cnt[a[i]^x]++;
        for ( int j=0; j<opt[i].size(); j++ )
        {
            l=opt[i][j].x; r=opt[i][j].y; qid=opt[i][j].pos;
            for ( int j=l,tmp; j<=r; j++ )
            {
                tmp=cnt[a[j]]; tmp-=(j<=i && k==0);
                (qid<0) ? q[-qid].ans-=tmp : q[qid].ans+=tmp;
            }
        }
    }
    for ( int i=1; i<=m; i++ ) q[i].ans+=q[i-1].ans;
    for ( int i=1; i<=m; i++ ) ans[q[i].id]=q[i].ans;
    for ( int i=1; i<=m; i++ ) printf( "%lld\n",ans[i] );
    return 0;
}
posted @ 2021-01-20 11:17  MontesquieuE  阅读(75)  评论(0)    收藏  举报