莫队入门

莫队是个神奇的东西~~

有了他,离线问答再不怕,\(n\sqrt{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\times 50000\) 嘛,常数国国王也救不了你,除非你用指令集优化,但那不是一般人用的,算了吧。

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

思路三:传说中的莫队!

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

NoNoNo,前面都是打好基础。

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

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

那怎么办?

把询问变成有序的!

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

那就是——

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

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

这样,从一个询问区间移动至下一个,左指针、右指针平均移动了\(\sqrt{n}\) 次,复杂度为\(O(n\sqrt{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\n",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\n",res[i]); //输出
    return 0;
}

2.2 回滚莫队

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

例题:AT1219 歴史の研究

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

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

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

接着,对于每个块内的左端点,假设每个块内的每个左端点都从块右端开始统计,每次都重新开始暴力统计一次,做完每个左端点复杂度\(O(\sqrt{n})\),共\(n\)个左端点,总复杂度\(O(n\sqrt{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\n",ans[i]);
    return 0;
}

3. 结束

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

posted @ 2020-05-04 01:08  Zesty_Fox  阅读(236)  评论(0编辑  收藏  举报