1. 题目来源
链接:3115. 疯狂的馒头
2. 题目解析
很经典的一道并查集的应用题目,在很多地方考过,由于 lc
周赛遇到了变种题,在此总结并学习这个经典模板。
维护所有的馒头为并查集,初始每个馒头的边均指向自己。由于后染色可以将前染色覆盖掉,所以直接从后往前针对区间开始染色即可,染过的不必再染,没染的染完就确定了它的颜色,故每个点只会被染色一次。这点也是离线询问,离线算法。
并查集的根,即 find(i)
,操作为找到 i
右边第一个未被染色的点。当染色 [l, r]
区间时,只需要判断 find(l)
是否大于等于 r
,即可判断当前区间还是否有点需要染色:
- 若大于等于了,则说明这段均已被染色,不能再重复染了。
- 若小于等于,则说明从
[find(l), r]
可以被染色了。令l=find(l)
让l
指向这个待染色块,记录这个块的染色结果,res[l]=m
。再维护并查集数组,将其指向下一个未被染色的块,即p[i]=i+1
, 也可p[i]=find(i+1)
,两者是等价的,即不管i+1
,染色与否,find(i+1)
都会找到i, i+1
右边第一个没被染色的点,只不过这个p[i]
数组的定义可能会发生改变,在p[i]=find(i+1)
下,存储的是右边第一个没被染色的点,但是在p[i]=i+1
下存的不一定是下一个未被染色的点。但是我们只考虑两个集合合并之后的根,根是唯一确定的,所以这两种更新方式是相同的。
是我的个人理解,若有误,请及时指出~~~,互相学习。
- 总是将集合合并,下一次进行查询的时候就可以跳过中间这些已经染色的块。
在此没有按秩合并,仅路径压缩的并查集时间复杂度理论上是 O ( l o g n ) O(logn) O(logn) 的,但实际运行效率为 O ( 1 ) O(1) O(1)。
它可以处理
1
0
7
10^7
107 的时间复杂度,但是在输入输出上需要使用 printf、scanf
。
时间复杂度:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
空间复杂度:
O
(
n
)
O(n)
O(n)
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e6+5;
int n, m, p, q;
int fa[N], res[N];
int find(int x) {
if (fa[x] != x) fa[x] = find(fa[x]);
return fa[x];
}
int main() {
scanf("%d%d%d%d", &n, &m, &p, &q);
for (int i = 1; i < N; i ++ ) fa[i] = i;
while (m) {
int l = (m * p + q) % n + 1, r = (m * q + p) % n + 1;
if (l > r) swap(l, r);
while (find(l) <= r) {
l = find(l);
fa[l] = find(l + 1);
// 等价 fa[l] = l + 1;
res[l] = m;
}
m -- ;
}
for (int i = 1; i <= n; i ++ ) printf("%d\n", res[i]);
return 0;
}