[洛谷Luogu]P1141 01迷宫[联通块 并查集]

通过并查集解决洛谷P1141 01迷宫问题,计算每个点所在的连通块大小。采用路径压缩和按节点大小合并优化,降低并查集复杂度至近似O(n^2)。

题目链接

大致题意

相邻格子不同为连通,计算每个点所在的连通块大小。

想法

我采用了并查集的做法。
开一个辅助数组记录连通块大小,每次合并的时候更新父亲节点的大小即可。

一个点先与它上面的点判定,若判定连通则加入上方点所属的块中。
再与左边的点判定,若连通则再将两个块合并。

总体复杂度 O ( n 3 ) O(n^3) O(n3),其中并查集不加优化复杂度为 O ( n ) O(n) O(n),使用路径压缩+按节点大小合并优化并查集,将并查集复杂度降低为近似 O ( 1 ) O(1) O(1),总体复杂度近似为 O ( n 2 ) O(n^2) O(n2)

并查集优化

路径压缩

每次查找的时候,将路径上的所有儿子的父亲改写为最原始的祖宗。这样下次就可以直接找到祖宗。
实现:

inline int find(const int &x)
{
    return fa[x] == x ? x : fa[x] = find(fa[x]);//路径压缩
}

通俗写法:

inline int find(const int &x)
{
    if(fa[x] == x)//找到无父亲的节点,即为祖宗
    	return x;
    fa[x] = find(fa[x]);
    return fa[x];
}

按秩合并

思考:
有2个块需要合并,那么将小块接在大块的后面能够让整体复杂度更优。
可以将每个祖宗及其所有儿子看做一棵树,那么节点到根的距离就是查找父亲需要的次数。
将小树并入大树,可以避免树的深度过深。
这个rnk数组保存的是树的深度的上界。
通常写法:

const int maxn = 10000000;
int rnk[maxn],fa[maxn];//rnk为该节点所在子树的深度
inline void unite(const int &x,const int &y)
{
	//合并x与y的块
	int t1 = find(x),t2 = find(y);//找到各自的祖宗
	if(t1 == t2)
		return;
	if(rnk[t1] == rnk[t2])
	{
		//此时两树深度相等,随便合并
		fa[t1] = t2;
		++rnk[t2];//合并后深度增加
	}
	else if(rnk[t1] > rnk[t2])
		fa[t2] = t1;
	else
		fa[t1] = t2;
}

总结

路径压缩和按秩合并都能将并查集复杂度降为 O ( l o g n ) O(logn) O(logn),而两者一起使用能够降为 O ( l o g ∗ n ) O(log^*n) O(logn)

n n n l o g ∗ n log^*n logn
(−∞, 1]0
(1, 2]1
(2, 4]2
(4, 16]3
(16, 65536]4
(65536, 2^65536]5

(数据转自链接,个人认为这篇博文讲并查集讲得相当不错。)

代码

因为按秩合并需要额外数组,而在此数量级下开数组的消耗可能比优化更大……
于是借用了题目中维护的“连通块大小”,即节点个数,进行了按size合并的优化。

