数据结构 —— 莫队算法 —— 普通莫队

【思想基础】

普通莫队常用于维护区间答案,比如:对于一个长度为 n n n 的序列,给出 m m m 次询问,每次询问区间 [ l , r ] [l,r] [l,r] 内有多少个不同的颜色,其中 n , m &lt; = 100000 n,m&lt;=100000 n,m<=100000.

首先考虑暴力,对于每次询问,遍历一遍 [ l , r ] [l,r] [l,r],这样的时间复杂度是 O ( n ∗ m ) O(n*m) O(nm) 的,最坏时间复杂度肯定会超时,那么考虑换一种方式进行暴力。

定义 q l 、 q r ql、qr qlqr,表示区间 [ q l , q r ] [ql,qr] [ql,qr] 内有多少种颜色,再定义 c n t cnt cnt 数组, c n t [ i ] cnt[i] cnt[i] 表示第 i i i 种颜色在区间 [ q l , q r ] [ql,qr] [ql,qr] 中出现的次数,然后一个个处理询问,对于询问 [ l , r ] [l,r] [l,r],挪动 q l ql ql l , q r l,qr lqr r r r.

以下图为例,进行模拟:
在这里插入图片描述
对于区间 [ q l , q r ] [ql,qr] [ql,qr],初始状态如上,假设蓝色为 1,红色为 2,绿色为 3,那么: c n t 1 = 3 , c n t 2 = 3 , c n t 3 = 1 cnt_1=3,cnt_2=3,cnt_3=1 cnt1=3cnt2=3cnt3=1

在这里插入图片描述
q r qr qr 向右挪一下,那么多了一个绿色,使得: c n t 3 = c n t 3 + 1 = 2 cnt_3=cnt_3+1=2 cnt3=cnt3+1=2

在这里插入图片描述

q r qr qr 继续向右挪动,那么多了一个红色,使得: c n t 2 = c n t 2 + 1 = 4 cnt_2=cnt_2+1=4 cnt2=cnt2+1=4,此时可以发现,右指针 q r qr qr 与询问右端点 r r r 重合,那么可以对左指针进行挪动
在这里插入图片描述
q l ql ql 向右挪动,那么少了一个蓝色,使得: c n t 1 = c n t 1 − 1 = 2 cnt_1=cnt_1-1=2 cnt1=cnt11=2,此时左指针 q l ql ql 与询问左端点 l l l 重合,可得出答案: c n t 1 = 2 , c n t 2 = 4 , c n t 3 = 2 cnt_1=2,cnt_2=4,cnt_3=2 cnt1=2cnt2=4cnt3=2.

通过以上模拟可以发现,每次挪动都是 O ( 1 ) O(1) O(1),每次询问最多挪动 n n n 次,这样时间复杂度依旧是 O ( n ∗ m ) O(n*m) O(nm),但通过对以上过程的模拟可以发现,这样暴力的耗时就消耗在挪动次数上,因此只要让挪动的次数尽可能的少就可以极大的降低时间复杂度。

而要想让挪动次数尽可能的小,可以将 m 次询问全部存储下来,然后按照某种方法进行排序,从而减少挪动次数,但这样的方法是强行离线,然后进行排序,因此普通莫队是不支持修改的。

int l=1,r=0,ans=0;
for(int i=1;i<=m;i++){
    while(l>q[i].l) add(--l);//[l-1,r]
    while(l<q[i].l) del(l++);//[l+1,r]
    while(r<q[i].r) add(++r);//[l,r+1]
    while(r>q[i].r) del(r--);//[l,r-1]
    res[q[i].id]=ans;//存储答案
}

【分块】

对于 n 与 m 同阶的情况,一般设块长度为 n \sqrt n n ,经过排序后,每个块内均摊有 \sqrt n 个询问的 l 左端点,那么显然这 l 个端点的右端点是有序的,最多会移动 n 次,因此对于每个块的时间复杂度是 O ( n ) O(n) O(n),然后有 n \sqrt n n 个块,那么这样的总复杂度为 O ( n n ) O(n\sqrt n) O(nn ),而对于询问 m 特别大的情况, O ( n n ) O(n\sqrt n) O(nn ) 会超时,因此需要用到其他的长度。

