莫队算法

莫队算法

作用

一个超级强大的暴力,可以处理一大堆区间问题,效率也不低。美中不足的是这是个离线算法(感觉很多强大的算法都是离线……)。

实现

有些区间问题可以在知道[L,R]的情况下用很短的时间(比如 O(1) O(log2(n)) )求出[L-1,R],[L+1,R],[L,R-1],[L,R+1],这样的话莫队算法就有很大的威力了。

比如求[L,R]中不同颜色的个数,一旦颜色数很大,线段树就不行了。而求不同颜色个数能在 O(1) 内完成递推,所以可以这样做:
1.求出[L,R]。
2.不停求[L-1,R],[L+1,R],[L,R-1],[L,R+1]直到求出下一个问题[L ,R]。

怎么样,很暴力吧……但是显然如果出现[1,1]和[n,n]交替的数据,n一大就可以卡死上述做法。所以莫队算法将对上述做法进行优化:分块。

将Q个询问按照L分块,分为[1, n ],( n , 2n ]……然后按R从小到大排序每个块,然后用上述做法处理即可。但复杂度如何保证?证明如下:
计算次数=L移动次数+R移动次数
L移动次数:
1.相同块中,i到i+1最多移动 n 次,总共Q个询问,移动次数为Q* n
2.不同块中,i到i+1最多移动 n 次,总共 n 个块,移动次数为n。
R移动次数:
1.相同块中,总计移动最多n次,总共 n 个块,移动次数为n* n
2.不同块中,i到i+1最多移动n次,总共 n 个块,移动次数为n* n

所以莫队算法的理论复杂度为 O((n+Q)n) ,实际处理起来会小很多,特别是随机数据。

模板

BZOJ1878为例。
就是上面说的问题,不解释了。

#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int maxn=50000,maxm=200000,maxc=1000000;

int n,m,col[maxn+5];
int ans[maxm+5],sum,ha[maxc+5];
struct Ask
{
    int L,R,pa,id;
    bool operator < (const Ask &a) const {return pa<a.pa||pa==a.pa&&R<a.R;}
};
Ask q[maxm+5];

bool Eoln(char ch) {return ch==10||ch==13||ch==EOF;}
int readi(int &x)
{
    int tot=0,f=1;char ch=getchar(),lst='+';
    while ('9'<ch||ch<'0') {if (ch==EOF) return EOF;lst=ch;ch=getchar();}
    if (lst=='-') f=-f;
    while ('0'<=ch&&ch<='9') tot=tot*10+ch-48,ch=getchar();
    x=tot*f;
    return Eoln(ch);
}
void Update(int pos,int tem) //更新
{
    if (!ha[col[pos]]) sum++;
    ha[col[pos]]+=tem;
    if (!ha[col[pos]]) sum--;
}
int main()
{
    freopen("MoTao.in","r",stdin);
    freopen("MoTao.out","w",stdout);
    readi(n);
    for (int i=1;i<=n;i++) readi(col[i]);
    readi(m);
    for (int i=1,sq=sqrt(n);i<=m;i++)
    {
        readi(q[i].L);readi(q[i].R);
        q[i].pa=(q[i].L-1)/sq+1;q[i].id=i;
    }
    sort(q+1,q+1+m);
    sum=0;for (int i=q[1].L;i<=q[1].R;i++) if (!(ha[col[i]]++)) sum++;
    ans[q[1].id]=sum;
    for (int i=2;i<=m;i++)
    {
        int L=q[i-1].L,R=q[i-1].R;
        while (L<q[i].L) Update(L++,-1);
        while (L>q[i].L) Update(--L,1);
        while (R<q[i].R) Update(++R,1);
        while (R>q[i].R) Update(R--,-1);
        //用上一状态求当前状态
        ans[q[i].id]=sum;
    }
    for (int i=1;i<=m;i++) printf("%d\n",ans[i]);
    return 0;
}

有单点修改的莫队算法

作用

可以解决有单点修改的区间问题。

实现

其实想法很简单,就是加一个时间ti,然后排序时按照L所在块,R所在块,ti大小进行排序。每次除了要移动L和R之外,还要移动ti。不过效率好像很神奇,貌似取决于分块的块大小。网上的大神们块大小都取 n23 ,这样时间复杂度就为 O((n+Q)n23) 了,证明如下:
计算次数=L移动次数+R移动次数+ti移动次数
L移动次数:
1.相同L块中,i到i+1最多移动 n23 次,总共Q个询问,移动次数为Q* n23
2.不同L块中,i到i+1最多移动 n23 次,总共 n13 个L块,移动次数为n。
R移动次数:
1.相同R块中,i到i+1最多移动 n23 次,总共Q个询问,移动次数为Q* n23
2.不同R块中,i到i+1最多移动 n23 次,总共 n23 个R块,移动次数为 n43
3.不同L块中,i到i+1最多移动n次,总共 n13 个L块,移动次数为 n43
ti移动次数:
1.相同R块中,总计移动最多n次,总共 n23 个R块,移动次数为 n53
2.不同R块中,i到i+1最多移动n次,总共 n23 个R块,移动次数为 n53
3.不同L块中,i到i+1最多移动n次,总共 n13 个L块,移动次数为 n43

