首发于ACM
莫队算法 (Mo's Algorithm)

莫队算法 (Mo's Algorithm)

@莫涛 嗯。。发明该算法的神犇就在这里。。

但刚听说这个词的时候,我一直以为这是提莫队长的缩写来着……

0x00 概论

莫队算法主要是用于离线解决 通常不带修改只有查询的一类区间问题。

以前遇到区间问题的时候一般都是用线段树解决,当然能用线段树解决的问题也在多数。线段树的主要思路就是通过一个左半拉序列[l, mid] 和一个右半拉序列[mid+1, r] 来维护它们的父亲(也就是两条序列接合在一起的完整序列)[l, r],通过层层递归下去从而完成对整体序列的维护。但在有些时候可能会遇到如下的问题:


给定一个大小为N的数组,数组中所有元素的大小a[i]<=N。你需要回答M个查询。每个查询的形式是L,R。你需要回答在范围[ L,R ]中至少重复3次的数字的个数。

假设此时我们已经分别维护好了左右两边序列中每个数字重复的个数,然后开始向上维护,我们就会发现。。。

。。。

我们依然要把每个数字在左右序列中出现的次数加起来才能完成对一个父节点的维护……换而言之,我们并不能在O(1)或者某个很短的时间内完成对线段树单一节点的维护(如果神犇能够无视这一句话请无视这一句话……)。

。。。


这时我们发现如果我们已知一个区间[l,r]的情况,我们可以在O(1)的时间内确定[l,r+1]的情况(只需开一个数组记录每一个数出现的次数,然后将a[r+1]的出现次数加一即可)。这样我们可以在O(N^{2} )的时间内完成对所有区间的扫描。


// 这里有一段N^2的伪代码

接着我们发现,我们不仅可以确定[l, r+1],还可以确定[l+1,r][l,r-1][l-1,r]。这个时候,就可以用到莫队算法啦。

0x01 莫队算法

莫队的精髓就在于,离线得到了一堆需要处理的区间后,合理的安排这些区间计算的次序以得到一个较优的复杂度。

再次假设我们已知区间[l,r],需要计算的区间为[l',r'],由于lr分别只能单步转移,所以需要的时间复杂度为O(\left| L-L' \right|+\left| R-R' \right|)。相当于把两个区间分别看成是平面上的两个整点p1(L,R)p2(L',R'),两点之间的转移开销为两点之间的曼哈顿距离。连接所有点的最优方案为一棵树,那么整体的时间复杂度就是这棵树上所有曼哈顿距离之和。

于是乎最优的复杂度肯定是这棵树是最小生成树的时候,也就是曼哈顿距离最小生成树

但这么打貌似代码复杂度有点大。。而且在实际的转移中肯定会出现分支,需要建边(结构是一棵树),那么有没有什么赛艇的替代品可以少敲代码并在一重循环里完成暴力转移呢?

当然是有的,我们先对序列分块,然后以询问左端点所在的分块的序号为第一关键字右端点的大小为第二关键字进行排序,按照排序好的顺序计算,复杂度就会大大降低。

  • 分块相同时,右端点递增是O(N)的,分块共有O(\sqrt{N} )个,复杂度为O(N^{1.5} )
  • 分块转移时,右端点最多变化N,分块共有O(\sqrt{N} )个,复杂度为O(N^{1.5} )
  • 分块相同时,左端点最多变化\sqrt{N} ,分块转移时,左端点最多变化2\sqrt{N} ,共有N个询问,复杂度为O(N^{1.5} )

所有总时间复杂度就是O(N^{1.5} )

0x02 树上莫队

树上莫队有一种树分块的做法这里不讲(因为我不会。。。),有兴趣可以看看vfk的博客WC 2013 糖果公园 park 题解

还有一种就是先把一棵树变成一条序列,然后直接用莫队做就可以了,比如说下面这棵树

我们把它整理成括号序的形式为:521134432665(也就是在dfs遍历树的时候,将每个结点进栈时记录一次,出栈时记录一次)。

如果要询问a\rightarrow b之间的信息,需要分类讨论:

  • 如果ab的祖先,所求信息即为a,b最后出现位置之间的信息。
  • 如果a不是b的祖先,所求信息即为a最先出现的位置以及b最后出现的位置之间的信息再加上lca(a,b)上的信息。

注意有些节点可能会在括号序中出现两次,说明这个节点在这段过程中入栈后又弹出了,不能计入所求信息(处理的话开个数组异或一下就好了)。

比如说要求4\rightarrow 6之间的信息,这一段信息为 括号序 4326 再加上lca(4,6)=5

然后把剩下的事情交给莫队……

0x03 带修改的莫队

对三元组(l,r,x)进行排序,表示在询问[l,r]之前已经进行了x次修改操作, 同理知道了(l,r,x) ,我们就可以知道(l+1,r,x)(l-1,r,x)(l,r+1,x)(l,r-1,x)(l,r,x+1)(l,r,x-1) 的情况,分块大小设为N^{\frac{2}{3} } ,总时间复杂度为O(N^{\frac{5}{3} } )

(证明略)


0x04 草丛里的莫队

待更,这个版本插了真眼也看不到。(摊手)

0x05 题目&代码

Problem 2038. -- [2009国家集训队]小Z的袜子(hose)

大意是询问区间内任意选两个数为同一个数的概率并化为最简分数。

设在某一区间内共有颜色a1,a2,a3...an,每双袜子的个数为b1,b2,b3...bn

答案为(\sum_{i=1}^{n}{b_{i}(b_{i}-1)/2} )/((R-L+1)(R-L)/2)

化简(\sum_{i=1}^{n}{b_{i}^{2} }-b)/((R-L+1)(R-L)/2)

((\sum_{i=1}^{n}{b_{i}^{2} })-(R-L+1))/((R-L+1)(R-L)/2)

所以只需要用莫队处理每个区间内不同数字的平方和就好了


#include <bits/stdc++.h>
using namespace std;
const int Maxn = 50005;
typedef long long ll;
inline ll sqr(const ll &x) {
	return x * x;
}
inline ll gcd(const ll &a, const ll &b) {
	if (!b) return a;
	else return gcd(b, a % b);
}
inline char get(void) {
	static char buf[1000000], *p1 = buf, *p2 = buf;
	if (p1 == p2) {
		p2 = (p1 = buf) + fread(buf, 1, 1000000, stdin);
		if (p1 == p2) return EOF;
	}
	return *p1++;
}
inline void read(int &x) {
	x = 0; static char c;
	for (; !(c >= '0' && c <= '9'); c = get());
	for (; c >= '0' && c <= '9'; x = x * 10 + c - '0', c = get());
}
int belong[Maxn]; // 每个点的分块预处理
ll ans1[Maxn], ans2[Maxn];
struct Cmd {
	int l, r, id;
	friend bool operator < (const Cmd &a, const Cmd &b) {
		if (belong[a.l] == belong[b.l]) return a.r < b.r; // 右端点大小作为第二关键字
		else return belong[a.l] < belong[b.l]; // 左端点所在分块作为第一关键字
	}
} cmd[Maxn];
int n, m, c[Maxn], sum[Maxn];
inline void upd(ll &now, int p, int v) { // 更新平方和
	now -= sqr(sum[c[p]]);
	sum[c[p]] += v;
	now += sqr(sum[c[p]]);
}
inline void solve(void) {
	int L = 1, R = 0; // [L,R]为当前维护好的区间
	ll now = 0, g; // now为当前区间的答案
	for (int i = 1; i <= m; i++) { // 莫队的主要部分
		for (; L < cmd[i].l; L++) upd(now, L, -1); //    [L+1,R]
		for (; R > cmd[i].r; R--) upd(now, R, -1); //    [L,R-1]
		for (; L > cmd[i].l; L--) upd(now, L - 1, 1); // [L-1,R]
		for (; R < cmd[i].r; R++) upd(now, R + 1, 1); // [L,R+1]
		if (cmd[i].l == cmd[i].r) {
			ans1[cmd[i].id] = 0, ans2[cmd[i].id] = 1;
			continue;
		}
		ans1[cmd[i].id] = now - (cmd[i].r - cmd[i].l + 1);
		ans2[cmd[i].id] = (ll)(cmd[i].r - cmd[i].l) * (cmd[i].r - cmd[i].l + 1);
		g = gcd(ans1[cmd[i].id], ans2[cmd[i].id]);
		ans1[cmd[i].id] /= g;
		ans2[cmd[i].id] /= g;
	}
}
int main(void) {
	//freopen("in.txt", "r", stdin);
	read(n), read(m); int s = sqrt(n);
	for (int i = 1; i <= n; i++) {
		read(c[i]); belong[i] = (i - 1) / s + 1; // 每个点的分块预处理
	}
	for (int i = 1; i <= m; i++) {
		read(cmd[i].l), read(cmd[i].r);
		cmd[i].id = i;
	}
	sort(cmd + 1, cmd + m + 1); // 对区间重新排序
	solve();
	for (int i = 1; i <= m; i++) {
		printf("%lld/%lld\n", ans1[i], ans2[i]);
	}
	return 0;
}

Problem 3289. -- Mato的文件管理

大意是询问将每个询问区间里的数恢复成有序最少需要的移动次数(只能相邻的两个数交换)。

假设区间[l,r]已经有序,再将一个数从左边插入就是按次序依次和比它小的数交换直到它归位,将一个数从右边插入就是按次序和R-L+1-比它小的数(比它大的数)交换直到它归位。

还是可以用莫队解决,其中找比一个数小的数的部分用树状数组。


#include <bits/stdc++.h>

const int Maxn = 50005;
using namespace std;

inline char get(void) {
    static char buf[100000], *p1 = buf, *p2 = buf;
    if (p1 == p2) {
        p2 = (p1 = buf) + fread(buf, 1, 100000, stdin);
        if (p1 == p2) return EOF;
    }
    return *p1++;
}
inline void read(int &x) {
    x = 0; static char c; bool minus = false;
    for (; !(c >= '0' && c <= '9'); c = get()) if (c == '-') minus = true;
    for (; c >= '0' && c <= '9'; x = x * 10 + c - '0', c = get()); if (minus) x = -x;
}

int tr[Maxn];
int a[Maxn], tmp[Maxn], n, m, bel[Maxn];
unsigned now, ans[Maxn];
struct Cmd {
	int l, r, id;
	friend bool operator < (const Cmd &a, const Cmd &b) {
		if (bel[a.l] == bel[b.l]) return a.r < b.r;
		else return bel[a.l] < bel[b.l];
	}
} cmd[Maxn];
inline int find(const int &x) {
	int l = 0, r = n + 1, mid;
	while (l < r - 1) {
		mid = (l + r) >> 1;
		if (x > tmp[mid]) l = mid;
		else r = mid;
	}
	return r;
}
inline void add(int x, int s) {
	for (; x <= n; x += x & (-x))
		tr[x] += s;
}
inline unsigned sum(int x) {
	unsigned s = 0;
	for (; x; x -= x & (-x))
		s += tr[x];
	return s;
}
inline void solve(void) {
	int L = 1, R = 0;
	for (int i = 1; i <= m; i++) {
		while (cmd[i].l > L) add(a[L], -1), now -= sum(a[L] - 1), L++;
		while (cmd[i].r < R) add(a[R], -1), now -= R - L - sum(a[R]), R--;
		while (cmd[i].l < L) L--, add(a[L], 1), now += sum(a[L] - 1);
		while (cmd[i].r > R) R++, add(a[R], 1), now += R - L - sum(a[R]) + 1;
		ans[cmd[i].id] = now;
	}
}
int main(void) {
 	 //freopen("3289.in", "r", stdin);
	//freopen("3289.out", "w", stdout);
	read(n); int s = sqrt(n);
	for (int i = 1; i <= n; i++) read(a[i]), tmp[i] = a[i];
	sort(tmp + 1, tmp + 1 + n);
	for (int i = 1; i <= n; i++) {
		a[i] = find(a[i]);
	}

	read(m);
	for (int i = 1; i <= m; i++) {
		read(cmd[i].l), read(cmd[i].r);
		cmd[i].id = i;
	}
	for (int i = 1; i <= n; i++) {
		bel[i] = (i - 1) / s + 1;
	}

	sort(cmd + 1, cmd + 1 + m);
	solve();
	for (int i = 1; i <= m; i++) printf("%d\n", ans[i]);

	return 0;
}

非常感谢您能阅读到这里,喜欢不妨给个赞(~ ̄▽ ̄)~*?

最后祝大家新年快乐辣~

编辑于 2017-01-25

文章被以下专栏收录

    谈谈ACM算法和思想,希望小学生坐在马桶上也能听懂。