莫队学习笔记(更新中)

笔记主要参考了博客莫队算法 --算法竞赛专题解析(26)_罗勇军的博客-CSDN博客

0x00、什么是莫队

罗勇军老师如此说道:莫队算法=离线+暴力+分块

1.离线是什么意思:“离线”是区别于“在线”的概念,“在线”主要体现在交互式问题中,题目要求一问一答,回答了第一个问题,第二个问题才会出现。而“离线”则是一开始就知道所有问题。

2.离线的作用:离线能让我们自由安排回答问题的顺序,从而达到优化算法的目的。莫队算法的核心就是通过对问题的排序,让两个问题之间的“距离”减少。

3.莫队的性能如何:和普通的分块差不多,比不上树状数组和线段树。


0x01、基础莫队算法

例题:[SDOI2009]HH的项链 - 洛谷

解法:用莫队把问题排序,然后双指针解决。(其实代码TLE了,这题正解必须树状数组或者线段树)。

#include <bits/stdc++.h>
using namespace std;
#define For(i, a, b) for (int i = (a); i <= (b); i++)
const int N=1e6+5;
struct node{int L,R,id;} q[N];
int n, m, len, a[N], bl[N], cnt[N], ans[N], curAns; //pos[i]表示a[i]所在分块编号,curAns表示当前种类数

bool cmp(node a,node b){
    if(bl[a.L]!=bl[b.L]) return bl[a.L]<bl[b.L]; //分块不同,L分块小的排前面
    if(bl[a.L]&1) return a.R>b.R; //分块相同,按R位置排序,奇偶方向相反
    return a.R<b.R;
}
inline void add(int x){
    cnt[x]++;
    if(cnt[x]==1) curAns++; //首次出现,种类数+1
}
inline void del(int x){
    cnt[x]--;
    if(cnt[x]==0) curAns--; //首次消失,种类数-1
}
signed main() {
	ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin>>n; len=sqrt(n); //分块长度
    For(i,1,n) cin>>a[i], bl[i]=i/len; //输入数据,记录分块编号
    cin>>m; For(i,1,m) cin>>q[i].L>>q[i].R, q[i].id=i;
    sort(q+1,q+m+1,cmp); //重点:对所有询问排序
    int L=1,R=0; //[L,R]即为所求区间
    For(i,1,m){
        while(L<q[i].L) del(a[L++]);
        while(R<q[i].R) add(a[++R]);
        while(L>q[i].L) add(a[--L]);
        while(R>q[i].R) del(a[R--]);
        ans[q[i].id] = curAns; //记录答案
    }
    For(i,1,m) cout<<ans[i]<<'\n';
}

补充练习题:[国家集训队] 小 Z 的袜子 - 洛谷 

解法:还是同样的莫队,同样的解法,但是这里是求概率,从n个物品中挑两个的种类数是n*(n-1)/2,计算的时候分子分母同时乘上2,最后再统一化简。注意概率为0时需要特判!

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define For(i, a, b) for (int i = (a); i <= (b); i++)
const int N=5e4+5;
struct node{int L,R,id;} q[N];
struct Ans{int x,y;} ans[N], now; //x分子,y分母
int n, m, len, a[N], bl[N], cnt[N];
 
