zoukankan      html  css  js  c++  java
  • 莫队入门

    莫队是个神奇的东西~~

    有了他,离线问答再不怕,(nsqrt{n})秒天下~~

    作为一个可与分块相论的优美的暴力,让我们一起学习,一起探索吧!

    好的,废话不多说(这还少吗),让我们进入正题~

    0. 你需要哪些知识

    1. 会写"Hello World!"

    学习莫队的最低要求:

    1. 学的是(C++)(P)党勿入
    2. 会熟练运用(sort)的进阶用法(为自定义结构体排序自定义函数等)

    推荐要求:

    1. 满足最低要求
    2. 熟练运用离散化(因为个别毒瘤的出题人会扩大数据范围)
    3. 会各种卡常技巧

    看来,莫队的要求还是不高的。

    1. 莫队入门

    先来看一道题:P1494 [国家集训队]小Z的袜子

    看到国家集训队,不禁瑟瑟发抖......

    (这还是入门吗......)

    思路一:暴力

    代码就不用放了,相信各位一定能够打出来的!

    最坏时间复杂度:(O(nm)) (当然,如果你能(n)方过百万,就当我没说)

    思路二:优雅一点的暴力

    我们可以思考一下,为什么思路一非常低效?

    因为它计算了重复的元素

    比如,我第一问问到了([1,n]),第二问问到了([1,n-1]),如果按照思路一,第二问就要推倒重算,根本没有利用到第一问所得到的信息。

    那么,怎么才能利用询问得到的信息呢?

    对于每次询问,我们先不直接枚举,而是定义两个指针(l,r),设计数数组为(cnt),存储当前区间([l,r])出现的各个颜色出现的次数。第一次询问之前,初始化(l=1,r=0)(至于为什么,待会儿讲)。

    每一次询问,我们移动(l,r),直至([l,r])与询问区间重合。每次移动后,统计一下移动前与移动后答案的差。

    具体落实到图上,是这样的:(以下图中所标颜色均为离散化后的,当前答案指([l,r])能找到的同色袜子对数)

    就这样,我们避免了区间([5,9])的重复计算。

    回到之前的问题,为什么初始化(l=1,r=0)呢?

    因为如果初始化(l=r=1),实际上([l,r])这个区间里面还有(1)个数,我们还得手动把这个数统计一下,麻烦。

    (其实就是偷懒)

    看起来,我们应该优化了不少(雾)。

    但仔细思考一下,貌似还是不太行。因为最坏情况下,对于每一个询问,(l,r)要移动(n)次。比如这样:

    时间复杂度依然是(O(nm))

    (50000 imes 50000) 嘛,常数国国王也救不了你,除非你用指令集优化,但那不是一般人用的,算了吧。

    所以,我们仍需另辟蹊径。

    思路三:传说中的莫队!

    有人问,现在才步入正题,我前面岂不是白看了?

    NoNoNo,前面都是打好基础。

    我们先思考一下,为什么思路二还是比较低效?

    因为询问序列是无序的,很有可能两个询问区间隔的太远,导致指针来回频繁移动。

    那怎么办?

    把询问变成有序的!

    所以,莫队比起思路二,只差两步。而这两步,是莫队的精髓所在,也是决定莫队效率的关键。

    那就是——

    1. 对整个区间进行分块。
    2. 对询问排序,把询问变得相对有序。排序方法:先按照左端点所属的块从小到大排,再按右端点从小到大排。

    排序完后,按照方法2那样搞就行了。

    这样,从一个询问区间移动至下一个,左指针、右指针平均移动了(sqrt{n}) 次,复杂度为(O(nsqrt{n}))

    (当然,具体时间复杂度取决于块大小,一般取(n^{2 over 3})为宜。

    欢乐的代码时间:

    #include <iostream>
    #include <cstdio>
    #include <algorithm>
    #include <cmath>
    #include <cctype>
    #define re register
    using namespace std;
    int n,m,col[50010],L[50010],R[50010],tot,len,belong[50010];
    long long cnt[50010];
    struct Question{
        long long l,r,ans,index;
    }q[50010];
    inline long long read(){
        int s=0;
        char ch=getchar();
        while(!isdigit(ch)){
            ch=getchar();
        }
        while(isdigit(ch)) {
            s=s*10+(ch^48);
            ch=getchar();
        }
        return s;
    }
    
    inline void build(){
        len=sqrt(m);tot=m/len;
        for(re int i=1;i<=tot;i++){
            L[i]=R[i-1]+1,R[i]=L[i]+len-1;
        }
        if(R[tot]<m) L[++tot]=R[tot-1]+1,R[tot]=m;
        for(re int i=1;i<=tot;i++){
            for(int j=L[i];j<=R[i];j++) belong[j]=i;
        }
    }
    
    inline bool cmp(const Question& a,const Question& b){
        return (belong[a.l]^belong[b.l])?belong[a.l]<belong[b.l]:((belong[a.l]&1)?a.r<b.r:a.r>b.r);//排序
    }
    
    inline bool cmp1(const Question& a,const Question& b){
        return a.index<b.index;//要按询问顺序输出答案
    }
    
    long long gcd(long long a,long long b){
        return b?gcd(b,a%b):a;
    }
    
    int main(){
        scanf("%d%d",&n,&m);
        for(re int i=1;i<=n;i++){
            col[i]=read();
        }
        for(re int i=1;i<=m;i++){
            q[i].l=read(),q[i].r=read();
            q[i].index=i;
        }
        build();
        sort(q+1,q+m+1,cmp);
        int l=1,r=0,ans=0;
        for(re int i=1;i<=m;i++){
            while(l<q[i].l){
                ans-=cnt[col[l]]*(cnt[col[l]]-1)/2-(cnt[col[l]]-1)*(cnt[col[l]]-2)/2;
                cnt[col[l++]]--;
            }
            while(l>q[i].l){
                cnt[col[--l]]++;
                ans+=cnt[col[l]]*(cnt[col[l]]-1)/2-(cnt[col[l]]-1)*(cnt[col[l]]-2)/2;
            }
            while(r<q[i].r){
                cnt[col[++r]]++;
                ans+=cnt[col[r]]*(cnt[col[r]]-1)/2-(cnt[col[r]]-1)*(cnt[col[r]]-2)/2;
            }
            while(r>q[i].r){
                ans-=cnt[col[r]]*(cnt[col[r]]-1)/2-(cnt[col[r]]-1)*(cnt[col[r]]-2)/2;
                cnt[col[r--]]--;
            }
            q[i].ans=ans;
        }
        sort(q+1,q+m+1,cmp1);
        for(re int i=1;i<=m;i++){
            long long x=(q[i].r-q[i].l+1)*(q[i].r-q[i].l);
            if(x==0) puts("0/1");
            else{
                long long xy=gcd(x,q[i].ans*2);
                printf("%lld/%lld
    ",q[i].ans*2/xy,x/xy);
            }
        }
        return 0;
    }
    

    2.莫队进阶

    2.1 带修莫队

    你以为到这里就结束了?当然不是。

    先来看一个问题:P1903 [国家集训队]数颜色 / 维护队列

    我们发现,中间涉及到了对序列的修改,显然不可能用只适用于静态问题的普通莫队来解决。

    难道莫队就无用武之地了吗?当然不是。

    带修莫队就此登场!思路如下:

    首先,我们记录每一次询问前最后一次修改的序号(以下称为时间戳)。同时,按照输入顺序执行每一次修改,并同时记录该修改所造成的影响(在本题中,影响指该修改对应位置执行前后的数值变化)

    接下来,进行关键的莫队环节。我先排序,记得还要判断询问区间时间戳的大小关系。们除了定义(l,r)两个指针以外,还要定义一个时间戳指针(t)。每一次询问,我们移动(l,r,t),(当(t)减小就撤回对应修改操作,当(t)增加就执行对应修改操作)直至([l,r,t])与询问区间重合。每次移动后,统计一下移动前与移动后答案的差,保存即可。

    说白了,就是在原来莫队的基础上加了一维,变成了三维空间的莫队而已。

    Code:

    #include <iostream>
    #include <cstdio>
    #include <cmath>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    int n,m,cnt[1000005],val[1000005],tmp[1000005],res[1000005];
    int totm,totq,ans;
    int len,tot,L[10005],R[10005],belong[1000005];
    struct Question{
        int l,r,ti,id;
    }q[1000005];
    struct Modify{
        int x,old,ne;
    }mo[1000005];
    void build(){
        len=pow(n,2.0/3);tot=n/len;
        for(int i=1;i<=tot;i++){
            L[i]=R[i-1]+1,R[i]=L[i]+len-1;
        }
        if(R[tot]<n) tot++,L[tot]=R[tot-1]+1,R[tot]=n;
        for(int i=1;i<=tot;i++){
            for(int j=L[i];j<=R[i];j++) belong[j]=i;
        }
    }
    
    bool cmp(const Question& a,const Question& b){
        return belong[a.l]!=belong[b.l]?belong[a.l]<belong[b.l]:(belong[a.r]!=belong[b.r]?belong[a.r]<belong[b.r]:a.ti<b.ti); //排序函数
    }
    
    int main(){
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++) 
            scanf("%d",&val[i]);
        memcpy(tmp,val,sizeof(val));
        for(int i=1;i<=m;i++){
            char ch[2];
            int x,y;
            scanf("%s%d%d",ch,&x,&y);
            // totq是当前已读入的询问总数,totm是当前已读入的修改次数
            if(ch[0]=='Q'){
                q[++totq].l=x,q[totq].r=y,q[totq].id=totq,q[totq].ti=totm; //询问区间,保存一下
            }
            else{
                mo[++totm].x=x,mo[totm].old=tmp[x],mo[totm].ne=y,tmp[x]=y; //修改操作,执行当前操作并记录修改位置的前后差异
            }
        }
        memcpy(tmp,val,sizeof(val));
        build(); //分块
        sort(q+1,q+totq+1,cmp);
        int l=1,r=0,ti=0;
        for(int i=1;i<=totq;i++){
            while(r>q[i].r){
                if(cnt[tmp[r]]==1) ans--;
                cnt[tmp[r--]]--;
            }
            while(r<q[i].r){
                cnt[tmp[++r]]++;
                if(cnt[tmp[r]]==1) ans++;
            }
            while(l<q[i].l){
                if(cnt[tmp[l]]==1) ans--;
                cnt[tmp[l++]]--;
            }
            while(l>q[i].l){
                cnt[tmp[--l]]++;
                if(cnt[tmp[l]]==1) ans++;
            }
            while(ti<q[i].ti){ //修改时间戳指针
                ti++;
                if(mo[ti].x<=r&&mo[ti].x>=l){
                    if(cnt[mo[ti].old]==1) ans--;
                    cnt[mo[ti].old]--;
                    if(cnt[mo[ti].ne]==0) ans++;
                    cnt[mo[ti].ne]++;
                }
                tmp[mo[ti].x]=mo[ti].ne;
            }
            while(ti>q[i].ti){ 
                if(mo[ti].x<=r&&mo[ti].x>=l){
                    if(cnt[mo[ti].old]==0) ans++;
                    cnt[mo[ti].old]++;
                    if(cnt[mo[ti].ne]==1) ans--;
                    cnt[mo[ti].ne]--;
                }
                tmp[mo[ti].x]=mo[ti].old;
                ti--;
            }
            res[q[i].id]=ans;
        }
        for(int i=1;i<=totq;i++) printf("%d
    ",res[i]); //输出
        return 0;
    }
    

    2.2 回滚莫队

    有时,我们发现询问区间转移过程中,会遇到添加值困难/减少值困难的情况。这该怎么办呢?

    例题:AT1219 歴史の研究

    我们发现,在本题中,添加一个值挺容易,减少一个值...不会。

    我们观察莫队的性质:左端点在同一块中的所有查询区间右端点单调递增

    这样,对于左端点在同一块中的每个区间,我们可以一次遍历,直接(O(n))解决掉所有的右端点,而且不用减少值。

    接着,对于每个块内的左端点,假设每个块内的每个左端点都从块右端开始统计,每次都重新开始暴力统计一次,做完每个左端点复杂度(O(sqrt{n})),共(n)个左端点,总复杂度(O(nsqrt{n}))

    这两种做法结合起来,便可得到一个另类的莫队算法——

    枚举每个块,先把(l,r)置于块尾+1的位置和块尾。先处理左右端点都在同一个块中的情况。接着,右端点向右暴力搞一通。

    搞完右端点,就该移动左指针了。移动左指针前,先备份记录一下,移动后再复原即可。

    Code:

    #include <bits/stdc++.h>
    using namespace std;
    #define LL long long
    int aa[100005], typ[100005], cnt[100005], cnt2[100005], belong[100005], L[100005], R[100005], a[100005];
    LL ans[100005];
    struct query {
        int l,r,id;
    }q[100005];
    int n,m,size,sum;
    int cmp(query a, query b) {
        return belong[a.l]!=belong[b.l]?belong[a.l]<belong[b.l]:a.r<b.r; 
    }
    int main() {
        scanf("%d%d",&n,&m);
        size=sqrt(n);
        sum=ceil((double) n / size);
        for(int i = 1; i <= sum; i++) {
            L[i] = size*(i-1)+1,R[i] = size*i;
            for(int j=L[i]; j <= R[i]; j++) belong[j] = i;
        }
        R[sum] = n;
        for(int i = 1; i <= n; i++) scanf("%d",&a[i]),aa[i]=a[i];
        sort(a+1,a+n+ 1);
        int tot=unique(a+1, a+n+1)-a-1;
        for(int i=1;i<=n;i++) typ[i]=lower_bound(a + 1, a + tot + 1, aa[i]) - a;
        for(int i=1;i<=m;i++) scanf("%d%d",&q[i].l,&q[i].r),q[i].id=i;
        sort(q + 1, q + m + 1, cmp);
        int i = 1;
        for(int k = 0; k <= sum; k++) {
            int l = R[k] + 1, r = R[k];
            LL now = 0;
            memset(cnt, 0, sizeof(cnt));
            for(;belong[q[i].l]==k;i++) {
                int ql= q[i].l, qr = q[i].r;
                LL tmp;
                if(belong[ql] == belong[qr]) {
                    tmp = 0;
                    for(int j = ql; j <= qr; ++j) cnt2[typ[j]] = 0;
                    for(int j = ql; j <= qr; ++j) {
                        ++cnt2[typ[j]]; tmp = max(tmp, 1ll * cnt2[typ[j]] * aa[j]);
                    }
                    ans[q[i].id] = tmp;
                    continue;
                }
                while(r < qr) {
                    ++cnt[typ[++r]]; now=max(now,(LL)cnt[typ[r]]*aa[r]);
                }
                tmp=now;
                while(l > ql){
                    ++cnt[typ[--l]]; now=max(now,(LL)cnt[typ[l]]*aa[l]);
                } 
                ans[q[i].id] = now;
                while(l < R[k] + 1) {
                    --cnt[typ[l]];
                    l++;
                }
                now=tmp;
            }
        }
        for(int i=1;i<=m;i++) printf("%lld
    ",ans[i]);
        return 0;
    }
    

    3. 结束

    今天先写到这里,有时间再来补充。再见!

  • 相关阅读:
    功能超级丰富的彩色贪吃蛇,有道具,有等级!
    【Android开发经验】LayoutInflater—— 你可能对它并不了解甚至错误使用
    数据库常见面试题总结
    数据结构——算法之(041)(寻找数组中的最大值和最小值)
    Riak VClock
    【面试虐菜】—— JAVA面试题(2)
    【面试虐菜】—— MongoDB知识整理
    【面试虐菜】—— Oracle知识整理《收获,不止Oracle》
    【面试虐菜】—— Oracle知识整理《DBA的思想天空》
    【面试虐菜】—— Oracle中CHAR、VARCHAR的区别
  • 原文地址:https://www.cnblogs.com/acceptedzhs/p/12824973.html
Copyright © 2011-2022 走看看