AcWing 1098. 城堡问题

\(AcWing\) \(1098\). 城堡问题

一、题目描述

   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\)

输入样例

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;
}
posted @ 2022-03-01 18:33  糖豆爸爸  阅读(240)  评论(0编辑  收藏  举报
Live2D