Loading

「学习笔记/刷题记录」莫队算法(小Z的袜子)

“暴力出奇迹!”
莫队算法真的是一个纯暴力算法,而且板子也很好记,可惜是个离线算法
普通的莫队算法是不支持修改的,学莫队,要先知道分块,还要会用 sort(能用\(cmp\)或重载运算符自定义排序)看这篇文章的人肯定都会,你就当我说了废话就行了
莫队的大体思路就是给定一个区间的左右端点,再给你一个要询问的区间,你要移动这个左右端点来向要询问的区间靠拢,最后左右端点与要查询的区间的左右端点相同,每次靠拢,要能很快地计算出到达或离开这个位置的答案
或许很不好理解,我们来看一道题

[国家集训队] 小 \(Z\) 的袜子

作为一个生活散漫的人,小 \(Z\) 每天早上都要耗费很久从一堆五颜六色的袜子中找出一双来穿。终于有一天,小 \(Z\) 再也无法忍受这恼人的找袜子过程,于是他决定听天由命……
具体来说,小 \(Z\) 把这 \(N\) 只袜子从 \(1\)\(N\) 编号,然后从编号 \(L\)\(R\) (\(L\) 尽管小 \(Z\) 并不在意两只袜子是不是完整的一双,甚至不在意两只袜子是否一左一右,他却很在意袜子的颜色,毕竟穿两只不同色的袜子会很尴尬。
你的任务便是告诉小 \(Z\),他有多大的概率抽到两只颜色相同的袜子。当然,小 \(Z\) 希望这个概率尽量高,所以他可能会询问多个 \((L,R)\) 以方便自己选择。
然而数据中有 \(L=R\) 的情况,请特判这种情况,输出 \(0/1\)


这道题,我们先抛开概率这回事,先考虑一个问题,我们如何统计每种颜色的袜子有多少只?
或许。。。可以用前缀和?
确实可以,但如果颜色太多,那空间就炸了
动态开点线段树?
也可以,空间炸不了,但是时间效率上就没有那么优秀了
正解就是——莫队,代码先拆开
大体做法:
先分块,这道题可以根据 \(\sqrt n\) 来分块

	n = read(), m = read();
	for (int i = 1; i <= n; ++ i) {
		c[i] = read();
	}
	int num = sqrt(n);
	for (int i = 1; i <= n; ++ i) {
		pos[i] = (i - 1) / num + 1;
	}

因为是离线算法,我们要将询问记录下来,进行排序,每个询问都有一个左端点 \(l\) 和右端点 \(r\),我们先根据左端点 \(l\) 排序,让 \(l\) 所处的块的序号小的排在前面,如果左端点所处的序号相同,再根据右端点 \(r\) 来排序,\(r\) 小的排在前面

for (int i = 1; i <= m; ++ i) {
		ask[i].l = read(), ask[i].r = read();
		ask[i].id = i;
		ass[ask[i].id] = C(ask[i].r - ask[i].l + 1);
	}
	sort(ask + 1, ask + m + 1);
struct xw {
	int l, r, id;
	bool operator < (const xw &b) {
		if (pos[l] == pos[b.l]) {
			return r < b.r;
		}
		return pos[l] < pos[b.l];
	}
} ask[N];

排序完成后,我们定义一个初始区间,\(l=0,r=-1\),此时的 \(ans\) 等于 \(0\)

	int l = 0, r = -1;
	ans = 0;

现在,慢慢向我们的询问区间靠拢,这里就要牵扯 \(4\) 种情况(为了方便,我们将初始区间的左右端点设为 \(l, r\),将查询的区间设为 \(L, R\)
1、 \(l < L\),我们要将 \(l\) 向右移,在移动之前,先在 \(ans\) 中删除当前 \(l\) 所处的位置的答案
2、 \(l > L\),我们要将 \(l\) 向左移,在移动之后,将 \(l\) 当前所处的位置大答案加入 \(ans\)
3、 \(r < R\),我们要将 \(r\) 向右移,在移动之后,将 \(r\) 当前所处的位置大答案加入 \(ans\)
4、 \(r > R\),我们要将 \(r\) 向左移,在移动之前,先在 \(ans\) 中删除当前 \(r\) 所处的位置的答案
最后于查询区间重合后,记录答案(因为排过序了,所以要记录答案),下面代码中的 del()add() 函数分别代表删除答案和加入答案,\(k\) 数组存储的是当前询问的区间取到相同颜色的袜子的方案数

	for (int i = 1; i <= m; ++ i) {
		while (l < ask[i].l) {
			del(l ++);
		}
		while (l > ask[i].l) {
			add(-- l);
		}
		while (r < ask[i].r) {
			add(++ r);
		}
		while (r > ask[i].r) {
			del(r --);
		}
		k[ask[i].id] = ans;
	}

最后,就是输出答案了,很简单,不用说吧!\(ass\) 数组存储的是这个询问区间取两只袜子的总方案数

	for (int i = 1; i <= m; ++ i) {
		if (k[i] == 0) {
			puts("0/1");
			continue;
		}
		int g = gcd(k[i], ass[i]);
		printf("%lld/%lld\n", k[i] / g, ass[i] / g);
	}

在这个题中还有一些其他的事,那就是概率以及 add()del() 该如何写,对于 add() 和 del() 函数,不同的题要求不同,写法也不同,而概率,就是 \(\frac{取到相同颜色袜子的情况的个数}{取两只袜子所有情况的个数}\),接下来给出完整代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

inline ll read() {
	ll x = 0;
	int fg = 0;
	char ch = getchar();
	while (ch < '0' || ch > '9') {
		fg |= (ch == '-');
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = (x << 3) + (x << 1) + (ch ^ 48);
		ch = getchar();
	}
	return fg ? ~x + 1 : x;
}

const int N = 5e4 + 5;

int n, m;
ll ans;
int pos[N], cnt[N];
ll c[N], k[N], ass[N];

struct xw {
	int l, r, id;
	bool operator < (const xw &b) {
		if (pos[l] == pos[b.l]) {
			return r < b.r;
		}
		return pos[l] < pos[b.l];
	}
} ask[N];

inline ll C(ll x) {
	if (x < 2)	return 0;
	return x * (x - 1) / 2;
}

inline void add(int x) {
	ll q = ++ cnt[c[x]];
	ans -= C(q - 1);
	ans += C(q);
}

inline void del(int x) {
	ll q = -- cnt[c[x]];
	ans -= C(q + 1);
	ans += C(q);
}

ll gcd(ll x, ll y) {
	if (y == 0) {
		return x;
	}
	return gcd(y, x % y);
}

int main() {
	n = read(), m = read();
	for (int i = 1; i <= n; ++ i) {
		c[i] = read();
	}
	int num = sqrt(n);
	for (int i = 1; i <= n; ++ i) {
		pos[i] = (i - 1) / num + 1;
	}
	for (int i = 1; i <= m; ++ i) {
		ask[i].l = read(), ask[i].r = read();
		ask[i].id = i;
		ass[ask[i].id] = C(ask[i].r - ask[i].l + 1);
	}
	sort(ask + 1, ask + m + 1);
	int l = 0, r = -1;
	ans = 0;
	for (int i = 1; i <= m; ++ i) {
		while (l < ask[i].l) {
			del(l ++);
		}
		while (l > ask[i].l) {
			add(-- l);
		}
		while (r < ask[i].r) {
			add(++ r);
		}
		while (r > ask[i].r) {
			del(r --);
		}
		k[ask[i].id] = ans;
	}
	for (int i = 1; i <= m; ++ i) {
		if (k[i] == 0) {
			puts("0/1");
			continue;
		}
		int g = gcd(k[i], ass[i]);
		printf("%lld/%lld\n", k[i] / g, ass[i] / g);
	}
	return 0;
}

image

你以为到这就完了吗?不,这个代码还可以优化!
对于我们定义的 \(l\)\(r\),我们会发现,\(l\) 一般只会在块中运动,而 \(r\) 则是全区间跑,如果 \(l\) 要到下一个区间,根据我们的排序,\(R\) 一开始会很小(至少应该比 \(r\) 小),所以 \(r\) 要在跑回来,这里的跑回来可不是直接跳回来,直接跳的话,\(ans\) 无法更新,所以 \(r\) 是一步一步挪回来的,挪回来后再根据需求在挪回去,你会发现,\(r\) 挪回来的过程其实把时间浪费了,这个时间我们也要利用起来,所以可以这样对需求排序

struct xunwen {
	int l, r, id;
	bool operator < (const xunwen &b) {
		if (pos[l] == pos[b.l]) {
			if (pos[l] & 1)	return r < b.r;
			else	return r > b.r;
		}
		return pos[l] < pos[b.l];
	}
} ask[N];

因为我们一开始在奇数块上(\(1\) 号块上),\(r\) 在区间左侧,所以将 \(R\) 从小到大排序,而当 \(l\) 跳到偶数块时(\(2\) 号块上),\(r\) 此时在区间右侧,所以我们可以将 \(R\) 从大到小排序,将它挪回来的这个过程利用起来,再往后跳到奇数块上,再跳到偶数块上,就是以此类推了
最终代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

inline ll read() {
	ll x = 0;
	int fg = 0;
	char ch = getchar();
	while (ch < '0' || ch > '9') {
		fg |= (ch == '-');
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = (x << 3) + (x << 1) + (ch ^ 48);
		ch = getchar();
	}
	return fg ? ~x + 1 : x;
}

const int N = 5e4 + 5;

int n, m;
ll ans;
int pos[N], cnt[N];
ll c[N], k[N], ass[N];

struct xunwen {
	int l, r, id;
	bool operator < (const xunwen &b) {
		if (pos[l] == pos[b.l]) {
			if (pos[l] & 1)	return r < b.r;
			else	return r > b.r;
		}
		return pos[l] < pos[b.l];
	}
} ask[N];

inline ll C(ll x) {
	if (x < 2)	return 0;
	return x * (x - 1) / 2;
}

inline void add(int x) {
	ll q = ++ cnt[c[x]];
	ans -= C(q - 1);
	ans += C(q);
}

inline void del(int x) {
	ll q = -- cnt[c[x]];
	ans -= C(q + 1);
	ans += C(q);
}

ll gcd(ll x, ll y) {
	if (y == 0) {
		return x;
	}
	return gcd(y, x % y);
}

int main() {
	n = read(), m = read();
	for (int i = 1; i <= n; ++ i) {
		c[i] = read();
	}
	int num = sqrt(n);
	for (int i = 1; i <= n; ++ i) {
		pos[i] = (i - 1) / num + 1;
	}
	for (int i = 1; i <= m; ++ i) {
		ask[i].l = read(), ask[i].r = read();
		ask[i].id = i;
		ass[ask[i].id] = C(ask[i].r - ask[i].l + 1);
	}
	sort(ask + 1, ask + m + 1);
	int l = 0, r = -1;
	ans = 0;
	for (int i = 1; i <= m; ++ i) {
		while (l < ask[i].l) {
			del(l ++);
		}
		while (l > ask[i].l) {
			add(-- l);
		}
		while (r < ask[i].r) {
			add(++ r);
		}
		while (r > ask[i].r) {
			del(r --);
		}
		k[ask[i].id] = ans;
	}
	for (int i = 1; i <= m; ++ i) {
		if (k[i] == 0) {
			puts("0/1");
			continue;
		}
		int g = gcd(k[i], ass[i]);
		printf("%lld/%lld\n", k[i] / g, ass[i] / g);
	}
	return 0;
}

image
时间一下子就缩短了

posted @ 2023-01-06 16:09  yi_fan0305  阅读(87)  评论(0编辑  收藏  举报