题解:洛谷 P1457 城堡

【题目来源】

洛谷:P1457 [IOI 1994 / USACO2.1] 城堡 The Castle

【题目描述】

喜欢吹嘘的农夫约翰立刻回到有着吹嘘传统的威斯康辛老家开始吹嘘了, 农夫约翰想要告诉他的奶牛们关于他城堡的一切。他需要做一些吹嘘前的准备工作:比如说知道城堡有多少个房间,每个房间有多大。

另外,农夫约翰想要把一面单独的墙(指两个单位间的墙)拆掉以形成一个更大的房间。 你的工作就是帮农夫约翰做以上的准备,算出房间数与房间的大小。

城堡的平面图被划分成 \(n \times m\) 个正方形的单位,一个这样的单位可以有 $0 \sim 4 $ 面墙环绕。城堡周围一定有外墙环绕以遮风挡雨。(就是说平面图的四周一定是墙。)

请仔细研究下面这个有注解的城堡平面图:

     1   2   3   4   5   6   7
   #############################
 1 #   |   #   |   #   |   |   #
   #####---#####---#---#####---#
 2 #   #   |   #   #   #   #   #
   #---#####---#####---#####---#
 3 #   |   |   #   #   #   #   #
   #---#########---#####---#---#
 4 # ->#   |   |   |   |   #   #
   #############################
  • \(\verb!#!\) 表示墙壁;
  • \(\verb!|!\)\(\verb!-!\) 表示没有墙壁;
  • \(\verb!->!\) 指向了一面墙,移除了这面墙我们就有一间最大的新房间。

友情提示,这个城堡的平面图是 \(4 \times 7\) 个单位的。一个“房间”的是平面图中一个由 #-| 围成的格子(就是图里面的那一个个的格子)。比如说这个样例就有 \(5\) 个房间。(大小分别为 \(9,7,3,1,8\) 个单位(排名不分先后))

移去箭头所指的那面墙,可以使 \(2\) 个房间合为一个新房间,且比移去其他墙所形成的房间都大。

城堡保证至少有 \(2\) 个房间,而且一定有一面墙可以被移走。

【输入】

第一行两个正整数 \(m,n\),表示城堡有 \(n\)\(m\) 列。

每一个单位的数字告诉我们这个单位的东西南北是否有墙存在。每个数字是由以下四个整数中的任意个加起来的。

\(1\): 在西面有墙

\(2\): 在北面有墙

\(4\): 在东面有墙

\(8\): 在南面有墙

城堡内部的墙会被规定两次。比如说 \((1,1)\) 南面的墙,亦会被标记为 \((2,1)\) 北面的墙。

【输出】

输出包含如下四行:

第一行:城堡的房间数目。

第二行:最大的房间的大小

第三行:移除一面墙能得到的最大的房间的大小

第四行:移除哪面墙可以得到面积最大的新房间。

选择最佳的墙来推倒。有多解时选最靠西的,仍然有多解时选最靠南的。同一格子北边的墙比东边的墙更优先。

用该墙的南邻单位的北墙或西邻单位的东墙来表示这面墙,方法是输出邻近单位的行数、列数和墙的方位( N(北)或者 E(东))。

【输入样例】

7 4
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
16
4 1 E

【核心思想】

  1. 问题分析:给定 \(n \times m\) 的城堡平面图,每个格子用二进制数表示四面墙的存在情况(\(1\) 西、\(2\) 北、\(4\) 东、\(8\) 南)。需要完成四个任务:统计房间数量、求最大房间面积、求推倒一面内部墙后能得到的最大房间面积、输出最佳墙的位置。这是一个BFS 连通块 + 枚举拆墙问题。

  2. 算法选择

    • BFS 找连通块:对每个未访问的格子进行 BFS,标记所有可达格子为同一房间,统计房间面积
    • 二进制位运算判墙:用 g[i][j] >> k & 1 判断第 \(k\) 个方向是否有墙(\(0\) 西、\(1\) 北、\(2\) 东、\(3\) 南)
    • 枚举拆墙:遍历所有内部墙(北边墙和东边墙),若墙两侧属于不同房间,计算合并后面积,按优先级选取最优墙
  3. 关键步骤

    • 读入数据\(m\)(列数)、\(n\)(行数),以及每个格子的墙信息 \(g[i][j]\)
    • BFS 找房间
      • 遍历所有格子,对未访问的格子启动 BFS
      • BFS 中向四个方向扩展,若该方向无墙且未越界,则加入队列
      • 记录每个格子所属房间编号 belong[i][j] 和每个房间的面积 area[id]
      • 统计房间总数 ans 和最大面积 mare
    • 枚举拆墙
      • 遍历每个格子的北边墙(g[i][j] & 2)和东边墙(g[i][j] & 4
      • 若墙两侧格子属于不同房间,计算合并面积 area[a] + area[b]
      • 按优先级比较:面积更大 > 列更靠西(列号更小)> 行更靠南(行号更大)> N 优先于 E
    • 输出结果:房间数、最大面积、拆墙后最大面积、最佳墙位置(转换为 1-based 坐标)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \cdot m)\),BFS 遍历每个格子一次,枚举墙也是 \(O(n \cdot m)\)
    • 空间复杂度:\(O(n \cdot m)\),存储地图、访问标记、房间编号等数组
  5. BFS 连通块 + 拆墙枚举的核心思想

    • 二进制编码墙信息:每个格子的墙用 4 个二进制位表示,位运算高效判断特定方向是否有墙
    • 连通块即房间:无墙连通的格子属于同一房间,BFS 自然划分房间
    • 拆墙合并房间:推倒一面内部墙等价于合并两个相邻房间,新面积 = 两房间面积之和
    • 多关键字优先级:面积 > 西 > 南 > N/E,通过自定义比较函数实现复杂排序规则
    • 适用于网格连通块、二进制状态、最优拆墙类问题