bool cmp(node a,node b){
    if(bl[a.L]!=bl[b.L]) return bl[a.L]<bl[b.L]; //分块不同,L分块小的排前面
    if(bl[a.L]&1) return a.R>b.R; //分块相同,按R位置排序,奇偶方向相反
    return a.R<b.R;
}
inline void add(int x){
    cnt[x]++;
    if(cnt[x]>=2) now.x += (cnt[x]*(cnt[x]-1) - (cnt[x]-1)*(cnt[x]-2)); //加上现在的,减去之前的
}
inline void del(int x){
    cnt[x]--;
    if(cnt[x]>=1) now.x -= ((cnt[x]+1)*cnt[x]-cnt[x]*(cnt[x]-1)); //减去之前的,加上现在的
}
inline int gcd(int a, int b){return b==0 ? a : gcd(b, a%b);}
inline void getans(int x,int y,int id){
    if(x==0) y=1; //概率为0的特判!!
    else{int Gcd = gcd(x,y); x/=Gcd, y/=Gcd;} //求出最简分数
    ans[id].x=x, ans[id].y=y; //记录结果
}
signed main() {
	ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    cin>>n>>m; len=sqrt(n); //分块长度
    For(i,1,n) cin>>a[i], bl[i]=i/len;
    For(i,1,m) cin>>q[i].L>>q[i].R, q[i].id=i;
    sort(q+1,q+m+1,cmp);
 
    int L=1, R=0;
    For(i,1,m){
        while(L<q[i].L) del(a[L++]);
        while(R<q[i].R) add(a[++R]);
        while(L>q[i].L) add(a[--L]);
        while(R>q[i].R) del(a[R--]);
        now.y = (R-L+1)*(R-L);
        getans(now.x,now.y,q[i].id);
    }
    For(i,1,m) cout<<ans[i].x<<'/'<<ans[i].y<<'\n';
}

0x02、带修改的莫队算法

例题:[国家集训队] 数颜色 / 维护队列 - 洛谷

这题可以理解为HH的项链+单点修改。有了修改之后,我们就不能只考虑左右边界,而还要加入另外一个量:时间t。从几何角度理解,这从一个二维问题变成了一个三维问题。从之前的只对L分块,变成要对L、R分块,并且加入关于 t 的排序。排序优先度为L分块 -> R分块 -> t大小。

另外,更重要的一点是,分块大小要从sqrt(n)变为n^(2/3)。

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define For(i, a, b) for (int i = (a); i <= (b); i++)
const int N = 2e6+5;
struct Q{int x,y,pre,id;} q[N];
struct C{int pos,val;} c[N];
int n,len,m,a[N],qn,cn,bl[N];
int cnt[N],ANS,ans[N]; //计数数组,当前答案,答案数组

bool cmp(const Q&a,const Q&b){
	//按照“左块->右块->时间”排序
	return bl[a.x]==bl[b.x] ? bl[a.y]==bl[b.y] ? a.pre<b.pre : bl[a.y]<bl[b.y] : bl[a.x]<bl[b.x];
}
inline void add(int x){if(++cnt[x]==1) ANS++;}
inline void del(int x){if(--cnt[x]==0) ANS--;}
void work(int now,int i){
    if(c[now].pos>=q[i].x && c[now].pos<=q[i].y){
		//如果这个修改的值在[l,r]区间内,则其变化将对答案造成影响
		del(a[c[now].pos]); //删掉原来那个数
		add(c[now].val); //替换成当前这个c的value
    }
    swap(c[now].val, a[c[now].pos]);//因为修改后的下一次操作一定相反(即修改该位置->还原该位置->修改该位置...如此循环),所以只需交换即可,而不需要写两个函数
}

void solve(){
    int l=1,r=0,now=0; //左右指针,修改指针
    For(i,1,qn){
        while(l<q[i].x) del(a[l++]);
        while(l>q[i].x) add(a[--l]);
        while(r<q[i].y) add(a[++r]);
        while(r>q[i].y) del(a[r--]);
        while(now<q[i].pre) work(++now,i);
        while(now>q[i].pre) work(now--,i);
        ans[q[i].id]=ANS;
    }
    For(i,1,qn) cout<<ans[i]<<'\n';
}
signed main() {
	ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin>>n>>m, len=pow(n,2.0/3.0);
    For(i,1,n) cin>>a[i], bl[i]=i/len;

    char op; int x,y;
    For(i,1,m){
        cin>>op>>x>>y;
        if(op=='Q'){
            q[++qn].x=x;
            q[qn].y=y;
            q[qn].pre=cn; //记录上一个修改的位置!!
            q[qn].id=qn; //记录这是第几次修改
        }else{
            c[++cn].pos=x; //修改位置
            c[cn].val=y; //改成的值
        }
    }
    sort(q+1,q+qn+1,cmp);
    solve();
    return 0;
}

 0x03、回滚莫队

