基础莫队例题

引入

看了很多篇解释,我个人理解是莫队是优化后的双指针算法;

比如说我们现在要维护区间内数字出现的次数;

在这里插入图片描述
[ A , B ] [A,B] [A,B]区间转移到 [ C , D ] [C,D] [C,D]区间;

对于 B B B D D D的过程,就是不断的新增数字,如果这个数字的种类是第一次出现,那么就++ans

对于 A A A C C C的过程,就是不断的减少数字,如果这个数字出现的次数已经归零了,那么就--ans


以上的转移可以做到
在这里插入图片描述
但是现在我们的询问是乱序的,那么我们可以将所有的询问存下来,并且排个序;


为了使得时间复杂度降低到 O ( n n ) O(n\sqrt{n}) O(nn ),以左端点的块号作为第一关键字升序排序,以右端点的递增或递减排序(见后面玄学优化解释);

如此我们就可以使得大体有序,从而快速推演;


初始化

在这里插入图片描述

扩展

在这里插入图片描述

玄学优化

一、

在这里插入图片描述

二、

奇偶排序;

具体操作如下;

对于左端点在同一奇数块的区间,右端点按升序排列,反之降序;

而处于不同块的排序和之前一样,按左端点块号升序排序;

代码如下;

bool cmp(Query &p,Query &q){
    //先按左端点所在块排
    if(get(p.l) != get(q.l)) return get(p.l) < get(q.l);
    //如果编号相同说明在同一块
    //奇偶排序
    //奇数升序 偶数降序
    if(get(p.l) & 1) return p.r < q.r;
    return p.r > q.r;
}
它的主要原理便是右指针跳完奇数块往回跳时
在同一个方向能顺路把偶数块跳完;
然后跳完这个偶数块又能顺带把下一个奇数块跳完。
理论上主算法运行时间减半,实际情况有所偏差。

例题

HH的项链

题面

传送门

在这里插入图片描述
在这里插入图片描述

思路

模板题,该讲的上面都讲啦;

这里讲一下块的大小怎么取;


假设块长为 S S S,那么就有 n / S n/S n/S个块;

对于右指针来说,每一个快最坏情况下可能移动 n n n次;

一共 n / S n/S n/S个块;

那么复杂度为 O ( n 2 / S ) O(n^2/S) O(n2/S)

对于左端点来说

块内:每次移动最多不会超过 S S S 次,最坏情况总共 m S mS mS 次;

块间:即这一块最后询问到下一块第一个询问,下一个询问左端点一定是大于这个询问左端点的。所以总复杂度不超过 n n n

只需要使得两边尽可能平均,即满足 n 2 / S = m S n^2/S = mS n2/S=mS

解得 S = n / m S=n/ \sqrt{m} S=n/m


简单一点,直接取最简单的 n \sqrt{n} n 也是可以过的;


至于处理每个询问的那几个while,自己手推一下,很容易理解;

仔细想一想当前的数是取还是删,或者得去操作下一个数

不过要注意x++,++x,x--,--x的区别!

Code

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>

using namespace std;

typedef long long ll;

const int N = 5e4 + 10,M = 1000000 + 10;

int a[N],n,m,ans[2*M];

int cnt[M],len,res;

struct Query{
    int l,r,id;
}query[M*2];

int get(int x){
    return x / len;
}
bool cmp(Query &p,Query &q){
    //先按左端点所在块排
    if(get(p.l) != get(q.l)) return get(p.l) < get(q.l);
    //如果编号相同说明在同一块
    //奇偶排序
    //奇数升序 偶数降序
    if(get(p.l) & 1) return p.r < q.r;
    return p.r > q.r;
}
void add(int x){
    if(cnt[x] == 0) ++res;
    ++cnt[x];
}
void del(int x){
    --cnt[x];
    if(cnt[x] == 0) --res;
}
void solve(){
    cin >> n;
    for(int i=1;i<=n;++i) cin >> a[i];
    cin >> m;
    len = n / sqrt(m);
    //防止毒瘤数据使得len为0
    if(len == 0) len = sqrt(n);
    for(int i=1;i<=m;++i){
        cin >> query[i].l >> query[i].r;
        query[i].id = i;   
    }
    sort(query+1,query+1+m,cmp);
    int L = 1,R = 0;
    for(int i=1;i<=m;++i){
        while(L < query[i].l) del(a[L++]);
        while(L > query[i].l) add(a[--L]);
        while(R < query[i].r) add(a[++R]);
        while(R > query[i].r) del(a[R--]);
        ans[query[i].id] = res;
    }
    for(int i=1;i<=m;++i) cout << ans[i] << '\n';
}

int main(){
    std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    solve();
    return 0;
}

Magic Number Group

题面

传送门
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

题意

简单来说就是给定一个区间,问我们任意取一个数,请问最多能整除这个区间里的多少数;

思路

考虑分解质因数,转化为莫队维护区间众数出现次数的问题;

s u m ( i ) sum(i) sum(i)表示出现i次的数有几个

c n t ( i ) cnt(i) cnt(i)表示i这个数出现的次数

Code

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <vector>

using namespace std;

typedef long long ll;

const int N = 5e4 + 10,M = 1e6;

vector<int> fact[M + 5];//维护每个数的所有质因子

bool vis[M + 5];

void init(){
    //考虑每个数能成为谁的因子
    for(int i=2;i<=M;++i){
        if(vis[i]) continue;
        for(int j=1;j*i<=M;++j){
            vis[j*i] = 1;
            fact[j*i].push_back(i);
        }
    }
}

int len;

int get(int x){
    return x / len;
}
struct Ask{
    int l,r,id;
    bool operator<(const Ask &other)const{
        if(get(l) != get(other.l)){
            return get(l) < get(other.l);
        }
        if(get(l) & 1){
            return r < other.r;
        }
        return r > other.r;
    }
}ask[N];

int n,q,a[N];

//sum[i]表示出现i次的数有几个
//cnt[i]表示i这个数出现的次数
int sum[M + 5],cnt[M + 5],mx;

void add(int x){
    for(auto &p : fact[x]){
        --sum[cnt[p]];
        ++cnt[p];
        ++sum[cnt[p]];
        mx = max(mx,cnt[p]);
    }
}
void del(int x){
    for(auto &p : fact[x]){
        --sum[cnt[p]];
        if(cnt[p] == mx && sum[cnt[p]] == 0){
            --mx;
        }
        --cnt[p];
        ++sum[cnt[p]];
    }
}
int ans[N];
void solve(){
    mx = 0;
    cin >> n >> q;
    for(int i=1;i<=n;++i) cin >> a[i];
    len = sqrt(n);
    int l,r;
    for(int i=1;i<=q;++i){
        cin >> l >> r;
        ask[i] = {l,r,i};
    }
    sort(ask+1,ask+1+q);
    l = 1,r = 0;
    for(int i=1;i<=q;++i){
        while(l < ask[i].l) del(a[l++]);
        while(l > ask[i].l) add(a[--l]);
        while(r < ask[i].r) add(a[++r]);
        while(r > ask[i].r) del(a[r--]);
        ans[ask[i].id] = mx;
    }
    for(int i=1;i<=q;++i){
        cout << ans[i] << '\n';
    }
    //清空操作
    while(l <= r) del(a[l++]);
}

int main(){
    std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    init();
    int t;
    cin >> t;
    while(t--)
        solve();
    return 0;
}

参考资料

dalao1
dalao2
墨巨

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值