莫队算法学习报告

一直听说这个神奇的“据说能解决所有区间问题的算法”,今天学习了一下,可能我只做了几个模板题,觉得不是很难。
我觉得这个写的挺好的:点我
莫队算法说起来也是暴力,只不过利用了分块的思想优雅的把n^2变成了n^1.5,太强了,orz。
用一个小栗子解释一下分块的思想:

有一栋楼有100层高,给你两个鸡蛋,让你最快的找出来鸡蛋在那层楼会摔碎(当然是要找第一个能摔碎的楼层,也就是要找一个k,满足鸡蛋在k层能摔碎,在第k-1层摔不碎)。

应该有很多人的第一想法是二分,当然如果鸡蛋是无限个的话肯定是二分最优,但是只有两个鸡蛋,一开始在50层楼摔碎了,你就剩下一个鸡蛋了,只能从第一层到50层挨着试了,如果鸡蛋在49楼才会摔碎,岂不是很亏。
这时候就用到分块的思想了,虽然二分不行,但是我们可以从上面的做法中总结一些经验,就像刚才说的如果在50楼碎了,傻瓜才会再去51楼试呢,如果50楼不碎相信也没人再去试49楼了吧,也就是说我们在50楼试之后实际上是把100层楼分成了两部分。
但是这样分两部分的话太不稳定了, 有时候两次就可以,有时候要50次,那如果我们试着多分几份呢。想必大家看到这里也都明白了,分成10份最为稳妥,因为分成aqrt(n) 份,每份有sqrt(n)层楼,这样期望次数最少。
莫队算法也是这样,把要查询的区间分成sqrt(n) 份,每份里面分别暴力,因为裸的暴力复杂度是O(n ^ 2)的,那么每份里面的复杂度就是O(n)的了,再加上我们分了sqrt(n)份,总体的复杂度就是O(n * sqrt(n))了。(这个复杂度的证明敢不敢再随便点啊,丢!)。
咳咳,所以外面就知道莫队算法的复杂度大概是O(n * sqrt(n))的了。
然后我在做题的时候发现我做的几个题可以直接复制过来改改条件就好, 所以我觉得这些模板题里面可以提纯一个模板出来。那就先来一道最简单的模板题来开开胃吧
DQUERY - D-query
题意就是给你一个序列,然后m组查询,对于每组查询l,r;找出区间中数的种类(不同的数的数目)。
翻译一下题意就是让你找区间中至少出现一次的数。这个题用线段树也可以做,但是如果要找至少出现三次的,线段树就无能为力了吧。
我们先想到O(n^2)的做法

for(i := 1 -> queryNumber){
    fill(count, 0, sizeof(count));
    for(j := query.l -> query.r){
        count[A[i]]++;
        if(count[A[i]] == 1) ans++;
    }
}

妥妥的超时啊,我们来稍稍优化一下。

procedure add(k) {
    count[A[k]]++;
    if(count[A[k]] == 1) ans++;
}

procedure sub(k) {
    count[A[k]]--;
    if(sount[A[k]] == 0) ans--;
}

for(i := 1 -> queryNumber) {
    while(l < s[i].l) {
        sub(l++);
    }
    while(l > s[i].l) {
        add(--l)
    }
    while(r > s[i].r) {
        sub(r--);
    }
    while(r < s[i].r) {
        add(++r);
    }
    output(ans);
}

