• 莫队算法学习笔记


    莫队算法

    一句话算法:莫队是一种基于分块和询问排序思想的序列处理算法,因此大部分时间,我们需要离线询问,再对询问按某些优先级排序处理。

    例题

    详情见小Z的袜子,推导过程与莫队无关故略去,这里直接给出结论:
    对于一个询问([l, r])(ans = frac{sumlimits_{i = 1}^{C}cnt[i] * (cnt[i] - 1)}{(r - l + 1)(r - l)}),其中(C)为颜色的值域

    考虑普通暴力:
    维护两个指针(l,r),表示当前处理的区间,每次挪动两指针来靠近询问并修改当前答案
    挪动的时候减掉和加上当前点的贡献的操作相信大家都会,比如对于这个例题,我要向左向右挪动一位,那么对应的程序是这样的

    void modify(int k, int d) {
    	ans -= cnt[col[k]] * (cnt[col[k]] - 1);
    	cnt[col[k]] += d;
    	ans += cnt[col[k]] * (cnt[col[k]] - 1);
    }
    

    其中(d)(1/-1),表示此次挪动是添加还是删除

    显然这样我们还是会被毒瘤出题人卡到(O(nm)),比如如果每次询问都需要把(l,r)指针横跨整个序列地挪动
    考虑对于询问,我们要怎样处理,才能够减小总的时间复杂度,而不是寄希望于数据湿度上呢
    于是莫涛大神提出了一个算法,可以将处理这类问题的复杂度优化到(O(nsqrt n))
    莫队算法就此产生

    算法流程

    首先我们离线所有询问,并把序列按某种方式分块。
    对于我们得到的询问,按第一关键字为左端点所属块的序号,第二关键字为右端点的位置排序。
    对于我们得到的询问,再按暴力的方式去处理它

    这样做就有用吗?对于时间复杂度的优化程度如何?
    下面给出证明。


    时间复杂度证明

    首先对于一次挪动,答案的更新是(O(1))
    对于(l)(r)指针的挪动分别讨论:
    设块大小为(base)

    • 对于(l)指针的挪动,当它不需要跨块的时候,它的单次最坏复杂度是(O(base))的,而需要跨块的时候单次最坏复杂度是(O(2 * base))的,常数(2)可以忽略不计,于是对于总共(m)次的挪动,(l)的挪动是(O(m*base))级别的。
    • 对于(r)指针的挪动,由于(r)指针在对应的(l)指针在同一块内是有序的,故对于每一块内的(l),右端点的挪动最坏情况是(O(n)),而左端点最劣是从头到尾一共(frac{n}{base})块都挪一遍,所以(r)指针的挪动是(O(n * frac{n}{base}))
      左右指针的挪动互不干涉,故总的时间复杂度是两者相加,即(O(m * base + n * frac{n}{base}))
      (m, n)同阶时,(base)(sqrt n)有最优时间复杂度(O(2 * n * sqrt n))

    本题代码:

    #include<bits/stdc++.h>
    #define N (100000 + 10)
    using namespace std;
    typedef long long ll;
    int col[N], n, m, pos[N];
    ll cnt[N], ans;
    ll ans1[N], ans2[N];
    inline int read() {
    	int cnt = 0, f = 1; char c = getchar();
    	while(!isdigit(c)) {if (c == '-') f = -f; c = getchar();}
    	while(isdigit(c)) {cnt = (cnt << 3) + (cnt << 1) + (c ^ 48); c = getchar();}
    	return cnt * f;
    }
    struct Q {
    	int l, r, id;
    }q[N];
    bool cmp (Q a, Q b) {
    	return (pos[a.l] == pos[b.l]) ? a.r < b.r : pos[a.l] < pos[b.l];
    }
    ll gcd(ll a, ll b) {return b ? gcd(b, a % b) : a;}
    void modify(int k, int d) {
    	ans -= cnt[col[k]] * (cnt[col[k]] - 1);
    	cnt[col[k]] += d;
    	ans += cnt[col[k]] * (cnt[col[k]] - 1);
    }
    int main() {
    	n = read(), m = read();
    	for (register int i = 1; i <= n; ++i) col[i] = read();
    	for (register int i = 1; i <= m; ++i) q[i].l = read(), q[i].r = read(), q[i].id = i;
    	int base = sqrt(n);
    	for (register int i = 1; i <= n; ++i) pos[i] = i / base;
    	sort (q + 1, q + m + 1, cmp);
    	int L = 1, R = 1;
    	++cnt[col[1]];
    	for (register int i = 1; i <= m; ++i) {
    		if (q[i].l == q[i].r) {ans1[q[i].id] = 0, ans2[q[i].id] = 1; continue;}
    		while (R < q[i].r) modify(++R, 1);
    		while (R > q[i].r) modify(R--, -1);
    		while (L < q[i].l) modify(L++, -1);
    		while (L > q[i].l) modify(--L, 1);
    		ll len = q[i].r - q[i].l + 1;
    		ll tot = len * (len - 1);
    		ll g = gcd(ans, tot);
    		ans1[q[i].id] = ans / g, ans2[q[i].id] = tot / g;
    	}
    	for (register int i = 1; i <= m; ++i) printf("%lld/%lld
    ", ans1[i], ans2[i]);
    	return 0;
    }
    
  • 相关阅读:
    Windows32位与64位操作系统的区别【转】
    【C#多线程详解】
    auto_ptr
    #if 1......
    vector 向量容器
    删除可视图中的类不能彻底避免它重新被编译
    _tWinMain 与wWinMain 区别
    explicit 用法
    转:atoi函数的实现
    string类的实现
  • 原文地址:https://www.cnblogs.com/kma093/p/12036305.html
Copyright © 2020-2023  润新知