普通莫队需要同时实现增和删的操作,但是有的时候,增和删其中一个比较难实现(比如增容易实现,但是删很难),这个时候就要用到回滚莫队。这个特殊的莫队只提供增的操作,不提供删的操作,而是用“回滚”来替代删除。

这部分比较难讲,我认为OI_wiki讲的不错,这里给出链接:回滚莫队 - OI Wiki

 例题:歴史の研究 - 洛谷

题目不难理解,容易发现增操作很容易,但是删操作很难实现,因为维护的最大值,删可能导致最大值改变,使得原本的次大值变成最大值,但是次大值又不知道是多少。 

所以考虑用回滚莫队,只实现增的操作,删交给回滚,这题是回滚莫队板子题,打上板子就能ac。

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define FOR(i, a, b) for (int i = (a); i <= (b); i++)
const int N=1e6+5;
int n,m,len,num,curAns,last;
int a[N], b[N], bl[N], ans[N], cnt[N], cnt1[N]; //a是原始值,b是离散后的值
vector<int> v;
struct Q{int l,r,id;} q[N];
bool operator<(const Q&a,const Q&b){
    return bl[a.l]==bl[b.l] ? a.r<b.r : bl[a.l]<bl[b.l];
}
inline void add(int x){
    cnt[b[x]]++;
    curAns = max(curAns, cnt[b[x]]*a[x]);
}
inline void del(int x){cnt[b[x]]--;}
int check(int l,int r){
    int res = 0;
    FOR(i,l,r) cnt1[b[i]]=0;
    FOR(i,l,r){
        cnt1[b[i]]++;
        res = max(res, cnt1[b[i]]*a[i]);
    }
    return res;
}
inline int Move(int pos,int curBl){
    curAns=last=0; int i=pos; //i表示当前询问的下标
    FOR(j,1,n) cnt[j]= 0; //清空
    int L = min(len*curBl, n); //分块边界
    int l=L+1, r=L; //左右边界的初始位置
    for(; bl[q[i].l]==curBl; i++){ //跑完所有左边界在当前块内的询问
        if(bl[q[i].l] == bl[q[i].r]){ //特殊情况,左右边界在同一块内
            ans[q[i].id] = check(q[i].l, q[i].r); //此时暴力跑
            continue;
        }
        //如果不在同一块中,就用回滚莫队处理
        while(r<q[i].r) r++, add(r); //向右扩展
        last = curAns; //记录last值,表示从边界拓展到这个位置的答案
        while(l>q[i].l) l--, add(l); //向左扩展
        ans[q[i].id] = curAns; //记录答案
        while(l<=L) del(l), l++; //回退
        curAns = last; //答案回滚到last
    }
    return i; //返回第一个越界位置,也就是第一个左边界不在块内的位置
}
signed main() {
	ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin>>n>>m; len = sqrt(n);
    FOR(i,1,n) cin>>a[i], v.push_back(a[i]), bl[i]=(i-1)/len+1;
    int num = bl[n]; //最后一个数字所在块的编号
    //离散化
    sort(v.begin(),v.end());
    v.erase(unique(v.begin(),v.end()),v.end());
    FOR(i,1,n) b[i]=lower_bound(v.begin(),v.end(),a[i])-v.begin()+1;

    FOR(i,1,m) cin>>q[i].l>>q[i].r, q[i].id=i; //输入问题并且编号
    sort(q+1,q+m+1); //对询问排序

    //下面是回滚莫队专属跑法,写法和普通莫队不同
    int pos=1; //从第一个询问开始往后跑
    FOR(i,1,num) //从第一个块跑到最后一个块
        pos = Move(pos,i);
    FOR(i,1,m) cout<<ans[i]<<'\n'; //输出答案
    return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值