(要特别注意上面的 --ll--
这样的话还是O(n^2)的,但是……神奇的莫队思想就要出场了,上面我们写的一点都不用变,只需要改变一下查询顺序,这个O(n^2)就变乘O(n * sqrt(n))了。
怎么改变顺序?
跟我做 :

1 取 t = sqrt(n);
2 对查询区间排序,第一关键字按左端点所在块从小到大,第二关键字按右区间从大到小。也就是将排序的判断条件写成:
if(a.l / t != b.l / t) return a.l < b.l;
return a.r > b.r;

然后就没了。

上面那道题的AC代码

#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#define mo 1000000007
using namespace std;

typedef long long ll;
const int N = 3e5;
ll a[N], sum[N], t;
ll c[N * 10];
struct p {
    ll l, r, id;
} s[N];

int cmp(p a, p b) {
    if(a.l / t != b.l / t) return a.l < b.l;
    return a.r > b.r;
}

int main() {
    int T, n, m;
    memset(c, 0, sizeof(c));
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
        scanf("%lld", &a[i]);
    scanf("%d", &m);
    for(int i = 1; i <= m; i++) {
        scanf("%lld%lld", &s[i].l, &s[i].r);
        s[i].id = i;
    }
    t = (int) (sqrt(n) + 0.5);
    sort(s + 1, s + m + 1, cmp);
    ll ans = 0, l = 0, r = 0;
    for(int i = 1; i <= m; i++) {
        while(l < s[i].l) {
            c[a[l]]--;
            if(c[a[l++]] == 0) ans--;
        }
        while(l > s[i].l){
            c[a[--l]]++;
            if(c[a[l]] == 1) ans++;
        }
        while(r > s[i].r){
            c[a[r]]--;
            if(c[a[r--]] == 0) ans--;
        }
        while(r < s[i].r){
            c[a[++r]]++;
            if(c[a[r]] == 1) ans++;
        }
        sum[s[i].id] = ans;
    }
    for(int i = 1; i <= m; i++) 
        printf("%lld\n", sum[i]);
    return 0;
}

我觉得这就是莫队的模板了吧。只要不涉及修改,而且在知道一段区间[l, r]的结果之后,能快速求得[l, r + 1]和[l - 1, r]的结果的题,都可以用莫队来解决。
比如说据说是莫队算法的“母体”的《小z的袜子》
嗯……我们刚才说到“知道一段区间[l, r]的结果之后,能快速求得[l, r + 1]和[l - 1, r]的结果”,我们来看一下一个区间[l, r]内的结果是怎么来的:
假设一个区间长度为len区间里面有n中袜子,每种的数目是ai。那么就应该是 (c(a1, 2) + c(a2, 2) + ……c(an, 2) ) / c(len, 2);
我们先不管分母,因为分母很好求。只看上面的,假设我们要扩展到r + 1, 如果 第 r + 1只袜子是区间[l, r] 里面的第k中,那么ak就应该加1了。那么现在的公式就是 c(a1, 2) + …… + c(ak + 1, 2) + c(a2, 2) + ……c(an, 2) 。其实就是sum[l, r] - c(ak, 2) + c(ak + 1, 2);化简一下就是 sum[l, r] + ak。这个是可以直接得出来的吧。
代码

#include<stdio.h>
#include<algorithm>
#include<cmath>
#define ai s[i]
using namespace std;

typedef long long ll;
const int N = 1e5;
ll a[N], c[N], t;
struct p{
    ll l, r, id;
}s[N], sum[N];

int cmp(p a, p b){
    if(a.l / t != b.l /t) return a.l < b.l;
    return a.r > b.r;
}

int main(){
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++){
        scanf("%lld\n", &a[i]);
    }
    for(int i = 1; i <= m; i++){
        scanf("%lld%lld", &s[i].l, &s[i].r);
        ai.id = i;
    }
    ll ans = 0;
    int l = 0, r = 0;
    t = (int) (sqrt(n) + 0.5);
    sort(s + 1, s + 1 + m, cmp);
    for(int i = 1; i <= m; i++){//其实和上面那个代码是差不多的,只不过这里会有改变罢了
        while(l > ai.l){
            ans += c[a[--l]];//注意这里,看这里是先计算ans
            c[a[l]]++;//再改变ai的值
        }
        while(l < ai.l){
            c[a[l]]--;//这里是先改变
            if(l) ans -= c[a[l++]];//再计算ans
            else l++;
        }
        while(r < ai.r){
            ans += c[a[++r]];//还有++r和下面的r++
            c[a[r]]++;
        }
        while(r > ai.r){
            c[a[r]]--;//边界问题要搞明白
            ans -= c[a[r--]];
        }
        sum[ai.id].l = ans;
        sum[ai.id].r = (ai.r - ai.l + 1) * (ai.r - ai.l) / 2;
    }
    for(int i = 1; i <= m; i++){
        ll a = sum[i].l, b = sum[i].r;
        printf("%lld/%lld\n", a / __gcd(a, b), b / __gcd(a, b));
    }
    return 0;
}

再来一道题吧,其实这是我做出来的第一道莫队的题。

HYSBZ - 3289

分析:
首先我们能想得到,如果只能交换相邻的数,想要将一个序列排成有序的,那么步数就是这个序列的逆序对数。假如我们知道一个序列的逆序对数,如果在它后面放一个数,就会增加“在这个数前面比他大的数的个数”个逆序对,(哈哈哈,好拗口。)同样的如果在它前面放一个数,就会增加“……比它小的数的个数……”个逆序对,知道了这两个,就相当于知道怎么从[l, r] 扩展到 [l - 1, r] 和 [l, r + 1]了。那么莫队走起!利用树状数组可以logn的时间找到比一个数大或小的个数,记得要离散化

#include<stdio.h>
#include<algorithm>
#include<cmath>
using namespace std;

typedef long long ll;
const int N = 1000005;
struct p{
    ll l, r, id;
}a[N], q[N];
ll s[N], c[N], sum[N];
ll t = 0, tot = 0;
int n, m;

int cmp(p a, p b){
    if(a.l / t != b.l / t) return a.l < b.l;
    return(a.r > b.r);
}

int cmp2(p a, p b){
    return a.l < b.l;
}

void set(int k, int t){
    if(!k) return;
    while(k <= n){
        s[k] += t;
        k += k & (-k);
    }
}

ll query(int k){
    ll ans = 0;
    while(k > 0){
        ans += s[k];
        k -= k & (-k);
    }
    return ans;
}

int main(){
    scanf("%d", &n);
    for(int i = 1; i <= n; i++){
        scanf("%lld", &q[i].l);
        q[i].id = i;
    }
    sort(q + 1, q + n + 1, cmp2);
    for(int i = 1; i <= n; i++){
        if(q[i].l == q[i-1].l && i != 1) 
            c[q[i].id] = tot;
        else 
            c[q[i].id] = ++tot;
    }
    scanf("%d", &m);
    for(int i = 1; i <= m; i++){
        scanf("%lld%lld", &a[i].l, &a[i].r);
        a[i].id = i;
    }
    t = (int) (sqrt(n) + 0.5);
    sort(a + 1, a + 1 + m, cmp);
    ll ans = 0;
    int countl = 0, countr = 0;
    for(int i = 1; i <= m; i++){
        while(countl > a[i].l) {
            ans += query(c[--countl] - 1);
            set(c[countl], 1);
        }
        while(countl < a[i].l) {
            set(c[countl], -1);
            ans -= query(c[countl++] - 1);
        }   
        while(countr < a[i].r) {
            ans += query(tot) - query(c[++countr]);
            set(c[countr], 1);
        }
        while(countr > a[i].r) {
            set(c[countr], -1);
            ans -= query(tot) - query(c[countr--]);
        }
        sum[a[i].id] = ans;
    }
    for(int i = 1; i <= m; i++){
        printf("%d\n", sum[i]);
    }
    return 0;
}
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值