一道很好的二维并查集模板题。
题目大意很简单:在一个\(N×N\)的棋盘上放\(M\)个棋子,求每放一次后棋盘上的连通块的个数
如果考虑\(DFS\)或\(BFS\)来求连通块的话,显然会\(T\)。
那么,我们该用什么高效的算法或数据结构来解决这个问题呢?
在讲普通的一维并查集的时候,我们采用了一种代表元的思想:一个集合用一个元素作为它的代表,将集合内的所有元素的父节点指向这个代表元,最后根据每个元素的父节点来判断元素所在集合。
那么,这个思想在求连通块个数时有什么应用呢?
我们可以认定一个棋子为代表元,在想四个方向扩展,如果有相同的棋子,就可以将这颗棋子的父节点指向原先的节点,这样不停的拓展下去,会构成一个集合(也就是这个连通块),而代表元就是原先的那枚棋子。
这样,集合个数即为我们所求。
那么,下面我来介绍一下二维并查集的实现。
首先,考虑\(f\)数组的定义。由于棋盘是二维的,我们可以有二维数组;同时,每个代表元也是一个二维的坐标,所以,我们可以将\(f\)数组定义为一个结构体,里面存着\((i,j)\)的代表元的坐标\((x,y)\)。
代码如下:
struct node {
int x, y;
};
node f[MAXN][MAXN];
仿照一维并查集,我们可以对二维并查集有如下操作:
\(·init()\):初始化集合,即将每个位置的代表元设为自己
\(·find(x)\):求\(x\)所在的集合的代表元
\(·unite(x, y)\):将\(x\)和\(y\)所在的集合合并
\(·same(x, y)\):询问\(x\)和\(y\)是否在一个集合内。
下面我来一一讲解。
对于\(init\)操作,十分简单:\(f_{i,j}=(node\{i, j\})\)
void init() {
_for (i, 1, n)
_for (j, 1, n)
f[i][j] = (node){i, j};
}
对于\(find\)操作,这就需要一定的思维。
这个\(find\)操作,我们也要进行路径压缩。
当查询到代表元就是自己时,判断标准自然是\(f_{i,j}\)的坐标和自己的坐标相同,扩展开写就是\(f_{i,j}.x == d.x\) \(&&\) \(f_{i,j}.y==d.y\),此时就可以return
;
否则的话,就要更新当前节点的代表元。
node find(node d) {
if (f[d.x][d.y].x == d.x && f[d.x][d.y].y == d.y) return d;
return f[d.x][d.y] = find(f[d.x][d.y]);
}
对于\(unite\)操作,则是十分简单。
void unite(node d1, node d2) {
d1 = find(d1);
d2 = find(d2);
if (d1.x == d2.x && d1.y == d2.y) return ;
f[d1.x][d1.y] = d2;
}
而\(same\)操作就只是判断两个元素的代表元坐标是否相等即可。
bool same(node d1, node d2) {
d1 = find(d1);
d2 = find(d2);
return d1.x == d2.x && d1.y == d2.y;
}
大体上就是差不多。其实我们发现,我在上面大量用到了判断坐标相等,所以我们可以考虑对于\(node\)来重载\(==\)这个运算符,这留给读者自己思考。
然后要根据每一次部署棋子来更新集合个数。我们可以从每一次部署的位置向四周扩展,如果遇到同类棋子,可以判断是否处在同一集合里,不是就可以合并。
至于统计答案,我们可以先设定\(ans=0\),每下一个棋子\(ans++\),但一旦发现周围有同类棋子且集合不一样时,合并集合的同时,\(ans--\),最后每次输出\(ans\)即可。
再讲一下一个小坑点。如果你一开始的棋盘数组初始化是\(0\),输入\(c\)时不能等于\(0\),否则。。。\(GG\)。
所以你可以对输入的\(c\)进行变换(例如\(+1\)什么的)
差不多了,上代码!
\(AC\) \(Code\)
#include
#define _for(i, a, b) for (int i=(a); i<=(b); i++)
using namespace std;
const int MAXN = 510, dir[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};//方向数组
struct node {
int x, y;
};
node f[MAXN][MAXN];
int n, m, map[MAXN][MAXN], ans;
void init() {
_for (i, 1, n)
_for (j, 1, n)
f[i][j] = (node){i, j};
}
node find(node d) {
if (f[d.x][d.y].x == d.x && f[d.x][d.y].y == d.y) return d;
return f[d.x][d.y] = find(f[d.x][d.y]);
}
void unite(node d1, node d2) {
d1 = find(d1);
d2 = find(d2);
if (d1.x == d2.x && d1.y == d2.y) return ;
f[d1.x][d1.y] = d2;
}
bool same(node d1, node d2) {
d1 = find(d1);
d2 = find(d2);
return d1.x == d2.x && d1.y == d2.y;
}//二维并查集基本操作
bool cango(int x, int y) {
return x > 0 && x <= n && y > 0 && y <= n;
}//判断是否出界
int main() {
cin >> n >> m;
init();//初始化莫忘掉
while (m--) {
int c, a, b;
cin >> c >> a >> b;
node now;
now.x = a, now.y = b;
map[a][b] = c+1;//变换莫忘掉
ans++;
_for (i, 0, 3) {
int nx = a + dir[i][0], ny = b + dir[i][1];
node next;
next.x = nx, next.y = ny;
if (cango(nx, ny) && map[a][b] == map[nx][ny]) {//一定要判断棋子种类是否一样
if (!same(now, next)) {
unite(now, next);
ans--;
}
}
}
cout << ans << endl;//换行莫忘掉
}
return 0;//完结撒花!
}