【算法标签】

普及 #BFS-二维

【代码详解】

#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;  // 定义PII为pair<int,int>,用于队列存储坐标
const int N = 1005;          // 定义数组最大尺寸为1005
int M = N * N;               // M未使用
int m, n;                    // m为列数,n为行数(注意输入顺序:先m后n)
int g[N][N];                 // g[i][j]存储第i行第j列格子的墙信息(二进制:1西2北4东8南)
int belong[N][N];            // belong[i][j]记录格子(i,j)属于哪个房间(房间编号)
int area[N * N];             // area[id]记录编号为id的房间的面积
bool st[N][N];               // st[i][j]标记格子(i,j)是否已被BFS访问过
int dx[4] = {0, -1, 0, 1};   // 四个方向的x偏移量:西、北、东、南(对应二进制位0,1,2,3)
int dy[4] = {-1, 0, 1, 0};   // 四个方向的y偏移量:西、北、东、南
int ans, mare;               // ans记录房间总数,mare记录最大房间面积

// 候选墙结构体:存储推倒该墙后的新房间面积、墙的位置和方向
struct Candidate
{
    int area, row, col;      // area为合并后的新房间面积,row和col为墙的坐标
    char dir;                // dir为墙的方向('N'北或'E'东)
};

// 广度优先搜索:从(sx,sy)开始遍历整个连通块(房间),标记所有属于该房间的格子
void bfs(int sx, int sy, int id)
{
    queue<PII> q;            // BFS队列
    int ar = 0;              // ar记录当前房间的面积(格子数)
    q.push({sx, sy});        // 将起点入队
    st[sx][sy] = 1;          // 标记起点已访问
    belong[sx][sy] = id;     // 标记起点属于房间id

    while (!q.empty())       // 当队列不为空时继续
    {
        auto nd = q.front(); q.pop();  // 取出队首格子
        ar++;                // 当前房间面积加1
        for (int i = 0; i < 4; i++)    // 枚举四个方向
        {
            int nx = nd.first + dx[i];   // 计算相邻格子的行坐标
            int ny = nd.second + dy[i];  // 计算相邻格子的列坐标

            if (nx < 0 || nx >= n || ny < 0 || ny >= m)  // 如果超出边界
                continue;
            if (st[nx][ny])  // 如果相邻格子已访问
                continue;
            // 检查当前方向是否有墙:(g[nd] >> i & 1) == 0 表示该方向没有墙
            if ((g[nd.first][nd.second] >> i & 1) == 0)
            {
                q.push({nx, ny});  // 相邻格子入队
                st[nx][ny] = 1;    // 标记已访问
                belong[nx][ny] = id;  // 标记属于当前房间
            }
        }
    }
    area[id] = ar;           // 记录房间id的面积
    mare = max(mare, ar);    // 更新最大房间面积
}

