加载中...

信息奥赛题解|流感传染


信息奥赛题解|流感传染


🚀 题目浏览

【题目描述】

有一批易感人群住在网格状的宿舍区内,宿舍区为 $n*n$ 的矩阵,每个格点为一个房间,房间里可能住人,也可能空着。

在第一天,有些房间里的人得了流感,以后每天,得流感的人会使其邻居传染上流感(已经得病的不变),空房间不会传染。

请输出第 $m$ 天得流感的人数。

【输入】

第一行一个数字 $n$,$n$ 不超过 $100$,表示有 $n*n$ 的宿舍房间。

接下来的 $n$ 行,每行 $n$ 个字符,’.’ 表示第一天该房间住着健康的人,’#’ 表示该房间空着,’@’ 表示第一天该房间住着得流感的人。

接下来的一行是一个整数 $m$,$m$ 不超过 $100$。

【输出】

输出第 $m$ 天,得流感的人数。

【输入样例】

5
....#
.#.@.
.#@..
#....
.....
4

【输出样例】

16

【原题链接】

http://ybt.ssoier.cn:8088/problem_show.php?pid=1191


☘️ 题解分析

在摊主看来,本题对于 算法初学者 主要有以下几个 难点

  • 如何读入字符矩阵 ?
  • 如何模拟感染过程 ?

对于第一个难点,摊主采用的方式是 「定义一个 二维的字符数组、使用 cin 读入数据」(需要开启 cin 读入优化,减少读入数据的时间)。

const int N = 105;
char s[N][N];  // 定义一个二维的字符数组

int main(){
    //cin 读入优化
    ios::sync_with_stdio(false);
    cin.tie(0);
  
    //读入矩阵
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) {
            cin >> s[i][j];
        }
    }
    return 0;
}

🍉 PS:使用 scanf 读入时,需要考虑 换行符的读入,而 cin 则不用,这是 scanf 的缺点;但 scanf 的读入速度比没有开 cin 读入优化的 cin 快很多,这是 scanf 的优点。

cin 在开了 cin 读入优化后,速度基本上能达到 scanf 的水平,此题完全够用了。


对于第二个难点,摊主采用了 遍历模拟 的方法,每天都遍历一次二维矩阵,模拟感染过程。

模拟过程也非常简单,只需在遍历时,遇到 ‘@’ 的房间,就把四周非空的房间置为 ‘@’

❗️❗️❗️但是这种方式非常容易出现 一个误区,就是 「每次矩阵还没有遍历完,就急着把 当前房间感染的新房间置为 ‘@’

这样会出现下面这种情况:

  • 第一天,初始情况
@..
#..
  • 第二天,遍历到第 1 个房间时
@@.
#..
  • 第二天,遍历到第 2 个房间时,由于当前房间被置为了 ‘@’,所以其周围的房间也被置为 ‘@’
@@@
#@.
  • 第二天,遍历到第 3 个房间时,由于当前房间被置为了 ‘@’,所以其周围的房间也被置为 ‘@’
@@@
#@@
  • … 省略剩下的遍历过程

  • 第二天的最终结果

@@@
#@@

这样的得到的结果显然是不对的 ❌

因为实际上第 2 天,仅有 2 号房间被感染,3 号房间需要等到第 3 天才会被感染。✅

  • 第二天实际结果
@@.
#..

所以,我们需要借助一个标记数组 b,存储当天被感染的房间,而不是在遍历时直接修改原矩阵的值。

等当天的 遍历结束后,再根据标记数组 b 来修改原矩阵的值,这样就不会出现上面这种情况了。

从代码的角度,是这样的区别:

//模拟感染过程,并标记
for (int i = 1; i <= n; ++i) {
    for (int j = 1; j <= n; ++j) {
        if (s[i][j] == '@') {
            // 错误的直接修改法
            //if (s[i - 1][j] != '#') s[i - 1][j] = '@';
            //if (s[i + 1][j] != '#') s[i + 1][j] = '@';
            //if (s[i][j - 1] != '#') s[i][j - 1] = '@';
            //if (s[i][j + 1] != '#') s[i][j + 1] = '@';
          
            // 正确的标记法
            if (s[i - 1][j] != '#') b[i - 1][j] = 1;
            if (s[i + 1][j] != '#') b[i + 1][j] = 1;
            if (s[i][j - 1] != '#') b[i][j - 1] = 1;
            if (s[i][j + 1] != '#') b[i][j + 1] = 1;
        }
    }
}

