Comet OJ - Contest #13 C2 佛御石之钵 -不碎的意志 -(并查集 + 技巧)

mac2022-10-04  46

Comet OJ - Contest #13 C2

题意

给出一个 n 行 m 列的 01 矩阵。有 q 次操作,每次操作选取一个子矩阵,将子矩阵变为全 1,每次操作后输出当前连通块个数,(上下左右算联通)。 n, m < 1e4; q < 3e4.

分析

首先,只有发生 0->1 的格子,才会导致连通块数目变化。也就是说,对于每次操作只关注子矩阵里原本是 0 的格子,(0 的格子会不断减小)

当原本是 0 的格子变为 1,连通块数目 + 1,然后分别遍历上下左右四个位置,如果遍历到的位置为 1,且之前没有在一个连通块,那么现在就将他们并入一个集合,同时连通块数目 - 1。

实现方面,可以在每一行开一个并查集,并查集的祖先表示右边第一个 0 的位置(包括本身),这样在遍历子矩阵时,只需要一行一行的找 0 的位置即可。

而判断两个 1 位置是否在一个联通块,可以用一个并查集维护,做法是将每个二维点都打上标号。

#include <bits/stdc++.h> using namespace std; typedef long long ll; #define fi first #define se second const int INF = 0x3f3f3f3f; const int N = 1e3 + 10; int t, n, m, q, ans; char arr[N][N]; int fa[N][N], dsu[N*N]; int dir[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; inline int find(int fa[], int x){ //在第 i 行并查集 return fa[x] = fa[x] == x ? fa[x] : find(fa, fa[x]); } inline int id(int x, int y){ // 对二维点打标号 return x * m + y; } void light(int x, int y){ // 像四个方向找 ‘1’,将两个点并入集合 arr[x][y] = '1'; ans++; // 0 -> 1,连通块数目 + 1 fa[x][y] = y + 1; for (int i = 0; i < 4; i++){ // 四个方向 int tx = x + dir[i][0], ty = y + dir[i][1]; if(tx >= 1 && tx <= n && ty >= 1 && ty <= m && arr[tx][ty] == '1' && find(dsu, id(x, y)) != find(dsu, id(tx, ty))){ dsu[find(dsu, id(x, y))] = find(dsu, id(tx, ty)); // 如果两个点不是同一个集合 ans--; // 并入同一个集合,联通数目减一 } } } int main(){ scanf("%d%d", &n, &m); ans = 0; for (int i = 1; i <= n; i++){ scanf("%s", arr[i] + 1); for (int j = 1; j <= m; j++){ fa[i][j] = j, dsu[id(i, j)] = id(i, j); } fa[i][m + 1] = m + 1; } for (int i = 1; i <= n; i++){ for (int j = 1; j <= m; j++){ if(arr[i][j] == '1') light(i, j); } } scanf("%d", &q); while(q--){ int a, b, c, d; scanf("%d%d%d%d", &a, &b, &c, &d); for (int i = a; i <= c; i++){ // 找每一行 0 的位置 int s = find(fa[i], b); while(s <= d){ light(i, s); s = find(fa[i], b); } } printf("%d\n", ans); } return 0; } /* 5 6 101010 000001 101010 000001 101010 3 1 2 5 2 4 4 4 4 1 3 3 6 */
最新回复(0)