设块长度为 S S S,那么对于任意多个在同一块内的询问,挪动的距离就是 n n n,一共有 n S \frac{n}{S} Sn 个块,移动的总次数就是 n 2 S \frac{n^2}{S} Sn2,由于移动时可能会跨块,因此还需要加上一个 O ( m ∗ S ) O(m*S) O(mS) 的复杂度,故而总复杂度为 O ( n 2 S + m ∗ S ) O(\frac{n^2}{S}+m*S) O(Sn2+mS),由于我们需要让这个值尽可能的小,通过简单的数学运算可以得出, S S S n m \frac{n}{\sqrt m} m n 是最优的,此时时间复杂度为 O ( n 2 n m + m ∗ n m ) = O ( n ∗ m ) O(\frac{n^2}{ \frac{n}{\sqrt m}}+m*\frac{n}{\sqrt m})=O(n*\sqrt m) O(m nn2+mm n)=O(nm ),而在随机情况下,块的大小为 n m ∗ 2 3 \frac{n}{\sqrt {m*\frac{2}{3}}} m32 n 是最优的,大约是原来的 0.9 倍。

需要注意的是,分块时块的大小不是固定的,要根据题目具体分析,分析的过程如上面分析 m 极大时的复杂度。

block=n/sqrt(m*2/3*1.0);//分块,不卡常数时
block=sqrt(m*2/3*1.0);//分块,卡常数时

【排序】

m m m 次询问强制离线进行进行排序,一种方法是优先按照左端点进行排序,这样的排序可以保证左端点只会右挪,但右端点最坏的情况还是每次从最前面挪动到最后面,再从最后面挪到最前面,这样的时间复杂度依然是 O ( n ∗ m ) O(n*m) O(nm),因此要考虑一种使左右端点挪动次数尽可能少的排序方法。

考虑将长度为 n n n 的序列分为 n \sqrt n n 个长度为 n \sqrt n n 的块,若左端点在同一个块内,则按照右端点排序,即以左端点所在块为第一关键字,右端点位置为第二关键字。
在这里插入图片描述

bool cmp(node a,node b){//正常排序
    if(a.l/block==b.l/block)//左端点在一个块中
        return a.r<b.r;//按照右端点从小到大排序
    else//左端点不在一个块中
        return a.l/block<b.l/block;//按照块的位置进行排序
}

在这里插入图片描述
正常排序时,由于每个块的右端点都是按照从小到大排序的,当指针跳回左边后处理下一个块又要跳回右边,增加了不必要的移动,因此,此时可以按照奇偶性排序进行优化:当左端点的块为奇数时,右端点按照从小到大排;当左端点的块偶数时,右端点按照从大到小排。这样可以保证指针移到右边不用再跳回左边,减少一半的操作,理论上可以快一倍。

bool cmp(Node a,Node b){//按照奇偶性排序
    if( (a.l/block)==(b.l/block) ){//当左端点位于同一个块时
        if( (a.l/block)%2 )//左端点的块序号为奇数时
            return a.r<b.r;//按照从小到大排
        else//左端点的块序号为偶数时
            return a.r>b.r;//按照从大到小排
    }
    else//当左端点不位于同一个块时
        return a.l<b.l;//按照块的位置进行排序
    //return (a.l/block)^(b.l/block) ? a.l<b.l : ( ((a.l/block)&1)?a.r<b.r:a.r>b.r );
}

【模版】

struct Node{
    int l,r;//询问的左右端点
    int id;//询问的编号
}q[N];
int n,m,a[N];
int block;//分块
int ans,cnt[N];
int res[N];
 
bool cmp(Node a,Node b){//奇偶性排序
    return (a.l/block)^(b.l/block)?a.l<b.l:(((a.l/block)&1)?a.r<b.r:a.r>b.r);
}
void add(int x){//统计新的,根据具体情况修改
    if(!cnt[a[x]])
        ans++;
    cnt[a[x]]++;
}
void del(int x){//减去旧的,根据具体情况修改
    cnt[a[x]]--;
    if(!cnt[a[x]])
        ans--;
}
int main(){
    //序列
    scanf("%d",&n);
    for(int i=1;i<=n;++i)
        scanf("%d",&a[i]);
    //询问
    scanf("%d",&m);
    for(int i=1;i<=m;i++){
        scanf("%d%d",&q[i].l,&q[i].r);
        q[i].id=i;
    }
    block=n/sqrt(m*2/3*1.0);//分块,不卡常数时
    //block=sqrt(m*2/3*1.0);//分块,卡常数时
    sort(q+1,q+m+1,cmp);//对询问进行排序
 
    int l=1,r=0;//左右指针
    for(int i=1;i<=m;i++){
        int ql=q[i].l,qr=q[i].r;//询问的左右端点
        while(l>ql) add(--l);//[l-1,r]
        while(l<ql) del(l++);//[l+1,r]
        while(r<qr) add(++r);//[l,r+1]
        while(r>qr) del(r--);//[l,r-1]
        res[q[i].id]=ans;//获取答案
    }
 
    for(int i=1;i<=m;i++)
        printf("%d\n",res[i]);
 
    return 0;

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值