「学习笔记/刷题记录」莫队算法(小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;
}
你以为到这就完了吗?不,这个代码还可以优化!
对于我们定义的 \(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;
}
时间一下子就缩短了