// 比较两个候选墙,判断x是否比y更优
// 优先级:面积更大 > 列更靠西(列号更小) > 行更靠南(行号更大) > 'N'优先于'E'
bool better(Candidate x, Candidate y)
{
    if (x.area != y.area)    // 首先比较面积
        return x.area > y.area;
    if (x.col != y.col)      // 面积相同,比较列(更靠西优先,即列号更小)
        return x.col < y.col;
    if (x.row != y.row)      // 列相同,比较行(更靠南优先,即行号更大)
        return x.row > y.row;
    return x.dir == 'N' && y.dir == 'E';  // 行列都相同,'N'优先于'E'
}

int main()
{
    cin >> m >> n;           // 读入列数m和行数n(注意输入顺序)

    for (int i = 0; i < n; i++)      // 读入每个格子的墙信息
        for (int j = 0; j < m; j++)
            cin >> g[i][j];

    int cnt = 0;             // cnt为房间编号计数器
    // 遍历所有格子,对每个未访问的格子进行BFS,找出所有房间
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            if (!st[i][j])   // 如果格子(i,j)未被访问
            {
                bfs(i, j, cnt);  // 从(i,j)开始BFS,房间编号为cnt
                cnt++;       // 房间编号加1
                ans++;       // 房间总数加1
            }

    cout << ans << endl;     // 第一行:输出城堡的房间数目
    cout << mare << endl;    // 第二行:输出最大的房间的大小

    Candidate best = {-1, 0, 0, 'N'};  // 初始化最佳候选墙

    // 枚举所有可以推倒的墙(内部墙)
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)    // 注意原代码中j=-0是j=0的笔误
        {
            // 检查北边是否有墙(二进制第1位为1表示北边有墙)
            // 用当前格子的北墙表示,即(i,j)的北边墙,对应(i-1,j)的南边
            if (i > 0 && (g[i][j] & 2))  // 如果北边有墙且不是边界
            {
                int a = belong[i][j];    // 当前格子所属房间
                int b = belong[i - 1][j];  // 北边相邻格子所属房间
                if (a != b)              // 如果属于不同房间
                {
                    Candidate cur = {area[a] + area[b], i, j, 'N'};  // 创建候选墙
                    if (better(cur, best))  // 如果当前候选更优
                        best = cur;         // 更新最佳候选
                }
            }
            // 检查东边是否有墙(二进制第2位为1表示东边有墙)
            // 用当前格子的东墙表示,即(i,j)的东边墙,对应(i,j+1)的西边
            if (j < m - 1 && (g[i][j] & 4))  // 如果东边有墙且不是边界
            {
                int a = belong[i][j];      // 当前格子所属房间
                int b = belong[i][j + 1];  // 东边相邻格子所属房间
                if (a != b)                // 如果属于不同房间
                {
                    Candidate cur = {area[a] + area[b], i, j, 'E'};  // 创建候选墙
                    if (better(cur, best))  // 如果当前候选更优
                        best = cur;         // 更新最佳候选
                }
            }
        }

    cout << best.area << endl;  // 第三行:输出移除一面墙能得到的最大的房间大小
    // 第四行:输出最佳墙的位置(转换为1-based坐标)和方向
    cout << best.row + 1 << " " << best.col + 1 << " " << best.dir << endl;

    return 0;
}

【运行结果】

7 4
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
16
4 1 E
posted @ 2026-08-05 16:22  团爸讲算法  阅读(9)  评论(0)    收藏  举报