//将带有标记的房间感染
for (int i = 1; i <= n; ++i) {
    for (int j = 1; j <= n; ++j) {
        if (b[i][j] == 1) {
            s[i][j] = '@';
        }
    }
}

PS 🍉:本题也可以使用 广度优先搜索 BFS 的方法求解,具体代码见下文。


🧑🏻‍💻 C++ 代码

模拟

#include<bits/stdc++.h>

using namespace std;
const int N = 105;

char s[N][N];  // 存储房间状态
int b[N][N];  //用于感染的标记数组
int n, m, cnt;

// 每天更新矩阵
void update();

// 统计并输出感染人数
void print();

int main() {
    ios::sync_with_stdio(false);  // cin读入优化
    cin.tie(0);

    //读入矩阵
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) {
            cin >> s[i][j];
        }
    }

    // 统计第m天,需要更新m-1次
    cin >> m;
    while (--m) {
        //更新矩阵
        update();
    }

    // 统计感染总人数
    print();

    return 0;
}


void update() {
    //初始化感染标记数组b
    memset(b, 0, sizeof(b));

    //模拟感染过程,并标记
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) {
            // 向四个方向进行感染,标记位置
            // 注意不能马上把相应房间改为 @,否则会出现一条线上的房间一天就感染完等情况
            if (s[i][j] == '@') {
                if (s[i - 1][j] != '#') b[i - 1][j] = 1;
                if (s[i + 1][j] != '#') b[i + 1][j] = 1;
                if (s[i][j - 1] != '#') b[i][j - 1] = 1;
                if (s[i][j + 1] != '#') b[i][j + 1] = 1;
            }
        }
    }

    //将带有标记的房间感染
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) {
            if (b[i][j] == 1) {
                s[i][j] = '@';
            }
        }
    }
}

void print() {
    cnt = 0;
    //统计感染人数
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) {
            if (s[i][j] == '@')
                cnt++;
        }
    }
    cout << cnt << endl;
}

BFS

#include<bits/stdc++.h>

using namespace std;
typedef pair<int, int> PII;
const int N = 1e2 + 10;
char a[N][N];
int n, m, ans;
int dx[4] = {-1, +1, 0, 0}; // 方向数组
int dy[4] = {0, 0, +1, -1};
queue<PII> q, tmp;  // BFS队列

// bfs返回新感染的人数
int bfs() {
    // q存储当天初始的感染人的坐标
    // tmp存储当天新感染的人的坐标
    // pop()当天q中的坐标,并将四周符合条件的坐标push()到tmp中
    while (!q.empty()) {
        // q的队首元素
        PII h = q.front();
        q.pop();
        int x = h.first, y = h.second;
        
        // 感染四周
        for (int i = 0; i < 4; i++) {
            int x1 = x + dx[i], y1 = y + dy[i];
            if (a[x1][y1] == '.') {
                a[x1][y1] = '@';
                tmp.push({x1, y1});
            }
        }
    }
    // 统计当天感染人数(即tmp中的元素数量),并将tmp中的元素全部push()到q中,便于下一天计算
    int cnt = 0;
    while (!tmp.empty()) {
        PII head = tmp.front();
        cnt++;
        q.push(head);
        tmp.pop();
    }
    return cnt;
}

int main() {
    ios::sync_with_stdio(false);  //cin读入优化
    cin.tie(0);

    cin >> n;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            cin >> a[i][j];
            // 将初始感染人数入队
            if (a[i][j] == '@') q.push({i, j}), ans++;
        }
    }

    cin >> m;  // m天(m==1为初始状态)
    // bfs返回新感染的人数
    for (int i = 1; i < m; i++) ans += bfs();
    cout << ans << endl;
    return 0;
}

文章作者: Rickyの水果摊
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Rickyの水果摊 !
  目录