You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

AcWing 1098. 城堡问题

一、题目描述

   1   2   3   4   5   6   7  
   #############################
 1 #   |   #   |   #   |   |   #
   #####---#####---#---#####---#
 2 #   #   |   #   #   #   #   #
   #---#####---#####---#####---#
 3 #   |   |   #   #   #   #   #
   #---#########---#####---#---#
 4 #   #   |   |   |   |   #   #
   #############################
           ( 1)

   #  = Wall   
   |  = No wall
   -  = No wall

   方向:上北下南左西右东。

1是一个城堡的地形图。

请你编写一个程序,计算城堡一共有多少房间,最大的房间有多大。

城堡被分割成 mn 个方格区域,每个方格区域可以有0\sim 4面墙。

注意:墙体厚度忽略不计。

输入格式 第一行包含两个整数 mn,分别表示城堡南北方向的长度和东西方向的长度。

接下来 m 行,每行包含 n 个整数,每个整数都表示平面图对应位置的方块的墙的特征。

每个方块中墙的特征由数字 P 来描述,我们用1表示西墙,2表示北墙,4表示东墙,8表示南墙,P 为该方块包含墙的数字之和。

例如,如果一个方块的 P3,则 3 = 1 + 2,该方块包含西墙和北墙。

城堡的内墙被计算两次,方块(1,1)的南墙同时也是方块(2,1)的北墙。

输入的数据保证城堡至少有两个房间。

输出格式 共两行,第一行输出房间总数,第二行输出最大房间的面积(方块数)。

数据范围 1≤m,n≤50,0≤P≤15

输入样例

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 

输出样例

5
9

二、前置知识

这道题的输入挺有意思,为一些数字,其实是理解为0 \sim 15的十进制数,转为二进制就是(0000)_2 \sim (1111)_2,这四个数位,是表示四个方向:

1表示西墙,2表示北墙,4表示东墙,8表示南墙

每个方向存在数字1表示此方向 有墙0表示 无墙

用墙维起来的是房间,求 房间个数最大面积

对于十进制转二进制,并输出二进制的每个数位的值,办法如下:

#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宽度优先搜索

#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+全局变量法]

#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+参数变量法]

#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就是房间的个数。

如图,共有五个房间:

#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;
}