• 平衡规划后的华丽暴力——莫队算法


    Perface

    • 莫队算法是一种用于处理一类支持离线的区间查询问题(基本上都和颜色有关)的暴力算法。
    • 它的主要思想是暴力查询,每次以尽量少的步数移动答案区间位置,从而达到一个可以接受的时间复杂度。

    普通莫队

    • 例题:给定一个长度为(N(Nin[1,10^5]))的数列,有(M(Min[1,10^5]))个询问,每次询问区间([a_i,b_i])中有多少种不同的数字。
    • 考虑离线。我们可以把整个序列分为若干个大小为(k)块,预处理出每个点属于哪个块。将询问([a,b])(a)所在块为第一关键字、(b)为第二关键字排序,每次算完当前询问答案则暴力跳到下一个询问的区间。

    • 分析一下时间复杂度:对于左端点(l),它每次移动最多移(2k-1)步(从一个块的块首移到下一个块的块尾),而总共移(m)次;对于右端点(r),若(l)在一个块中,(r)最多移(n-1)步,而(l)总共可能在(frac nk)个块中。故为(O(mk+n^2k^{-1}))
    • 这是一个对勾函数,当(k=sqrt{frac{n^2}m}=nm^{-frac 12})时有最小值(O(nm^{frac 12}))。当然,在(n)(m)相近时,也可近似取(k=n^{frac 12})

    • 这个的排序有个小优化:对于询问区间([a,b]),如果(a)在奇数块,按(b)升序排序;否则降序排序。这样就不会每次(l)进入新块时,(r)要从序列末尾跳回来。
    struct query
    {
    	int l,r,i;
    	inline bool operator<(const query a)const
    	{return bel[l]^bel[a.l]?bel[l]<bel[a.l]:bel[l]&1?r<a.r:r>a.r;}
    }q[N];
    
    • 实测似乎可以快(200ms)

    带修莫队

    • 例题:上面的例题加个操作——修改某个位置的数字,然后(n)(m)范围降为(5*10^4)
    • 这看似不能离线了,也不好(CDQ)分治,但我们可以转换思路:记录操作的时间戳(t)。我们可以修改排序方式:将询问([a,b,t])(a)所在块为第一关键字、(b)所在块为第二关键字、(t)为第三关键字排序。然后,同样是每次算完当前询问答案暴力跳到下一个询问的区间,同时计算在这段时间范围内的操作对该询问的影响。
    • 那么结构体就改成这样:
    struct query
    {
    	int l,r,t,i;
    	inline bool operator<(const query a)const
    	{return bel[l]^bel[a.l]?bel[l]<bel[a.l]:bel[r]^bel[a.r]?bel[r]<bel[a.r]:bel[r]&1?t<a.t:t>a.t;}
    }q[N];
    

    • 尝试企图分析时间复杂度。左右两端点的移动步数不变;而对于时间端点(t),若(l)(r)在一定的块中,(t)最多移最大时间(T)步。故为(O(mk+n^2k^{-1}+Tn^2k^{-2}))
    • 这个怎么破?我们对它求导:((mk+n^2k^{-1}+Tn^2k^{-2})'=m-n^2k^{-2}-2Tn^2k^{-3})。当它为(0)时,即为一个极值。假设(m)(n)(T)为同一数量级,我们给它乘一个(frac{k^3}n)变成(k^3-nk-2n^2=0)。那么直接套卡尔丹公式可得实根(k=sqrt[3]{n^2+sqrt{n^4-frac{n^3}{27}}}+sqrt[3]{n^2-sqrt{n^4-frac{n^3}{27}}})。是不是很妙?
    • 我们可以大略地令(k=n^{frac 23}),那么时间复杂度即为(O(mn^{frac 23}))。这已经很优秀了,不必再苛求了。(我不会告诉你我不会化简上面那个式子)

    树上莫队

    • 这是一个很妙的东西。
    • 先说一下欧拉序。我看别人的博客得知欧拉序大致有两种:第一种是括号序,即(dfs)时进/出某个点都将其入队;第二种,则是第一种+某个子节点结束时也将该点入队。这里采用第一种。
    • 我们先求出树的欧拉序,记录每个点进/出时的时间戳(in_x)(out_x)。对于一个询问((x,y)),记(z=lca(x,y)),若(x=z),则(x)(y)的链对应欧拉序中的区间([in_x,in_y]);否则对应区间([out_x,int_y])
    • 这样会有一些点的左右括号都在里面,那些点是不在(x)(y)的链中的,所以我们可以用一个类似异或的东西除去它们;还有,如果(x≠z),对应区间([out_x,int_y])中是没有(z)的,所以我们还要额外算(z)的贡献。

    • 时间复杂度就同普通莫队了。
    • 当然,这个也可以带修,当然是修改点权,复杂度同上。如果它敢修改树的形态,就可以考虑打一个ETT之类的维护一下欧拉序暴力撵标算。

    • 有一道例题:【GMOJ3360】【NOI2013模拟】苹果树,题意大概是:给定一颗大小为(N(Nin[1,50 000])的树,树的每个点有一种颜色;有(M(Min[1,10^5]))个询问,每次询问将颜色(a_i)视为颜色(b_i)的前提下(u_i)(v_i)的链上的颜色种数。

    Code

    #include <iostream>
    #include <cstdio>
    #include <cmath>
    #include <algorithm>
    #define fo(i,a,b) for(i=a;i<=b;i++)
    #define fd(i,a,b) for(i=a;i>=b;i--)
    using namespace std;
    
    const int N=21e4;
    int i,j,n,m,col[N],x,y,tot,tov[N],nex[N],las[N],ti,ord[N],in[N],out[N],dep[N],anc[N][17],sz,bs,bel[N],now,s[N],cnt[N],ans[N];
    struct query
    {
    	int l,r,lca,a,b,i;
    	inline bool operator<(const query a)const
    	{return bel[l]^bel[a.l]?bel[l]<bel[a.l]:bel[l]&1?r<a.r:r>a.r;}
    }q[N];
    
    void read(int&x)
    {
    	char c=getchar(); x=0;
    	for(;!isdigit(c);c=getchar());
    	for(;isdigit(c);c=getchar()) x=x*10+(c^48);
    }
    
    void dfs(int x)
    {
    	ord[in[x]=++ti]=x;
    	for(int i=las[x],y;y=tov[i];i=nex[i])
    		if(y!=anc[x][0])
    		{
    			int j=1,f=anc[y][0]=x;
    			while(f) f=anc[y][j]=anc[f][j-1], j++;
    			dep[y]=dep[x]+1, dfs(y);
    		}
    	ord[out[x]=++ti]=x;
    }
    
    int LCA(int x,int y)
    {
    	int i,fx,fy;
    	if(dep[x]<dep[y]) swap(x,y);
    	fd(i,15,0) if((fx=anc[x][i])&&dep[fx]>=dep[y]) x=fx;
    	fd(i,15,0) if((fx=anc[x][i])^(fy=anc[y][i])) x=fx, y=fy;
    	return x==y?x:anc[x][0];
    }
    
    inline void work(int p)
    {
    	now+=s[p]*(~s[p]?!cnt[col[p]]++:!--cnt[col[p]]);
    	s[p]*=-1;
    }
    
    int main()
    {
    	read(n), read(m);
    	fo(i,1,n) read(col[i]);
    	fo(i,1,n)
    	{
    		read(x), read(y);
    		if(!x|!y) continue;
    		tov[++tot]=y, nex[tot]=las[x], las[x]=tot;
    		tov[++tot]=x, nex[tot]=las[y], las[y]=tot;	
    	}
    	dfs(1);
    	sz=round(ti/sqrt(m)), bs=ceil((double)ti/sz);
    	fo(i,1,bs) fo(j,(i-1)*sz+1,i*sz) bel[j]=i;
    	fo(i,1,m)
    	{
    		read(x), read(y), read(q[i].a), read(q[i].b);
    		if(in[x]>in[y]) swap(x,y);
    		int lca=LCA(x,y);
    		if(x==lca)
    				q[i].l=in[x],  q[i].r=in[y];
    		else	q[i].l=out[x], q[i].r=in[y], q[i].lca=lca;
    		q[i].i=i;
    	}
    	sort(q+1,q+m+1);
    	fo(i,1,ti) s[i]=1;
    	int l=1,r=0;
    	fo(i,1,m)
    	{
    		int ql=q[i].l, qr=q[i].r, lca=q[i].lca;
    		while(l<ql) work(ord[l++]);
    		while(l>ql) work(ord[--l]);
    		while(r<qr) work(ord[++r]);
    		while(r>qr) work(ord[r--]);
    		if(lca) work(lca);
    		ans[q[i].i]=now-(q[i].a^q[i].b&&cnt[q[i].a]&&cnt[q[i].b]);
    		if(lca) work(lca);
    	}
    	fo(i,1,m) printf("%d
    ",ans[i]);
    }
    

    回滚莫队(只增莫队)

    • 这个是普通莫队的升级版,它支持求一些更为奇怪的东西。
    • 比如有道例题: AT1219 [JOI2013]歴史の研究,这题加点容易删点难,而我们每次移动(l)(r)指针又不得不删点。怎么破?
    • 那么我们可以不删点。我们扫一遍每个块,先暴力处理左右端点在一个块的特殊询问(每个(O(k))),然后每次移右端点照样移(反正左端点同块右端点单增,只会加点),仍是(O(n^2k^{-1}));而左端点先以当前块尾+1为起点,先记录一波答案,左移左端点(这样也只会加点),然后下一波询问的时候将左端点变回起点并将答案变回来(当然,也要把左移左端点对桶的影响弄回来),于是这类似删点的操作复杂度也是(O(mk))。那么和普通莫队一眼,取(k=n^{frac 12})即可。
    • 其他的以此类推吧。

    与曼哈顿距离最小生成树有机结合♂♂

    • 如果你觉得分块太玄,太容易T,那么我们可以考虑直接算出移动左/右端点的最小步数和方案。
    • 具体地说,对于一个询问(i[a_i,b_i]),我们定义一个平面直角坐标系上的点(P_i(a_i,b_i)),那么询问(i)移到询问(j)的代价就是(P_i)(P_j)的曼哈顿距离嘛。
    • 那么就做最小生成树嘛,做出来之后按最小生成树的边走,绝对最优,撵爆分块。所以随便上个(Kruskal)或者(Prim)什么的就可以T了。等等,为什么会T?这是完全图啊!!!
    • 但是,真正有用的边是远远小于(O(n^2))的。

    • 有一个结论:以一个点为原点建立直角坐标系,在每45度内只会向距离该点最近的一个点连边。
    • 这个分类讨论一下,随便证明。
    • 这样一来,有用边数就是(O(n))级别的了。

    • 考虑一个点(A(x0,y0))。它可以把平面分为8个部分:
      在这里插入图片描述
    • 我们只需考虑在一块区域内的点,其他区域内的点可以通过坐标变换“移动”到这个区域内。为了方便处理,我们考虑图中的(R1)区域。在(R1)区域内的点(B(x1,y1))满足(x1≥x0∧y1-x1>y0-x0)。那么(|AB|=y1-y0+x1-x0=(x1+y1)-(x0+y0))。在(R1)的区域内距离(A)最近的点也即满足条件的点中(x+y)最小的点。因此我们可以将所有点按(x)坐标排序,再按(y-x)离散,用线段树或者树状数组维护大于当前点的(y-x)的最小的(x+y)对应的点。时间复杂度(O(Nlog_2N))
    • 至于坐标变换,一个比较好处理的方法是第一次直接做;第二次沿直线(y=x)翻转,即交换(x)(y)坐标;第三次沿直线(x=0)翻转,即将(x)坐标取相反数;第四次再沿直线(y=x)翻转。注意只需要做4次,因为边是双向的。
    • 求出所有有用边后,我们再做一波(Kruskal),即可在(O(Nlog_2N))的复杂度内快乐解决。
  • 相关阅读:
    PS封装ES流
    win7无法删除文件夹,提示“找不到该项目”
    声明
    ZR#1005
    ZR#1004
    ZR#1009
    ZR#1008
    ZR#1015
    ZR#1012
    ZR#985
  • 原文地址:https://www.cnblogs.com/Iking123/p/11178449.html
Copyright © 2020-2023  润新知