zoukankan      html  css  js  c++  java
  • 【学习笔记】莫队入门

    写在前面

    首先,莫队应该很久以前写的,为什么咕到了省选之后呢?主要是 5ab 发现自己连莫队都还不会,所以就来补笔记+补题了。

    u1s1,莫队是一个十分神奇的算法,这是一个离线处理的范例,也是每次考数据结构必卡的。所以学一学莫队没什么坏处,写起来简单易懂,说不定还能卡得过去骗不少分。

    关于莫队

    什么是莫队?按照 LJ 的说法:

    莫队就是优雅的暴力。

    这可能很玄乎,我们先来康一个例子。


    给定序列 (a),求 ([l,r]) 中不同数的数量。

    暴力当然是每一次扫区间,统计是否有相同的数。当然出题人肯定不会让暴力过,所以我们要优化。

    注意到不少的暴力都会重复扫区间,所以我们很自然地就会问:能不能利用前面已经完成计算的结果?这和 KMP 是如出一辙的,当然就连解决方案也是类似的。

    区间移动操作

    假设我们在区间 ([l,r]) 上完成了暴力操作,即得到了区间中每一个数的个数(假设已经完成了离散化,并且记录在 (cnt) 数组中)。接下来我们想要得到区间 ([l^prime, r^prime])(cnt) 数组,我们应该如何操作?

    显而易见,我们可以移动 (r)(r^prime),这中间要经过若干次的增加节点和删除节点的过程。如图:

    同理,我们可以将 (l) 移动到 (l^prime)。这样我们就可以移动区间。这是莫队的核心操作。

    优化,优化,优化!

    现在有一个问题,我们能够这样移动区间,但是并没有效率提升,我们又应该如何优化呢?

    有一个显而易见的优化,那就是离线将左端点升序排序,这样左端点的遍历就是 (mathcal{O}(n)) 的,会节省一点常数。

    但是,只需要一组右端点反复横跳的数据,就能将这个做法卡掉。我们要怎么优化呢?别忘了——分块大法好!


    我们可以对左端点分块,设块大小为 (B),数列长为 (N)。在排序时采用如下策略:

    • 如果左端点都在同一块内,则根据右端点排序;
    • 如果左端点不在同一块内,那么根据左端点排序。

    这样的优点显而易见——既避免了右端点的反复横跳,又利用了排序单调的性质。那么——复杂度如何呢?

    对于第一种情况,则左端点变化最大为 (B),而右端点从 (1)(N)。如果有 (M) 组询问(假设 (N)(M) 同阶),则复杂度为 (mathcal{O}(MB+N))

    对于第二种情况,我们只需要考虑块与块之间的衔接:单次复杂度为 (mathcal{O}(N)),整体复杂度为 (mathcal{O}left(dfrac{NM}{B} ight))

    综合上述两种情况,我们发现,当 (B=sqrt{N}) 时,总复杂度为 (mathcal{O}(Msqrt{N}+N)sim O(Nsqrt{N})),也就是常见的莫队复杂度。

    这就是莫队,一个优化了排序的离线区间算法。

    例题

    数列找不同

    link: https://www.luogu.com.cn/problem/P3901

    一道比较裸的莫队模板题。每一次在更新时检查是否有重复即可。

    Code:

    #include <cstdio>
    #include <cctype>
    #include <algorithm>
    using namespace std;
    
    const int max_n = 100000, max_bk = 400; // 块长
    struct ass
    {
    	int l, r, id;
    
    	bool operator<(const ass& a) const
    	{
    		return ((l / max_bk == a.l / max_bk)? (r < a.r):(l < a.l)); // 莫队排序
    	}
    };
    
    ass qrs[max_n];
    int cnt[max_n+1], a[max_n], mrs = 0; // mrs:出现次数 >= 2 的数的个数
    bool ans[max_n];
    
    #define gc getchar
    inline int read()
    {
    	int c = gc(), t = 1, n = 0;
    	while (isspace(c)) { c = gc(); }
    	if (c == '-') { t = -1, c = gc(); }
    	while (isdigit(c)) { n = n * 10 + c - '0', c = gc(); }
    	return n * t;
    }
    #undef gc
    
    inline void add(int id) // 加入
    {
    	cnt[a[id]]++;
    	if (cnt[a[id]] == 2)
    		mrs++;
    }
    
    inline void del(int id) // 移除
    {
    	cnt[a[id]]--;
    	if (cnt[a[id]] == 1)
    		mrs--;
    }
    
    int main()
    {
    	int n = read(), q = read(), cl = 1, cr = 0;
    
    	for (int i = 0; i < n; i++)
    		a[i] = read();
    	
    	for (int i = 0; i < q; i++)
    		qrs[i].l = read() - 1, qrs[i].r = read() - 1, qrs[i].id = i;
    	sort(qrs, qrs + q); // 离线排序询问
    
    	for (int i = 0; i < q; i++)
    	{
    		while (cl > qrs[i].l)
    			add(--cl);
    		while (cr < qrs[i].r)
    			add(++cr);
    		while (cl < qrs[i].l)
    			del(cl++);
    		while (cr > qrs[i].r)
    			del(cr--); // 移动区间
    		
    		ans[qrs[i].id] = !mrs; // 是否两两不同
    	}
    
    	for (int i = 0; i < q; i++) // 重新根据输入顺序输出答案
    	{
    		if (ans[i])
    			puts("Yes");
    		else
    			puts("No");
    	}
    
    	return 0;
    }
    

    小 Z 的袜子

    link: https://www.luogu.com.cn/problem/P1494

    被很多人尊为莫队模板题,但还不是那么明显。需要推一下式子((l) 代表区间长度):

    [egin{aligned} E=&dfrac{sum cnt_i(cnt_i-1)}{l(l-1)}\ =&dfrac{sum cnt_i^2-sum cnt_i}{l^2-l}\ =&dfrac{sum cnt_i^2-l}{l^2-l} end{aligned} ]

    这样,一切都明白了——我们需要维护区间中每一个数出现次数的平方和。这对莫队就是小菜一碟。

    Code:

    #include <cstdio>
    #include <cctype>
    #include <algorithm>
    using namespace std;
    
    typedef long long ll;
    const int max_n = 50000, max_m = 50000, max_bk = 250;
    
    struct ass
    {
    	int l, r, id;
    	bool operator<(const ass& as) const
    	{
    		return (l/max_bk == as.l/max_bk)? (((l/max_bk) & 1)? (r < as.r):(r > as.r))
    		                                 :(l < as.l); // 这是一个奇怪的优化
    	}
    };
    
    int a[max_n], cnt[max_n+1] = {};
    ll afm[max_m], afz[max_m], sum = 0;
    ass qrs[max_m];
    
    ll my_gcd(ll a, ll b) { return b? my_gcd(b, a%b):a; }
    inline ll sq(ll x) { return x * x; }
    
    #define gc getchar
    inline int read()
    {
    	int c = gc(), t = 1, n = 0;
    	while (isspace(c)) { c = gc(); }
    	if (c == '-') { t = -1, c = gc(); }
    	while (isdigit(c)) { n = n * 10 + c - '0', c = gc(); }
    	return n * t;
    }
    #undef gc
    
    inline void add(int id)
    {
    	sum -= sq(cnt[a[id]]);
    	cnt[a[id]]++;
    	sum += sq(cnt[a[id]]);
    }
    // add 和 del 一行之差
    inline void del(int id)
    {
    	sum -= sq(cnt[a[id]]);
    	cnt[a[id]]--;
    	sum += sq(cnt[a[id]]);
    }
    
    int main()
    {
    	int n = read(), m = read(), cl = 1, cr = 0;
    	ll tgc;
    
    	for (int i = 0; i < n; i++)
    		a[i] = read();
    	
    	for (int i = 0; i < m; i++)
    		qrs[i].l = read() - 1, qrs[i].r = read() - 1, qrs[i].id = i;
    	
    	sort(qrs, qrs + m);
    
    	for (int i = 0; i < m; i++)
    	{
    		while (cl > qrs[i].l)
    			add(--cl);
    		while (cr < qrs[i].r)
    			add(++cr);
    		while (cl < qrs[i].l)
    			del(cl++);
    		while (cr > qrs[i].r)
    			del(cr--);
    		
    		afm[qrs[i].id] = 1ll * (qrs[i].r - qrs[i].l) * (qrs[i].r - qrs[i].l + 1);
    		afz[qrs[i].id] = 1ll * sum - (qrs[i].r - qrs[i].l + 1);
    	}
    
    	for (int i = 0; i < m; i++)
    	{
    		if (afz[i] != 0)
    		{
    			tgc = my_gcd(afz[i], afm[i]);
    			printf("%lld/%lld
    ", afz[i] / tgc, afm[i] / tgc);
    		}
    		else
    			puts("0/1"); // 特判
    	}
    
    	return 0;
    }
    

    非常感谢您读完此文章。

    如果您喜欢这篇文章,请点一个赞支持一下博主。

    如果您对此文有意见,欢迎在评论区留下你的看法,5ab 会尽力达到。

    如果您真的不喜欢这篇文章,也请不要喷,5ab 欢迎私信反馈意见。

  • 相关阅读:
    C#通过属性名字符串获取、设置对象属性值
    程序员的入门 简单的编程HelloWord
    第二章
    Winform笔记
    Semantic框架的使用
    Java多线程中锁的理解与使用
    vue实用组件——页面公共头部
    SpringBoot 整合ElasticSearch,实现站内搜索,高亮关键字
    maven出现如下报错Dmaven.multiModuleProjectDirectory system property is not set
    vue中引入bootsrtap,jquery,elementUI
  • 原文地址:https://www.cnblogs.com/5ab-juruo/p/notes-moqueue.html
Copyright © 2011-2022 走看看