并查集的回顾:
并查集用树的形式维护所有的集合(不一定是二叉树),每一个集合的编号是根节点的编号。对于每个点x我们都存储一下它的父节点p[x]。
并查集的操作:(三点核心)
一、初始化:
初始化将每个点的父节点设为自己即可。
O(n)
代码实现:
void init(int n)
{
for(int i=1;i<=n;++i)//下标是0是1灵活变化即可
{
p[i] = i;
}
}
下面两个并查集基本操作都是近乎O(1)
的
二、并:
如果两个点不属于一个集合,将其中一个点的根节点的父节点指向另一个点的根节点即可。
代码实现:
void _union(int a,int b)
{
int pa = find(a);//a的根节点
int pb = find(b);//b的根节点
p[pa] = pb;//a根节点的父节点指向b的根节点
}
三、查:
当我们查询某个点属于哪个集合的时候(换句话说就是查询属于的是哪个树根),我们可以先找到这个点的father,判断其father是否是树根,如果不是树根(if(p[x]!=x))再往上找,直到找到为止,这样我们就找到了树根 。这是一个递归的过程,在回溯的时候我们可以顺便进行优化,将除了根节点外路径上的每个节点的父节点p[x]都指向树根,这就完成了路径压缩优化。
代码实现:
int find(int x)
{
if(p[x]!=x) p[x] = find(p[x]);
return p[x];
}
题意:
在一个一个 n×n 的点阵中,两个人轮流在相邻的点之间画上红边和蓝边,直到围成一个封闭的圈(面积不必为 1)为止,“封圈”的那个人就是赢家。但这个题目并不是问我们谁是赢家,只是问:在第几步的时候游戏结束,如果输入的指定步数上限 m 步之后也没有结束,则输出一行“draw”(平局)。
思想:
并查集判环:任意一个图中,增加一条边形成环当且仅当这条边连接的两点已经连通,于是可以将点分为若干个集合,每个集合对应图中的一个连通块。
对于并查集我们一般更习惯进行一维操作,对于题中的二维坐标(x, y)我们通过坐标变换为一维坐标即可,变换方式:x*n + y(下标从0开始,n表示矩阵边的大小)
因此:
我们可以把每一个点看做图当中的点,把每一条边看做图中的边。当我们发现图中出现环的时候,就等价于这两个点在连边之前,就已经在同一个联通块当中了。
所以说每一步是否会形成环,就等价于是说这两个点在连边之前有没有在集合里。(解题核心在这里)
①如果已经出现过了,就代表会形成环。
②否则,就不会形成环。
这样,我们就可以使用路径压缩的并查集了。
时间复杂度:
首先并查集这个数据结构能够在近乎O(1)
的时间复杂度内快速支持合并和查询两个操作。
之后我们可以计算本题的时间复杂度为O(mlogn)
(其中logn
的很小,因为并查集的常数非常小)
代码+注释:
#include<bits/stdc++.h>
using namespace std;
int n, m;
int x, y;
char op;
const int N = 200*200+10;
int p[N];
void init(int n)
{
for(int i=0;i<n;++i)
{
p[i] = i;
}
}
int find(int x)
{
if(p[x]!=x) p[x] = find(p[x]);
return p[x];
}
int get(int x, int y)
{
return x*n + y;
}
int main()
{
cin>>n>>m;
init(n*n);//从n*n的矩阵降为成n*n的一维数组p
int ans = 0;
for(int i=1;i<=m;++i)//枚举所有操作,通过并查集操作判断a和b是否在同一个集合,
{
cin>>x>>y>>op;
--x, --y;//由于输入的坐标都是以1开始的,为了能够用get函数中的转化公式,我们要先减一化成以0开始的坐标
int a, b;
a = get(x, y);
if(op=='D') b = get(x+1, y);
else b = get(x, y+1);
int pa = find(a), pb = find(b);
if(pa!=pb) p[pa] = pb;//若不在同一个集合,则需要将两个集合进行合并
else//若在同一个集合则标记此操作可以让格子形成环
{
//任意一个图中,增加一条边形成环当且仅当这条边连接的两点已经联通
ans = i;
break;
}
}
if(!ans) cout<<"draw"<<endl;//ans的值不变说明是平局,未成环
else cout<<ans<<endl;
return 0;
}