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

Perface

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

普通莫队

  • 例题:给定一个长度为\(N(N\in[1,10^5])\)的数列,有\(M(M\in[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(N\in[1,50\ 000]\)的树,树的每个点有一种颜色;有\(M(M\in[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\n",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(N\log_2N)\)
  • 至于坐标变换,一个比较好处理的方法是第一次直接做;第二次沿直线\(y=x\)翻转,即交换\(x\)\(y\)坐标;第三次沿直线\(x=0\)翻转,即将\(x\)坐标取相反数;第四次再沿直线\(y=x\)翻转。注意只需要做4次,因为边是双向的。
  • 求出所有有用边后,我们再做一波\(Kruskal\),即可在\(O(N\log_2N)\)的复杂度内快乐解决。

转载于:https://www.cnblogs.com/Iking123/p/11178449.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值