综上所述,当块大小为 n23 时,理论复杂度为 O((n+Q)n23) 。虽然感觉和证明里一连串式子比起来偏差有点大,不过同普通莫对一样,实际处理起来比理论复杂度小很多,不需要太过担心。

模板

BZOJ2120为例。
就是上面说的问题,不解释了。

#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int maxn=10000,maxm=10000,maxc=1000000;

int n,m,mQ,mM,col[maxn+5],lst[maxn+5];
int ans[maxm+5],sum,ha[maxc+5];
struct Query
{
    int L,R,ti,id,paL,paR;
    bool operator < (const Query &a) const
    {
        if (paL<a.paL) return true;
        if (paL==a.paL&&paR<a.paR) return true;
        if (paL==a.paL&&paR==a.paR&&ti<a.ti) return true;
        return false;
    }
};
Query Q[maxm+5];
struct Modify
{
    int pos,col,lst;
};
Modify M[maxm+5];

bool Eoln(char ch) {return ch==10||ch==13||ch==EOF;}
int readi(int &x)
{
    int tot=0,f=1;char ch=getchar(),lst='+';
    while ('9'<ch||ch<'0') {if (ch==EOF) return EOF;lst=ch;ch=getchar();}
    if (lst=='-') f=-f;
    while ('0'<=ch&&ch<='9') tot=tot*10+ch-48,ch=getchar();
    x=tot*f;
    return Eoln(ch);
}
char getrch() {char ch=getchar();while (ch!='Q'&&ch!='R') ch=getchar();return ch;}
void Update(int pos,int tem)
{
    if (!ha[col[pos]]) sum++;
    ha[col[pos]]+=tem;
    if (!ha[col[pos]]) sum--;
}
void Change(int pos,int now,int L,int R) //将pos修改为now
{
    if (L<=pos&&pos<=R) Update(pos,-1),col[pos]=now,Update(pos,1); else
    col[pos]=now;
}
int main()
{
    freopen("MoTao.in","r",stdin);
    freopen("MoTao.out","w",stdout);
    readi(n);readi(m);
    for (int i=1;i<=n;i++) readi(col[i]),lst[i]=col[i];
    for (int i=1,si=pow(n,(double)2/3);i<=m;i++)
    {
        char ch=getrch();int x,y;readi(x);readi(y);
        if (ch=='Q')
        {
            Q[++mQ].L=x;Q[mQ].R=y;Q[mQ].ti=mM;Q[mQ].id=mQ;
            Q[mQ].paL=(x-1)/si+1;Q[mQ].paR=(y-1)/si+1;
        } else M[++mM].pos=x,M[mM].col=y,M[mM].lst=lst[x],lst[x]=y;
        //因为当前x的颜色在改成y之前可能就被改过了,所以要记录lst
    }
    sort(Q+1,Q+1+mQ);
    int L=1,R=1,ti=0;Update(1,1);
    for (int i=1;i<=mQ;i++)
    {
        while (ti<Q[i].ti) ti++,Change(M[ti].pos,M[ti].col,L,R);
        while (ti>Q[i].ti) Change(M[ti].pos,M[ti].lst,L,R),ti--;
        while (L<Q[i].L) Update(L++,-1);
        while (L>Q[i].L) Update(--L,1);
        while (R<Q[i].R) Update(++R,1);
        while (R>Q[i].R) Update(R--,-1);
        ans[Q[i].id]=sum;
    }
    for (int i=1;i<=mQ;i++) printf("%d\n",ans[i]);
    return 0;
}
莫队算法是一种基于分块的算法,用于解决一些静态区间查询问题,时间复杂度为 $O(n\sqrt{n})$。以下是一个基于Python的莫队算法的示例代码: ```python import math # 定义块的大小 BLOCK_SIZE = 0 # 初始化块的大小 def init_block_size(n): global BLOCK_SIZE BLOCK_SIZE = int(math.sqrt(n)) # 定义查询操作 def query(left, right): pass # 在这里写查询操作的代码 # 定义添加操作 def add(x): pass # 在这里写添加操作的代码 # 定义删除操作 def remove(x): pass # 在这里写删除操作的代码 # 定义莫队算法 def mo_algorithm(n, q, queries): init_block_size(n) queries.sort(key=lambda x: (x[0] // BLOCK_SIZE, x[1])) left, right = 0, -1 for query in queries: while left > query[0]: left -= 1 add(left) while right < query[1]: right += 1 add(right) while left < query[0]: remove(left) left += 1 while right > query[1]: remove(right) right -= 1 query(query[0], query[1]) ``` 在这段代码中,我们首先定义了一个全局变量 `BLOCK_SIZE`,用于表示块的大小。接着,我们定义了三个操作函数 `query()`、`add()` 和 `remove()`,分别用于查询、添加和删除元素。在 `mo_algorithm()` 函数中,我们首先调用 `init_block_size()` 函数初始化块的大小,然后将查询操作按照块的大小和右端点排序,接着使用双指针维护当前查询区间的左右端点,每次移动指针时调用 `add()` 和 `remove()` 函数更新块的状态,最后调用 `query()` 函数进行查询操作。 请注意,这段代码只是一个示例,具体的 `query()`、`add()` 和 `remove()` 函数的实现取决于具体的问题。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值