|
|
|
|
## [$AcWing$ $1098$. 城堡问题](https://www.acwing.com/problem/content/1100/)
|
|
|
|
|
|
|
|
|
|
### 一、题目描述
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
1 2 3 4 5 6 7
|
|
|
|
|
#############################
|
|
|
|
|
1 # | # | # | | #
|
|
|
|
|
#####---#####---#---#####---#
|
|
|
|
|
2 # # | # # # # #
|
|
|
|
|
#---#####---#####---#####---#
|
|
|
|
|
3 # | | # # # # #
|
|
|
|
|
#---#########---#####---#---#
|
|
|
|
|
4 # # | | | | # #
|
|
|
|
|
#############################
|
|
|
|
|
(图 1)
|
|
|
|
|
|
|
|
|
|
# = Wall
|
|
|
|
|
| = No wall
|
|
|
|
|
- = No wall
|
|
|
|
|
|
|
|
|
|
方向:上北下南左西右东。
|
|
|
|
|
```
|
|
|
|
|
图$1$是一个城堡的地形图。
|
|
|
|
|
|
|
|
|
|
请你编写一个程序,计算城堡一共有多少房间,最大的房间有多大。
|
|
|
|
|
|
|
|
|
|
城堡被分割成 $m∗n$ 个方格区域,每个方格区域可以有$0\sim 4$面墙。
|
|
|
|
|
|
|
|
|
|
注意:墙体厚度忽略不计。
|
|
|
|
|
|
|
|
|
|
**输入格式**
|
|
|
|
|
第一行包含两个整数 $m$ 和 $n$,分别表示城堡南北方向的长度和东西方向的长度。
|
|
|
|
|
|
|
|
|
|
接下来 $m$ 行,每行包含 $n$ 个整数,每个整数都表示平面图对应位置的方块的墙的特征。
|
|
|
|
|
|
|
|
|
|
每个方块中墙的特征由数字 $P$ 来描述,我们用$1$表示西墙,$2$表示北墙,$4$表示东墙,$8$表示南墙,$P$ 为该方块包含墙的数字之和。
|
|
|
|
|
|
|
|
|
|
例如,如果一个方块的 $P$ 为$3$,则 $3 = 1 + 2$,该方块包含西墙和北墙。
|
|
|
|
|
|
|
|
|
|
城堡的内墙被计算两次,方块$(1,1)$的南墙同时也是方块$(2,1)$的北墙。
|
|
|
|
|
|
|
|
|
|
输入的数据保证城堡至少有两个房间。
|
|
|
|
|
|
|
|
|
|
**输出格式**
|
|
|
|
|
共两行,第一行输出房间总数,第二行输出最大房间的面积(方块数)。
|
|
|
|
|
|
|
|
|
|
**数据范围**
|
|
|
|
|
$1≤m,n≤50,0≤P≤15$
|
|
|
|
|
|
|
|
|
|
**输入样例**:
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
4 7
|
|
|
|
|
11 6 11 6 3 10 6
|
|
|
|
|
7 9 6 13 5 15 5
|
|
|
|
|
1 10 12 7 13 7 5
|
|
|
|
|
13 11 10 8 10 12 13
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**输出样例**:
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
5
|
|
|
|
|
9
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 二、前置知识
|
|
|
|
|
这道题的输入挺有意思,为一些数字,其实是理解为$0 \sim 15$的十进制数,转为二进制就是$(0000)_2 \sim (1111)_2$,这四个数位,是表示四个方向:
|
|
|
|
|
|
|
|
|
|
**$1$表示西墙,$2$表示北墙,$4$表示东墙,$8$表示南墙**
|
|
|
|
|
|
|
|
|
|
每个方向存在数字$1$表示此方向 **有墙**,$0$表示 **无墙**
|
|
|
|
|
|
|
|
|
|
用墙维起来的是房间,求 **房间个数** 和 **最大面积**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
对于十进制转二进制,并输出二进制的每个数位的值,办法如下:
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#include <bits/stdc++.h>
|
|
|
|
|
|
|
|
|
|
using namespace std;
|
|
|
|
|
|
|
|
|
|
int main() {
|
|
|
|
|
//将十进制转为二进制,并输出每个二进制位的值是0还是1
|
|
|
|
|
int n = 13;
|
|
|
|
|
for (int i = 3; i >= 0; i--)//共4个数位
|
|
|
|
|
cout << (n >> i & 1) << " ";
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 三、$bfs$宽度优先搜索
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#include <bits/stdc++.h>
|
|
|
|
|
|
|
|
|
|
using namespace std;
|
|
|
|
|
typedef pair<int, int> PII;
|
|
|
|
|
#define x first
|
|
|
|
|
#define y second
|
|
|
|
|
const int N = 55, M = N * N;
|
|
|
|
|
|
|
|
|
|
int n, m;
|
|
|
|
|
int g[N][N]; // 地图
|
|
|
|
|
PII q[M]; // 队列
|
|
|
|
|
bool st[N][N]; // 标识是否走过
|
|
|
|
|
|
|
|
|
|
int dx[] = {0, -1, 0, 1}; // 左上右下
|
|
|
|
|
int dy[] = {-1, 0, 1, 0}; // 西北东南 1 2 4 8 二进制位运算,参考1098_0.cpp
|
|
|
|
|
|
|
|
|
|
int bfs(int sx, int sy) {
|
|
|
|
|
int hh = 0, tt = -1;
|
|
|
|
|
q[++tt] = {sx, sy};
|
|
|
|
|
st[sx][sy] = true;
|
|
|
|
|
|
|
|
|
|
// 一次bfs跑一个连通块,统计一个连通块的面积
|
|
|
|
|
int area = 1; // 既然能入队列,最起码有入口房间,是一个面积
|
|
|
|
|
|
|
|
|
|
while (hh <= tt) {
|
|
|
|
|
PII t = q[hh++];
|
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
|
|
|
int tx = t.x + dx[i], ty = t.y + dy[i];
|
|
|
|
|
if (tx == 0 || tx > n || ty == 0 || ty > m) continue;
|
|
|
|
|
if (st[tx][ty]) continue;
|
|
|
|
|
// 1表示西墙,2表示北墙,4表示东墙,8表示南墙
|
|
|
|
|
if (g[t.x][t.y] >> i & 1) continue; // 前进的方向上有墙
|
|
|
|
|
// 连通的房间入队列
|
|
|
|
|
q[++tt] = {tx, ty};
|
|
|
|
|
st[tx][ty] = true;
|
|
|
|
|
area++; // 入队列时房间面积++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return area;
|
|
|
|
|
}
|
|
|
|
|
int cnt, area; // 房间数,最大面积
|
|
|
|
|
int main() {
|
|
|
|
|
cin >> n >> m;
|
|
|
|
|
for (int i = 1; i <= n; i++)
|
|
|
|
|
for (int j = 1; j <= m; j++)
|
|
|
|
|
cin >> g[i][j];
|
|
|
|
|
|
|
|
|
|
for (int i = 1; i <= n; i++)
|
|
|
|
|
for (int j = 1; j <= m; j++)
|
|
|
|
|
if (!st[i][j]) {
|
|
|
|
|
// 从此点出发找连通块
|
|
|
|
|
area = max(area, bfs(i, j));
|
|
|
|
|
cnt++; // 记录连通块个数
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
printf("%d\n%d\n", cnt, area);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
### 四、$dfs$深度优先搜索 [$void$+全局变量法]
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#include <bits/stdc++.h>
|
|
|
|
|
using namespace std;
|
|
|
|
|
const int N = 55;
|
|
|
|
|
int g[N][N];
|
|
|
|
|
int st[N][N];
|
|
|
|
|
int n, m, ans; //注意这里的ans不能做为dfs参数进行传递,因为维护的是同一个变量
|
|
|
|
|
|
|
|
|
|
int dx[] = {0, -1, 0, 1}; //左上右上
|
|
|
|
|
int dy[] = {-1, 0, 1, 0}; //西北东南 1 2 4 8 二进制位运算,参考1098_0.cpp
|
|
|
|
|
|
|
|
|
|
void dfs(int sx, int sy) {
|
|
|
|
|
st[sx][sy] = true; //标识此位置已访问过
|
|
|
|
|
ans++; //到达一个位置,那么面积肯定增大一个
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
|
|
|
int x = sx + dx[i], y = sy + dy[i];
|
|
|
|
|
if (x == 0 || x > n || y == 0 || y > m) continue;
|
|
|
|
|
if (st[x][y]) continue;
|
|
|
|
|
if (g[sx][sy] >> i & 1) continue; //自带数位压缩表示法~,有墙
|
|
|
|
|
dfs(x, y);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
int cnt, area;
|
|
|
|
|
|
|
|
|
|
int main() {
|
|
|
|
|
cin >> n >> m;
|
|
|
|
|
for (int i = 1; i <= n; i++)
|
|
|
|
|
for (int j = 1; j <= m; j++)
|
|
|
|
|
cin >> g[i][j];
|
|
|
|
|
|
|
|
|
|
for (int i = 1; i <= n; i++)
|
|
|
|
|
for (int j = 1; j <= m; j++)
|
|
|
|
|
if (!st[i][j]) {
|
|
|
|
|
cnt++; //连通块数量
|
|
|
|
|
ans = 0; //清零重新统计
|
|
|
|
|
dfs(i, j); //开始Flood Fill
|
|
|
|
|
area = max(area, ans); // PK目前的最大面积
|
|
|
|
|
}
|
|
|
|
|
//输出结果
|
|
|
|
|
printf("%d\n%d\n", cnt, area);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 五、$dfs$深度优先搜索 [$int$+参数变量法]
|
|
|
|
|
```c++
|
|
|
|
|
#include <bits/stdc++.h>
|
|
|
|
|
using namespace std;
|
|
|
|
|
const int N = 55;
|
|
|
|
|
int g[N][N];
|
|
|
|
|
int st[N][N];
|
|
|
|
|
int n, m;
|
|
|
|
|
|
|
|
|
|
int dx[] = {0, -1, 0, 1}; // 左上右上
|
|
|
|
|
int dy[] = {-1, 0, 1, 0}; // 西北东南 1 2 4 8 二进制位运算,参考1098_0.cpp
|
|
|
|
|
|
|
|
|
|
int dfs(int sx, int sy) {
|
|
|
|
|
st[sx][sy] = true; // 标识此位置已访问过
|
|
|
|
|
int ans = 1; // 自己贡献一个面积
|
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
|
|
|
int tx = sx + dx[i], ty = sy + dy[i];
|
|
|
|
|
if (tx == 0 || tx > n || ty == 0 || ty > m) continue;
|
|
|
|
|
if (st[tx][ty]) continue;
|
|
|
|
|
if (g[sx][sy] >> i & 1) continue; // 自带数位压缩表示法~,有墙
|
|
|
|
|
ans += dfs(tx, ty); // 孩子们继续贡献面积
|
|
|
|
|
}
|
|
|
|
|
return ans; // 我们的总面积
|
|
|
|
|
}
|
|
|
|
|
int cnt, area;
|
|
|
|
|
|
|
|
|
|
int main() {
|
|
|
|
|
cin >> n >> m;
|
|
|
|
|
for (int i = 1; i <= n; i++)
|
|
|
|
|
for (int j = 1; j <= m; j++)
|
|
|
|
|
cin >> g[i][j];
|
|
|
|
|
|
|
|
|
|
for (int i = 1; i <= n; i++)
|
|
|
|
|
for (int j = 1; j <= m; j++)
|
|
|
|
|
if (!st[i][j]) {
|
|
|
|
|
cnt++; // 连通块数量
|
|
|
|
|
area = max(area, dfs(i, j)); // PK目前的最大面积
|
|
|
|
|
}
|
|
|
|
|
// 输出结果
|
|
|
|
|
printf("%d\n%d\n", cnt, area);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 六、总结与反思
|
|
|
|
|
* $bfs$时,需要注意一下起点的初始加入,标记个数++
|
|
|
|
|
* $dfs$时,也是在进入函数执行时,就意味着此位置可达,标识个数++
|
|
|
|
|
|
|
|
|
|
### 七、并查集解法
|
|
|
|
|
> $2022-10-08$晚 补充
|
|
|
|
|
|
|
|
|
|
这道题难度并不大,只要熟练使用 **并查集** 即可。给我们一张地图,`#`代表墙壁,我们用`1`表示西墙,`2`表示北墙,`4`表示东墙,`8`表示南墙,将**没有墙壁隔开的连通的方块记为一个房间**,题目共有两问:
|
|
|
|
|
|
|
|
|
|
* 房间的总数
|
|
|
|
|
* 最大的房间的面积
|
|
|
|
|
|
|
|
|
|
这道题可以使用 **并查集** 和 $FloodFill$ 来做,这里讲解一下使用 **并查集** 的做法。
|
|
|
|
|
|
|
|
|
|
我们可以从城堡 **左上角** 开始 **逐行枚举**,每次枚举 **北边** 和 **东边** 两个方向(当然,也可以是 **南边** 和 **东边** 两个方向),就可以**不漏的将可以连接在一起的房间都尽可能的连通**,假设枚举到的位置的数$p=9$,我们需要枚举他东边的墙,我们只需要让$p \& 4$判断是否是$1$,若是$1$则证明 **东边有墙** 。当枚举到的方向没有墙的话,我们就是用 **并查集** 将这两个方块加入到一个连通块中,初始`cnt=n*m`,也就是开始的时候有`n*m`个房间,我们 **连通一次,少一个房间**,最后`cnt`就是房间的个数。
|
|
|
|
|
|
|
|
|
|
如图,共有五个房间:
|
|
|
|
|
<center><img src='https://www.freesion.com/images/447/a50c75a6140b30314b43507557b0d6b7.png'></center>
|
|
|
|
|
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#include <bits/stdc++.h>
|
|
|
|
|
|
|
|
|
|
using namespace std;
|
|
|
|
|
|
|
|
|
|
const int N = 55, M = N * N;
|
|
|
|
|
|
|
|
|
|
// 1表示西墙,2表示北墙,4表示东墙,8表示南墙
|
|
|
|
|
|
|
|
|
|
// 模拟向北和向东两个方向
|
|
|
|
|
int dx[] = {-1, 0}; // 向北走时,x-1,y不变;向东走时,x不变,y+1
|
|
|
|
|
int dy[] = {0, 1};
|
|
|
|
|
int dw[] = {2, 4}; // 2:北墙, 4:东墙
|
|
|
|
|
|
|
|
|
|
// 模拟向东和向南两个方向 也是可以的
|
|
|
|
|
// int dx[] = {0, 1}; //向东走时,x不变,y+1; 向南走时,x+1,y不变
|
|
|
|
|
// int dy[] = {1, 0};
|
|
|
|
|
// int dw[] = {4, 8}; // 4:东墙, 8:南墙
|
|
|
|
|
|
|
|
|
|
// 模拟向南向西 也是可以的
|
|
|
|
|
// int dx[] = {1, 0}; //向南走时,x+1,y不变;向西走时,x不变,y-1
|
|
|
|
|
// int dy[] = {0, -1};
|
|
|
|
|
// int dw[] = {8, 1}; // 8:南墙, 1:西墙
|
|
|
|
|
|
|
|
|
|
// 模拟向南向东 也是可以的
|
|
|
|
|
// int dx[] = {1, 0}; //向南走时,x+1,y不变;向东走时,x不变,y+1
|
|
|
|
|
// int dy[] = {0, 1};
|
|
|
|
|
// int dw[] = {8, 4}; // 8:南墙, 4:东墙
|
|
|
|
|
|
|
|
|
|
int n, m; // n行m列
|
|
|
|
|
int g[N][N]; // 地图
|
|
|
|
|
int p[M], sz[M]; // 并查集数组,与并查集内成员个数
|
|
|
|
|
int find(int x) {
|
|
|
|
|
if (p[x] != x) p[x] = find(p[x]);
|
|
|
|
|
return p[x];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int main() {
|
|
|
|
|
// 读入地图
|
|
|
|
|
cin >> n >> m;
|
|
|
|
|
for (int i = 1; i <= n; i++)
|
|
|
|
|
for (int j = 1; j <= m; j++)
|
|
|
|
|
cin >> g[i][j];
|
|
|
|
|
|
|
|
|
|
// 初始化并查集
|
|
|
|
|
for (int i = 0; i < M; i++) p[i] = i, sz[i] = 1; // 每个人是自己的祖先,并且自己家族的成员数量为1
|
|
|
|
|
|
|
|
|
|
int cnt = n * m, area = 1; // 总共最多有n*m个房间,最大面积最小是1
|
|
|
|
|
|
|
|
|
|
// 开始从上到下,从左到右,枚举每个位置
|
|
|
|
|
for (int i = 1; i <= n; i++)
|
|
|
|
|
for (int j = 1; j <= m; j++)
|
|
|
|
|
// 探讨这个位置的房间,是不是能够向北和向东两个方向前进,也就是检查向北和向东是不是会遇到墙
|
|
|
|
|
for (int u = 0; u < 2; u++)
|
|
|
|
|
if (!(g[i][j] & dw[u])) { // 如果没有遇到墙
|
|
|
|
|
int x = i + dx[u], y = j + dy[u]; // 获取新的位置
|
|
|
|
|
if (x <= 0 || x > n || y <= 0 || y > m) continue; // 出界判断
|
|
|
|
|
int a = (i - 1) * m + j, b = (x - 1) * m + y; // a是指原位置的编号,b是指拓展后位置的编号
|
|
|
|
|
// int a = i * m + j, b = x * m + y; //并查集的编号其实是很灵活的,不是非得啥号不可.上面的那行代码一样可以AC
|
|
|
|
|
|
|
|
|
|
// join 合并并查集
|
|
|
|
|
a = find(a), b = find(b);
|
|
|
|
|
if (a != b) {
|
|
|
|
|
cnt--; // 合并后,连通块的数量将少了一个
|
|
|
|
|
sz[b] += sz[a]; // a家族成员加入b家族中
|
|
|
|
|
p[a] = b; // a认b做父亲
|
|
|
|
|
area = max(area, sz[b]); // 更新答案
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 输出房间总数,输出最大面积
|
|
|
|
|
printf("%d\n%d\n", cnt, area);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
```
|