并查集(Disjoint Set Union,DSU)是一种处理非连通性问题的经典数据结构,特别适合处理元素分组和动态连通性问题。在解决"白雪皑皑"这类问题时,我们需要理解并查集的三个核心操作:
在P2391问题中,场景可以抽象为一个长度为n的序列,初始时每个位置都是"未被染色"状态。当进行染色操作时,我们需要高效地处理区间染色和查询操作,这正是并查集的用武之地。
关键理解:并查集在染色问题中的核心价值在于它能跳过已被处理的区间,将时间复杂度从O(n)降低到接近O(1)的均摊复杂度。
P2391题目描述大致为:有一个长度为n的雪地(初始全白),进行m次染色操作,每次将区间[l,r]染成颜色c。最终需要输出每个位置的颜色。关键在于:
传统暴力方法(每次操作遍历整个区间)时间复杂度为O(mn),无法通过大规模数据测试。
我们可以将序列视为一系列"未被染色"的块,使用并查集来维护这些块:
cpp复制int fa[MAXN]; // 并查集数组
void init(int n) {
for(int i=1; i<=n+1; i++) fa[i] = i; // 多开一个哨兵节点
}
int find(int x) {
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
void color(int l, int r, int c) {
for(int i=find(l); i<=r; i=find(i+1)) {
ans[i] = c;
fa[i] = i+1; // 指向下一个位置
}
}
标准的并查集实现需要路径压缩来保证效率。在上述代码中,find函数已经包含了路径压缩:
cpp复制int find(int x) {
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
这种递归写法简洁但可能栈溢出,对于极端大数据可以改为迭代版本:
cpp复制int find(int x) {
int root = x;
while(root != fa[root]) root = fa[root];
while(x != root) {
int next = fa[x];
fa[x] = root;
x = next;
}
return root;
}
在初始化时,我们多开了一个节点(n+1)作为哨兵:
cpp复制void init(int n) {
for(int i=1; i<=n+1; i++) fa[i] = i;
}
这个设计非常关键,它保证了当我们处理到序列末尾时,find操作能正确返回而不会越界。
题目中后面的染色操作会覆盖前面的,因此我们可以逆向处理操作序列:
这种逆向处理思路可以进一步减少不必要的操作。
以下是结合了所有优化思路的完整实现:
cpp复制#include <iostream>
using namespace std;
const int MAXN = 1e6 + 10;
int fa[MAXN]; // 并查集数组
int ans[MAXN]; // 存储最终颜色
struct Query {
int l, r, c;
} q[MAXN];
int find(int x) {
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
// 初始化并查集
for(int i=1; i<=n+1; i++) fa[i] = i;
// 读入所有查询
for(int i=1; i<=m; i++) {
cin >> q[i].l >> q[i].r >> q[i].c;
}
// 逆向处理查询
for(int i=m; i>=1; i--) {
int l = q[i].l, r = q[i].r, c = q[i].c;
// 从l开始,找到第一个未被染色的位置
for(int j=find(l); j<=r; j=find(j+1)) {
ans[j] = c; // 染色
fa[j] = j+1; // 指向下一个位置
}
}
// 输出结果
for(int i=1; i<=n; i++) {
cout << ans[i] << "\n";
}
return 0;
}
该算法的时间复杂度主要取决于并查集操作:
因此总时间复杂度为O(nα(n)),其中α(n)是增长极其缓慢的反阿克曼函数,对于任何实际应用中的n值,α(n)不超过4。
空间消耗主要来自:
总空间复杂度为O(n+m),对于题目给定的约束条件完全足够。
常见错误包括:
对于n=1e6级别的数据:
有些同学可能会问:为什么不能正向处理?正向处理的问题是:
并查集在算法竞赛中应用广泛,除染色问题外,还包括:
以经典的"朋友圈"问题为例,可以使用并查集高效统计社交网络中的连通分量。
有时需要支持操作的撤销,可以采用:
对于二维平面上的染色问题,可以:
对于超大规模数据,可以考虑:
在实际编码比赛中,掌握基础的单线程优化版本通常已经足够应对绝大多数题目。