#include <cstdio>
using namespace std;
#define getId(x, y) (((x - 1) * n) + y) //将二维的点赋予一个一维的别名
int fa[1001000];
int h[1001000];
char all[2][1010];
int n, m, i, j;
//读入优化
inline char nc()
{
    static char buf[400000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 400000, stdin), p1 == p2) ? EOF : *p1++;
}
void read(char *s)
{
    static char c;
    for (c = nc(); c != '1' && c != '0'; c = nc());
    for (; c == '0' || c == '1'; *++s = c, c = nc());
}
void read(int &r)
{
    static char c;  r = 0;
    for (c = nc(); c > '9' || c < '0'; c = nc());
    for (; c >= '0' && c <= '9'; r = (r << 1) + (r << 3) + (c ^ 48), c = nc());
}
//并查集
inline int find(const int &x)
{
    return fa[x] == x ? x : fa[x] = find(fa[x]);//路径压缩
}
inline void unite(const int &a, const int &b)
{
    int t1 = find(a), t2 = find(b);
    if (t1 == t2)
        return;
    h[t1] > h[t2] ? fa[t2] = t1, h[t1] += h[t2] : fa[t1] = t2, h[t2] += h[t1]; //类似按秩合并的做法,合并同时将儿子的大小加入父亲的大小中
}
//
int main()
{
    read(n);
    read(m);
    int n2 = n * n;
    for (int i = 1; i <= n2; ++i)
        fa[i] = i, h[i] = 1; //初始化每个点都是单独的联通块,大小为1
    
    int now = 0, pre = 1;
    //第一行特判,不需要与上一行作比较
    i = 1;
    read(all[now]);
    for (j = 2; j <= n; ++j)//j从2开始,可以略过第一个格子,最左上角的格子无需判断
        if (all[now][j] != all[now][j - 1])
            unite(getId(i, j), getId(i, j - 1));

    for (i = 2; i <= n; ++i)
    {
        now ^= 1;//使用滚动数组
        pre ^= 1;
        read(all[now]);
        if (all[now][1] != all[pre][1])
            unite(getId(i, 1), getId(i - 1, 1));//第一个格子无需与左边判断
        for (j = 2; j <= n; ++j)
        {
            if (all[now][j] != all[now][j - 1]) //因为是按行按列遍历,实际上只需要处理每个点的上方点和左边点的合并
                unite(getId(i, j), getId(i, j - 1));
            if (all[now][j] != all[pre][j])
                unite(getId(i, j), getId(i - 1, j));
        }
    }
    int x, y;
    for (i = 1; i <= m; ++i)
    {
        read(x);
        read(y);
        printf("%d\n", h[find(getId(x,y))]);
    }
    return 0;
}
### 关于可撤销并查集洛谷练习题及相关数据结构 #### 可撤销并查集简介 可撤销并查集是一种扩展版本的并查集,能够在执行合并操作的同时保留回退的能力。这意味着可以在任意时刻撤消最近的一次 `union` 操作,从而恢复到之前的状态。这一特性使得该数据结构非常适合解决涉及动态连通性和历史状态查询的问题。 实现可撤销并查集的核心在于记录每次路径压缩或合并操作的变化,并将其存入栈中以便后续回溯。具体来说,在标准并查集中引入额外的数据结构(如栈)来保存父节点指针的历史修改情况[^1]。 下面是一些适合初学者和中级选手练习的洛谷平台上的题目: --- #### 推荐洛谷练习题 1. **P2860 [USACO06FEB]Redundant Paths G** 这道题考察的是如何利用带权并查集或者可撤销并查集计算最小边数使图成为双联通分量。虽然不强制要求使用可撤销并查集,但如果尝试用此方法解题会更加直观。 题目链接: https://www.luogu.com.cn/problem/P2860 2. **P3970 [TJOI2015]线性代数** 虽然名字看起来与矩阵运算有关,但实际上可以通过构建虚拟点的方式转化为经典的并查集问题。进一步优化时可以考虑加入可撤销机制以应对复杂度较高的测试样例。 题目链接: https://www.luogu.com.cn/problem/P3970 3. **P4180 [BJOI2012]树的难题** 此题需要维护森林中的多个独立子树之间的连接关系,并支持删除某些边的操作。因此非常适合作为学习可撤销并查集的应用实例之一。 题目链接: https://www.luogu.com.cn/problem/P4180 4. **P2024 [AHOI2009]中国象棋** 将二维网格抽象成一维数组之后,可以用带有时间戳功能的可撤销并查集高效解答本题提出的询问类问题。 题目链接: https://www.luogu.com.cn/problem/P2024 --- #### 实现代码示例 以下是一个简单的 C++ 版本的可撤销并查集模板程序: ```cpp #include <bits/stdc++.h> using namespace std; struct UndoUnionFind { vector<int> parent; vector<pair<int, int>> history; // 记录每一次改变 (x, old_parent) UndoUnionFind(int n): parent(n){ for(int i=0;i<n;i++) parent[i]=i; } int find_set(int x) { while(parent[x]!=x){ history.emplace_back(x,parent[x]); parent[x]=parent[parent[x]]; // Path compression x=parent[x]; } return x; } bool unite_sets(int x, int y){ int fx=find_set(x); int fy=find_set(y); if(fx !=fy ){ history.emplace_back(fy,fy); // Record the change of root node's father. parent[fy]=fx; return true; } return false; } void undo(){ if(history.empty())return ; auto &[node,new_father]=history.back(); parent[node]=new_father; history.pop_back(); } }; int main() { int n,m,q; cin >> n >> m >> q; UndoUnionFind uf(n+1); for(int i=0;i<m;i++){ int u,v; cin>>u>>v; uf.unite_sets(u,v); } while(q--){ string cmd; cin>>cmd; if(cmd=="undo"){ uf.undo(); }else{ int u,v; cin>>u>>v; cout <<(uf.find_set(u)==uf.find_set(v)? "YES":"NO")<<'\n'; } } } ``` --- #### 总结 通过以上介绍可以看出,掌握好基础版并查集的基础上再去深入理解其变种形式——比如种类并查集以及今天的主题可撤销并查集——对于提高算法竞赛水平至关重要。这些技巧不仅限于比赛场景下有用,在实际软件开发过程中也可能遇到类似的逻辑需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值