• 莫队学习笔记


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

    普通莫队

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

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

    大致思路:

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

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

    例题 SP3267 D-query

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

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


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

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

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

    由于左端点在同一个块的时候,右端点最坏会遍历整个序列,总共有 (sqrt n) 个块,所以复杂度是 (mathcal{O}(nsqrt 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
    ",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) 。然后发现就是对于左端点,统计有多少 (xoplus 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 o y) ,设 (fir[x]leq fir[y]) ,在序列上的对应位置有如下规律:

    • 如果 ( ext{lca}(x,y)=x) ,那么对应区间为 ([fir[x],fir[y]])
    • 否则,对应区间为 ([las[x],fir[y]]+ ext{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}(nksqrt n))(mathcal{O}(nk+nsqrt n)) .

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

    [forall xin[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_ioplus 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
    " ); 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
    ",ans[i] );
        return 0;
    }
    
  • 相关阅读:
    NanUI for Winform发布,让Winform界面设计拥有无限可能
    新浪微博.Net SDK第三版源代码和示例【最后一次更新了】
    写个C#命令行参数解析的小工具
    Mac安装Windows 10的简明教程
    自己动手,让Entity Framework Power Tools在VS2015重放光彩
    C++CLI使用.net委托,*Callback注意"this"
    【转】IIS上的反向代理
    asp.net mvc 验证码
    win2008R2 下解决关于mysql odbc无法正常工作问题
    中国健康医学教育网
  • 原文地址:https://www.cnblogs.com/UntitledCpp/p/CaptainMo_Study.html
Copyright © 2020-2023  润新知