对于牛客网—语言学习篇—编程初学者入门训练—复合类型:BC141 井字棋及BC142 扫雷题目的解析

发布于:2025-09-02 ⋅ 阅读:(17) ⋅ 点赞:(0)

开篇介绍:

hello 大家,上一篇博客便提到了我会把牛客网—语言学习篇—编程初学者入门训练—复合类型:二维数组中较难的题目拆分为三篇博客来进行讲解,上篇博客我们已经解决了BC136 KiKi 判断上三角矩阵BC139 矩阵交换这两道小难题,那么本篇,我们便继续分析BC141 井字棋BC142 扫雷这两道题目。

值得一提的是,扫雷这个小游戏应该是大家学习c语言之路的老相识了,在学习c语言,大家所写的第一个比较复杂的小游戏,也就是扫雷了,不过我们这篇博客的扫雷可不是完整版的扫雷哦,至于是什么样的扫雷,大家看到下文就知道了。

至于井字棋这个小游戏,大家肯定也不会陌生,其相当于是五子棋的三子版,也算是大家的老熟人了,但是同样的,我们这篇博客的井字棋也并不是完整体的井字棋,只能算是实现井字棋这个小游戏的一个不可或缺的部分,至于是哪一部分,大家往下看就知道啦。

下面先给出这两道题的链接,大家可以自行练手哦:

井字棋_牛客题霸_牛客网 https://www.nowcoder.com/practice/0375c1d206ef48a3894f72aa07b2fdde?tpId=290&tqId=618639&sourceUrl=%2Fexam%2Foj%3Fpage%3D1%26tab%3D%25E8%25AF%25AD%25E8%25A8%2580%25E5%25AD%25A6%25E4%25B9%25A0%25E7%25AF%2587%26topicId%3D290扫雷_牛客题霸_牛客网 https://www.nowcoder.com/practice/d5f277427d9a4cd3ae60ea6c276dddfd?tpId=290&tqId=540914&sourceUrl=%2Fexam%2Foj%3Fpage%3D1%26tab%3D%25E8%25AF%25AD%25E8%25A8%2580%25E5%25AD%25A6%25E4%25B9%25A0%25E7%25AF%2587%26topicId%3D290

在这里我先申明一下,接下来本文所讲的方法(第二题不是),对于二维数组元素的输入,都是采用忽略0行0列,从1行1列开始输入,大家有不懂的可以看一下这篇博客:

对于牛客网—语言学习篇—编程初学者入门训练—复合类型:二维数组较简单题目的解析-CSDN博客

BC141 井字棋:

对于开篇介绍中埋下的问题,在我们看了题目后就可以知道答案了,所以我们先看答案:

题意分析:

这道题是经典的井字棋胜负判断问题。我们需要根据输入的三行三列棋盘状态,判断是 Kiki 获胜、BoBo 获胜,还是没有赢家。

  • 游戏规则:在三行三列的九宫格棋盘里,任意一行、任意一列,或者任意一条对角线上出现三个连续相同的棋子(Kiki 的棋子为K,BoBo 的棋子为B,空位为O),持有该棋子的玩家就获胜。
  • 输入:三行三列的字符元素,用空格分隔,元素为K(Kiki 的棋子)、O(空位)、B(BoBo 的棋子)。
  • 输出:若 Kiki 获胜,输出"Kiki wins!";若 BoBo 获胜,输出"BoBo wins!";若没有玩家获胜,输出"No winner"

解题思路:

知道了题意之后,很明显的摆在我们面前的一个问题就是,要怎么判断是哪个人赢了亦或是平局,

  • 游戏规则:在三行三列的九宫格棋盘里,任意一行、任意一列,或者任意一条对角线上出现三个连续相同的棋子(Kiki 的棋子为K,BoBo 的棋子为B,空位为O),持有该棋子的玩家就获胜。

通过这一句,我们便可知道,可是具体要怎么做呢?

要解决这个问题,我们需要检查所有可能的获胜情况,包括:

  1. 行检查:遍历每一行,看是否有一行的三个元素都为K或者都为B
  2. 列检查:遍历每一列,看是否有一列的三个元素都为K或者都为B
  3. 对角线检查:检查两条对角线(从左上到右下、从右上到左下),看是否有一条对角线上的三个元素都为K或者都为B。(这一点需要注意,有两条对角线)

如果在上述任何一种检查中,发现有玩家的三个棋子连成一线,就可以判定该玩家获胜;如果所有检查都完成后,没有玩家满足获胜条件,就判定为没有赢家。

知道了如何判定获胜者,那么接下来我们就得解决如何用代码实现这一目的的问题了,在这里,我给大家提供两个方法。

方法一:

这个方法,就是最简单粗暴的一个方法,因为题目中已经指明了是3*3大小的棋盘,那最直接的方法,便是我们去把每行每列以及两条对角线进行逐个判断即可,因为我们确定了大小,那我们肯定也就能知道棋盘中每个旗子的坐标,我们便可以直接去进行逐个检查,第一行、第二行、第三行、第一列、第二列、第三列、两条对角线,去分别对元素进行判定(借助坐标),详细步骤如下:

  1. 输入棋盘数据:使用嵌套循环,读取三行三列的字符,这些字符代表棋盘状态,其中K是 KiKi 的棋子,B是 BoBo 的棋子,O表示空位。这一步我们需要注意,因为我们是采用忽略0行0列,从1行1列开始输入,而由于创建一个arr数组,编译器是会自动开辟0行0列的空间,相对应的,它也就没有n行n列的空间,但是我们又想要对n行n列进行输入,那么我们就需要在数组初始化时去多一个,比如你要3行3列的空间,你就不能使用arr[3][3],要用arr[4][4]的初始化,这样子才能存到第3行3列。
  2. 检查对角线获胜情况(注意我们是从1行1列开始输入)
    • 检查主对角线(从左上到右下):判断m[1][1]m[2][2]m[3][3]是否相同。若相同,再看是K还是B,若是K则 KiKi 获胜,若是B则 BoBo 获胜。
    • 检查副对角线(从右上到左下):判断m[1][3]m[2][2]m[3][1]是否相同。若相同,再看是K还是B,确定获胜方。
  3. 检查行和列获胜情况
    • 检查每一行:遍历每一行,判断该行的三个元素是否相同。若相同,再看是K还是B,确定获胜方。
    • 检查每一列:遍历每一列,判断该列的三个元素是否相同。若相同,再看是K还是B,确定获胜方。
  4. 判断无获胜情况:如果经过上述所有对角线、行、列的检查,都没有出现三个相同的KB,则输出"No winner"

由上,我们便能得到第一个方法的实现过程了,下面我便直接给出完整代码:

#include<stdio.h>

int main(){
    char m[4][4];
    for(int i=1;i<=3;i++){
        for(int j=1;j<=3;j++){
            scanf("%c ",&m[i][j]);
        }
    }
    if(m[1][1]==m[2][2]&&m[1][1]==m[3][3]){
        if(m[1][1]=='K') goto K;
        else if(m[1][1]=='B') goto B;
    }
    if(m[1][3]==m[2][2]&&m[1][3]==m[3][1]){
        if(m[1][3]=='K')goto K;
        else if(m[1][3]=='B')goto B;
    }
    for(int i=1;i<=3;i++){
        if(m[i][1]==m[i][2]&&m[i][1]==m[i][3]){
            if(m[i][1]=='K')goto K;
            else if(m[i][1]=='B')goto B;
        }
        if(m[1][i]==m[2][i]&&m[1][i]==m[3][i]){
            if(m[1][i]=='K')goto K;
            else if(m[1][i]=='B')goto B;
        }
    }
    printf("No winner!\n");
    return 0;
    K:
    printf("KiKi wins!\n");
    return 0;
    B:
    printf("BoBo wins!\n");
    return 0;
}

goto语句大家应该不陌生,我这里就不多于讲述,下面再给上该代码的详细注释版:

#include<stdio.h>
int main(){
    // 定义一个4x4的字符数组m,目的是让数组索引从1开始使用(下标1-3)
    // 实际有效存储区域是m[1][1]到m[3][3],对应棋盘的1行1列到3行3列
    // 其中,'K'代表KiKi的棋子,'B'代表BoBo的棋子,'O'代表空位
    char m[4][4];
    
    // 外层循环控制行,i从1到3,分别对应棋盘的第1行、第2行、第3行
    // 采用1开始的索引,更符合日常对"第1行"的直观认知
    for(int i=1;i<=3;i++){
        // 内层循环控制列,j从1到3,分别对应棋盘的第1列、第2列、第3列
        for(int j=1;j<=3;j++){
            // 读取输入的字符并存入数组的第i行第j列
            // 格式字符串"%c "中的空格用于跳过输入中的分隔空格,确保正确读取每个棋子
            scanf("%c ",&m[i][j]);
        }
    }
    
    // 检查主对角线(从左上角1行1列到右下角3行3列)是否有获胜情况
    // 主对角线的三个关键位置:(1,1)、(2,2)、(3,3)
    // 判断这三个位置的字符是否完全相同(可能是'K'、'B'或'O')
    if(m[1][1]==m[2][2]&&m[1][1]==m[3][3]){
        // 如果主对角线上的字符是'K',说明KiKi获胜,跳转到K标签输出结果
        if(m[1][1]=='K') goto K;
        // 如果主对角线上的字符是'B',说明BoBo获胜,跳转到B标签输出结果
        else if(m[1][1]=='B') goto B;
        // 若为'O',则是三个空位,不满足获胜条件,继续检查其他情况
    }
    
    // 检查副对角线(从右上角1行3列到左下角3行1列)是否有获胜情况
    // 副对角线的三个关键位置:(1,3)、(2,2)、(3,1)
    if(m[1][3]==m[2][2]&&m[1][3]==m[3][1]){
        // 如果副对角线上的字符是'K',跳转到K标签
        if(m[1][3]=='K')goto K;
        // 如果副对角线上的字符是'B',跳转到B标签
        else if(m[1][3]=='B')goto B;
        // 若为'O',继续检查其他情况
    }
    
    // 循环检查每一行和每一列是否有获胜情况(i从1到3,代表行号和列号)
    for(int i=1;i<=3;i++){
        // 检查第i行的三个元素是否相同(i行1列、i行2列、i行3列)
        if(m[i][1]==m[i][2]&&m[i][1]==m[i][3]){
            // 若为'K',说明KiKi在第i行连成三个,跳转到K标签
            if(m[i][1]=='K')goto K;
            // 若为'B',说明BoBo在第i行连成三个,跳转到B标签
            else if(m[i][1]=='B')goto B;
        }
        
        // 检查第i列的三个元素是否相同(1行i列、2行i列、3行i列)
        if(m[1][i]==m[2][i]&&m[1][i]==m[3][i]){
            // 若为'K',说明KiKi在第i列连成三个,跳转到K标签
            if(m[1][i]=='K')goto K;
            // 若为'B',说明BoBo在第i列连成三个,跳转到B标签
            else if(m[1][i]=='B')goto B;
        }
    }
    
    // 如果所有检查都没有发现获胜情况(没有任何一行、一列或对角线出现三个相同的'K'或'B')
    // 则输出"No winner!",表示没有赢家
    printf("No winner!\n");
    return 0;
    
    // K标签:当KiKi获胜时,程序会跳转到这里
    // 输出"KiKi wins!"并结束程序
    K:
    printf("KiKi wins!\n");
    return 0;
    
    // B标签:当BoBo获胜时,程序会跳转到这里
    // 输出"BoBo wins!"并结束程序
    B:
    printf("BoBo wins!\n");
    return 0;
}

方法二:

这个方法,才是我最为推荐的,虽然大家可能会觉得方法一简单好使,但是它的通用性却不强,三子棋我们还可以一个一个敲元素坐标,可要是五子棋、十子棋呢?那我们还是一个一个的那么手敲,那不得敲到猴年马月,所以,为了更强的通用性,方法二便来了。

行的判断:

我们最重要的便是要实现胜利的判断,而这又有三部分:行、列、对角线,那么我们这边就先进行行的判断,我们可以思考一下,对于判断行胜利时,是要求某一行的三个元素都为K或者B,那我们针对这个,可以进行遍历二维数组的元素,从第一行第一个到最后一行最后一个,但是问题是,我们要怎么才能知道某一行的三个元素都为K或者B呢?

其实很简单,我们可以设置两个变量k和b,当某个元素为K时,k++,当某个元素为B时,b++,之后再设置两个标识变量flagk、flagb,初始化为0,当出现某一行的元素都为K时(即k==3),flagk变为1,出现某一行的元素都为B时(即b==3),flagb变为1,在最后表达的时候,我们再借助这两个标识变量进行限制输出,flagk为1时,输出k获胜,flagb为1时,输出b获胜。

如上便是我们实现行判断的一个思路,但是却不止这些,准确来说是,仅仅上面那些还不够,而是哪里不够呢?设置两个变量k和b,当某个元素为K时,k++,当某个元素为B时,b++,这一步,还不够完善,还有瑕疵,我们可以知道flagk/b的判断是看k/b有没有变成3来进行的,但是由于我们是对整个二维数组的所有元素进行遍历,如果第一行有2个K,第二行有1个K,第三行无K,那此时k也==3,flagk变为3,但是我们能说是k获胜了吗,很显然,并不能,还有就是第一行有3个K,第二行有2个K,第三行没有K,此时k为5,很明显不为3,flagk不会为1,也就不会表达k获胜,可是k真的没有获胜吗?很显然,k是获胜的。

所以,知道了上面的问题之后,我们就得对这一步k、b的计数进行修改(完善了),因为我们是进行行判断,即一行一行的进行判断,当出现某一行有三个相同的K或者B时,就相应对flagk、flagb进行改变,并不是需要判断所有的元素,那么我们便可以针对这个问题,进行限制了,即在每一行结束时,就判断k或者b为不为3,为的话,就相应的改变标识变量,同时终止对元素的遍历,节省消耗,那么如果一行中只有1个或者2个亦或是0个K/B的h话,我们就不让标识变量改变,也不终止遍历,而是进入下一行进行遍历,但是要注意,在进入下一行时,我们要人k、b的值重新为0,不难很有可能就会出现k/b==5等等的情况,那么就无法正常判断,所以我们就需要在每次进入下一行时进行k、b的重置为0,使其不会计数超过3个,能够正常的只判断一行中K/B的个数。

如下便是进行行判断的详细代码:

int k = 0;
int b = 0;
int flagk = 0;
int flagb = 0;

// 判断行
for (int i = 1; i <= 3; i++)
{
    for (int j = 1; j <= 3; j++)
    {
        if (arr[i][j] == 'K')
        {
            k++;  // 当k等于3的时候代表3个都是K
        }
        
        if (j == 3)  // 每到一行尾,检测这行是否有三个相同的
        {
            if (k == 3)
            {
                flagk = 1;
                break;//终止循环,减少消耗
            }
            k = 0;  // 若该行不满足,重置k统计下一行
        }
        
        if (arr[i][j] == 'B')
        {
            b++;  // 当b等于3的时候代表3个都是B
        }
        
        if (j == 3)
        {
            if (b == 3)
            {
                flagb = 1;
                break;
            }
            b = 0;// 若该行不满足,重置b统计下一行
        }
    }
}

列的判断:

知道了行的判断之后,我们就得着手于列的判断,其实列的判断的原理与行的判断一样,也需要借助k、b、flagk、flagb的帮助,只不过列的遍历,却是不能像行那样子遍历,那么我们要怎么实现对列的遍历呢?

我们不妨先看一下每一列的元素的坐标:

列号 元素坐标及内容
第 1 列 (1,1)(K)、(2,1)(O)、(3,1)(B)
第 2 列 (1,2)(O)、(2,2)(K)、(3,2)(O)
第 3 列 (1,3)(B)、(2,3)(B)、(3,3)(K)

我们可以观察到,在进行列的遍历时,元素坐标的行是不变的,而列是依次递增的,那么知道了这一个信息之后吗,我们就能知道如何改变循环条件去实现列的遍历。

for (int i = 1; i <= 3; i++)//行遍历
{
    for (int j = 1; j <= 3; j++)
    {
        if (arr[i][j] == 'K')
    }
}

结合这段进行行遍历的代码,这段代码是当j全部循环完,i才++,相当于是(1,1)->(1,2)->(1,3),列变行不变,便可以是实现行遍历,那么我们想要实现列遍历,那就让i和j互换不就行了,从arr[i][j]变成arr[j][i],这样子就是(1,1)->(2,1)->(3,1),由此,便可以精确实现列的遍历。即如下代码:

for (int i = 1; i <= 3; i++)//行遍历
{
    for (int j = 1; j <= 3; j++)
    {
        if (arr[j][i] == 'K')
    }
}

当然,由于我们一般默认i表示行,j表示列,如上的代码,我们自己看都有点别扭,更边说第一次见到的其他人了,所以,为了避免这种情况,我们可以继续使用arr[i][j],只不过,我们就需要把两层循环的i和j对调了,即变成这样子:

for (int j = 1; j <= 3; j++)//行遍历
{
    for (int i = 1; i <= 3; i++)
    {
        if (arr[i][j] == 'K')
    }
}

这样子,也可以实现(1,1)->(2,1)->(3,1),大家可以自行模拟一下过程。

到这里,我们便顺利解决了列的判断,下面是完整代码:

// 判断列
for (int i = 1; i <= 3; i++)
{
    for (int j = 1; j <= 3; j++)
    {
        if (arr[j][i] == 'K')
        {
            k++;  // 当k等于3的时候代表3个都是K
        }
        
        if (j == 3)  // 每到一列尾,检测这列是否有三个相同的
        {
            if (k == 3)
            {
                flagk = 1;
                break;
            }
            k = 0;  // 若该列不满足,重置k统计下一列
        }
        
        if (arr[j][i] == 'B')
        {
            b++;  // 当b等于3的时候代表3个都是B
        }
        
        if (j == 3)
        {
            if (b == 3)
            {
                flagb = 1;
                break;
            }
            b = 0;
        }
    }
}

对角线的判断:

接下来,我们就得开始把目光投向对角线了,我们先从主对角线(从左到右)开始,我们可以先观察主对角线元素的坐标:

位置描述 坐标 元素
主对角线第一个 (1, 1) K
主对角线第二个 (2, 2) K
主对角线第三个 (3, 3) K

可以看到,主对角线的元素坐标中,行和列都是一样的,也就是i==j,那么,我们也就可以实现对主对角线的判断,即如下:

// 判断对角线,第一条对角线(主对角线:从左上角到右下角)
for (int i = 1; i <= 3; i++)
{
    if (arr[i][i] == 'K')
    {
        k++;
    }
    if (arr[i][i] == 'B')
    {
        b++;
    }
}

//第二种方式:
// 判断主对角线(从左上角到右下角)
for (int i = 1; i <= 3; i++)
{
    for (int j = 1; j <= 3; j++)
    {
        // 仅处理主对角线上的元素(行索引等于列索引)
        if (i == j)
        {
            if (arr[i][j] == 'K')
            {
                k++;
            }
            if (arr[i][j] == 'B')
            {
                b++;
            }
        }
    }
}

// 检查主对角线上是否有3个连续的'K'
if (k == 3)
{
    flagk = 1;
}

// 检查主对角线上是否有3个连续的'B'
if (b == 3)
{
    flagb = 1;
}

较简单,不过要注意,因为对角线上就3个元素,所以我们不用对k/b重置为0

接下来,我们便需要判断从右往左的对角线(副对角线)了,我们依旧是先看这条对角线的元素坐标:

位置描述 坐标 元素
从右往左对角线第一个 (1, 3) B
从右往左对角线第二个 (2, 2) K
从右往左对角线第三个 (3, 1) K

不知道大家有没有发现什么规律,说实话,我是没发现,嘿嘿,开玩笑,其实这个规律很难发现,所以我这边就直接给大家说吧:

从右往左的对角线(副对角线)元素的坐标规律是:行号与列号之和为 4,如果是从0行0列开始存储的话,那就是和为3。

具体来看:

  • 第一个元素坐标 (1, 3),1+3=4;即j=4-i或者i=j-4
  • 第二个元素坐标 (2, 2),2+2=4;即j=4-i或者i=j-4
  • 第三个元素坐标 (3, 1),3+1=4。即j=4-i或者i=j-4

不错,这便是规律,所以我们想要遍历副对角线的元素也有迹可循了。具体如下:

// 判断第二条对角线(右上→左下:行i从1→3,列j从3→1,规律:j = 4 - i)
k = 0;  // 复用之前定义的k、b变量,无需重新定义
b = 0;

for (int i = 1; i <= 3; i++)
{
    int j = 4 - i;  // 核心规律:i=1→j=3,i=2→j=2,i=3→j=1
    
    if (arr[i][j] == 'K')
        k++; //由于对角线就三个元素,也就不用重置为0了
    if (arr[i][j] == 'B')
        b++;
}

// 若计数满3,置对应获胜标志
if (k == 3)
    flagk = 1;
if (b == 3)
    flagb = 1;

如上。

总代码:

到此,本题收工,下面给上方法二的详细代码:

#include <stdio.h>

int main()
{
	char arr[4][4] = { 0 };//因为我们不存储0那一行和列,但是编译器会自动开辟0,导致就没有3的空间,所以我们要多一个
	for (int i = 1; i <= 3; i++)
	{
		for (int j = 1; j <= 3; j++)
		{
			scanf(" %c", &arr[i][j]);
		}
	}
	int k = 0;
	int b = 0;
	int flagk = 0;
	int flagb = 0;
	//判断行
	for (int i = 1; i <= 3; i++)
	{
		for (int j = 1; j <= 3; j++)
		{
			if (arr[i][j] == 'K')
			{
				k++;//当k等于3的时候代表3个都是K
			}
			if (j == 3)//每到一行尾,我们就检测一下这行是不是有三个一样的
			{
				if (k == 3)
				{
					flagk = 1;
					break;
				}
				k = 0;//如果某一行没有出现3个都为相同,就让k为0,统计下一行
			}
			if (arr[i][j] == 'B')
			{
				b++;//当b等于3的时候代表3个都是B
			}
			if (j == 3)
			{
				if (b == 3)
				{
					flagb = 1;
					break;
				}
				b = 0;
			}
		}
	}
	k = 0;
	b = 0;
	//判断列
	for (int i = 1; i <= 3; i++)
	{
		for (int j = 1; j <= 3; j++)
		{
			if (arr[j][i] == 'K')
			{
				k++;//当k等于3的时候代表3个都是K
			}
			if (j == 3)//每到一列尾,我们就检测一下这列是不是有三个一样的
			{
				if (k == 3)
				{
					flagk = 1;
					break;
				}
				k = 0;//如果某一行没有出现3个都为相同,就让k为0,统计下一行
			}
			if (arr[j][i] == 'B')
			{
				b++;//当b等于3的时候代表3个都是B
			}
			if (j == 3)
			{
				if (b == 3)
				{
					flagb = 1;
					break;
				}
				b = 0;
			}
		}
	}
	k = 0;
	b = 0;
	//判断对角线,第一条对角线
	for (int i = 1; i <= 3; i++)
	{
		if (arr[i][i] == 'K')
		{
			k++;
		}
		if (arr[i][i] == 'B')
		{
			b++;
		}
	}
	if (k == 3)
	{
		flagk = 1;
	}
	if (b == 3)
	{
		flagb = 1;
	}
// 判断第二条对角线(右上→左下:行i从1→3,列j从3→1,规律:j = 4 - i)
    k = 0;  // 复用之前定义的k、b变量,无需重新定义
    b = 0;
    for (int i = 1; i <= 3; i++)
    {
        int j = 4 - i;  // 核心规律:i=1→j=3,i=2→j=2,i=3→j=1
        if (arr[i][j] == 'K')
            k++;
        if (arr[i][j] == 'B')
            b++;
    }
// 若计数满3,置对应获胜标志
if (k == 3)
    flagk = 1;
if (b == 3)
    flagb = 1;
	//进行表达
	if (flagk == 1)
	{
		printf("KiKi wins!");
	}
	else if (flagb == 1)
	{
		printf("BoBo wins!");
	}
	else
	{
		printf("No winner!");
	}
	return 0;
}

再给上详细注释版的:

#include <stdio.h>

// 主函数:程序的入口点,负责整个井字棋游戏的胜负判断流程
int main()
{
    /*
        定义一个4行4列的字符数组arr,用于存储井字棋棋盘的状态
        为什么用4x4而不是3x3?
        - 因为我们要使用1-based索引(即行和列都从1开始计数,符合日常习惯)
        - 实际有效使用的是arr[1][1]到arr[3][3]的区域(共3x3=9个位置)
        - 初始化值为0,确保未使用的arr[0][*]和arr[*][0]不会干扰判断
    */
    char arr[4][4] = { 0 };

    /*
        读取用户输入的3x3棋盘数据
        外层循环变量i表示行号,范围1~3(对应第1行到第3行)
        内层循环变量j表示列号,范围1~3(对应第1列到第3列)
    */
    for (int i = 1; i <= 3; i++)
    {
        for (int j = 1; j <= 3; j++)
        {
            /*
                使用scanf函数读取一个字符到arr[i][j]
                格式字符串" %c"中的空格非常重要:
                - 作用是跳过输入中的空白字符(包括空格、换行符、制表符等)
                - 确保每次都能正确读取到棋盘上的有效字符('K'、'B'或表示空位的字符)
                - 如果没有这个空格,可能会误读换行符作为棋盘数据
            */
            scanf(" %c", &arr[i][j]);
        }
    }

    /*
        定义变量用于判断胜负:
        - k:计数器,统计当前检查线上'K'的数量
        - b:计数器,统计当前检查线上'B'的数量
        - flagk:KiKi获胜的标志(1表示获胜,0表示未获胜)
        - flagb:BoBo获胜的标志(1表示获胜,0表示未获胜)
    */
    int k = 0;
    int b = 0;
    int flagk = 0;
    int flagb = 0;

    /*
        第一步:检查所有行是否有获胜情况
        井字棋规则:如果某一行的3个位置都是同一个玩家的棋子,则该玩家获胜
    */
    // 外层循环i遍历每一行(i=1对应第1行,i=2对应第2行,i=3对应第3行)
    for (int i = 1; i <= 3; i++)
    {
        // 内层循环j遍历当前行的每一列(j=1到3)
        for (int j = 1; j <= 3; j++)
        {
            // 检查当前位置(arr[i][j])是否是'K',如果是则k计数器加1
            if (arr[i][j] == 'K')
            {
                k++;  // 每找到一个'K',计数加1,当k=3时表示整行都是'K'
            }

            // 当j=3时,表示已经遍历到当前行的最后一个元素(第3列)
            if (j == 3)
            {
                // 如果k=3,说明当前行的3个元素都是'K',KiKi获胜
                if (k == 3)
                {
                    flagk = 1;  // 设置KiKi获胜标志为1
                    break;      // 跳出内层循环,无需检查当前行的其他元素
                }
                k = 0;  // 重置k计数器,准备统计下一行的'K'数量
            }

            // 检查当前位置(arr[i][j])是否是'B',如果是则b计数器加1
            if (arr[i][j] == 'B')
            {
                b++;  // 每找到一个'B',计数加1,当b=3时表示整行都是'B'
            }

            // 再次检查是否到达当前行的最后一列(j=3)
            if (j == 3)
            {
                // 如果b=3,说明当前行的3个元素都是'B',BoBo获胜
                if (b == 3)
                {
                    flagb = 1;  // 设置BoBo获胜标志为1
                    break;      // 跳出内层循环,无需检查当前行的其他元素
                }
                b = 0;  // 重置b计数器,准备统计下一行的'B'数量
            }
        }
    }

    /*
        第二步:检查所有列是否有获胜情况
        先重置计数器k和b,避免受到之前行检查的影响
    */
    k = 0;
    b = 0;

    // 外层循环i遍历每一列(i=1对应第1列,i=2对应第2列,i=3对应第3列)
    for (int i = 1; i <= 3; i++)
    {
        // 内层循环j遍历当前列的每一行(j=1到3)
        for (int j = 1; j <= 3; j++)
        {
            /*
                注意数组访问方式是arr[j][i]而不是arr[i][j]
                - j表示行号,i表示列号
                - 这样就能按列访问:对于第i列,依次访问第1行、第2行、第3行的元素
            */
            if (arr[j][i] == 'K')
            {
                k++;  // 统计当前列中'K'的数量
            }

            // 当j=3时,表示已经遍历到当前列的最后一个元素(第3行)
            if (j == 3)
            {
                // 如果k=3,说明当前列的3个元素都是'K',KiKi获胜
                if (k == 3)
                {
                    flagk = 1;  // 设置KiKi获胜标志
                    break;      // 跳出内层循环
                }
                k = 0;  // 重置k计数器,准备统计下一列
            }

            // 检查当前列中的'B'数量
            if (arr[j][i] == 'B')
            {
                b++;  // 统计当前列中'B'的数量
            }

            // 检查是否到达当前列的最后一行(j=3)
            if (j == 3)
            {
                // 如果b=3,说明当前列的3个元素都是'B',BoBo获胜
                if (b == 3)
                {
                    flagb = 1;  // 设置BoBo获胜标志
                    break;      // 跳出内层循环
                }
                b = 0;  // 重置b计数器,准备统计下一列
            }
        }
    }

    /*
        第三步:检查第一条对角线(主对角线)是否有获胜情况
        主对角线:从左上角到右下角的对角线,坐标满足"行号=列号"
        包含的位置:(1,1)、(2,2)、(3,3)
        先重置计数器k和b
    */
    k = 0;
    b = 0;

    // 循环变量i同时代表行号和列号(因为主对角线上i==j)
    for (int i = 1; i <= 3; i++)
    {
        // 检查主对角线上的元素是否为'K'
        if (arr[i][i] == 'K')
        {
            k++;  // 统计'K'的数量
        }
        // 检查主对角线上的元素是否为'B'
        if (arr[i][i] == 'B')
        {
            b++;  // 统计'B'的数量
        }
    }
    // 主对角线上有3个'K',则KiKi获胜
    if (k == 3)
    {
        flagk = 1;
    }
    // 主对角线上有3个'B',则BoBo获胜
    if (b == 3)
    {
        flagb = 1;
    }

    /*
        第四步:检查第二条对角线(副对角线)是否有获胜情况
        副对角线:从右上角到左下角的对角线,坐标满足"行号+列号=4"
        包含的位置:(1,3)、(2,2)、(3,1)
        先重置计数器k和b
    */
    k = 0;
    b = 0;

    // 外层循环i表示行号(1到3)
    for (int i = 1; i <= 3; i++)
    {
        // 根据副对角线规律计算列号:j = 4 - i
        // 当i=1时,j=3;i=2时,j=2;i=3时,j=1,正好对应副对角线上的位置
        int j = 4 - i;

        // 检查副对角线上的元素是否为'K'
        if (arr[i][j] == 'K')
            k++;  // 统计'K'的数量
        // 检查副对角线上的元素是否为'B'
        if (arr[i][j] == 'B')
            b++;  // 统计'B'的数量
    }
    // 副对角线上有3个'K',则KiKi获胜
    if (k == 3)
        flagk = 1;
    // 副对角线上有3个'B',则BoBo获胜
    if (b == 3)
        flagb = 1;

    /*
        根据获胜标志输出最终结果
        判断优先级:先判断KiKi是否获胜,再判断BoBo是否获胜,最后判断无赢家
    */
    if (flagk == 1)
    {
        printf("KiKi wins!");  // KiKi获胜时输出
    }
    else if (flagb == 1)
    {
        printf("BoBo wins!");  // BoBo获胜时输出
    }
    else
    {
        printf("No winner!");  // 没有获胜者时输出
    }

    return 0;  // 程序正常结束
}

扩展:

前面说了,方法二通用性较强,可以用于五子棋等多子棋,那么要怎么实现呢?其实也很简单,我们只需要把原本的3(注意:我们这里的3是指每行每列有几个元素,并不代表是3子棋的3)改为你想要实现的n子棋的每行每列的元素个数就行,下面是五子棋的示例:

#include <stdio.h>

// 定义五子棋棋盘大小(15x15),使用1-based索引
#define SIZE 16  // 实际使用1~15行和列

int main()
{
    // 创建16x16数组,0行0列弃用,实际使用1~15范围
    char arr[SIZE][SIZE] = {0};
    
    // 读取15x15的棋盘数据
    printf("请输入15x15棋盘数据(B表示黑棋,W表示白棋,O表示空位):\n");
    for (int i = 1; i < SIZE; i++)
    {
        for (int j = 1; j < SIZE; j++)
        {
            scanf(" %c", &arr[i][j]);  // 空格用于跳过空白字符
        }
    }
    
    // 定义计数变量和标志变量
    int b = 0;       // 黑棋计数器
    int w = 0;       // 白棋计数器
    int flagb = 0;   // 黑棋获胜标志
    int flagw = 0;   // 白棋获胜标志
    
    // 1. 判断所有行是否有五连子
    for (int i = 1; i < SIZE; i++)       // 遍历每一行
    {
        for (int j = 1; j < SIZE; j++)   // 遍历当前行的每一列
        {
            // 统计黑棋数量
            if (arr[i][j] == 'B')
            {
                b++;
                // 连续5个黑棋则获胜
                if (b >= 5)
                {
                    flagb = 1;
                    goto result;  // 找到获胜者直接跳转至结果输出
                }
            }
            else
            {
                b = 0;  // 非黑棋则重置计数器
            }
            
            // 统计白棋数量
            if (arr[i][j] == 'W')
            {
                w++;
                // 连续5个白棋则获胜
                if (w >= 5)
                {
                    flagw = 1;
                    goto result;  // 找到获胜者直接跳转至结果输出
                }
            }
            else
            {
                w = 0;  // 非白棋则重置计数器
            }
        }
        // 一行结束后重置计数器
        b = 0;
        w = 0;
    }
    
    // 重置计数器,准备判断列
    b = 0;
    w = 0;
    
    // 2. 判断所有列是否有五连子
    for (int i = 1; i < SIZE; i++)       // 遍历每一列
    {
        for (int j = 1; j < SIZE; j++)   // 遍历当前列的每一行
        {
            // 统计黑棋数量(注意数组索引是arr[j][i])
            if (arr[j][i] == 'B')
            {
                b++;
                if (b >= 5)
                {
                    flagb = 1;
                    goto result;
                }
            }
            else
            {
                b = 0;
            }
            
            // 统计白棋数量
            if (arr[j][i] == 'W')
            {
                w++;
                if (w >= 5)
                {
                    flagw = 1;
                    goto result;
                }
            }
            else
            {
                w = 0;
            }
        }
        // 一列结束后重置计数器
        b = 0;
        w = 0;
    }
    
    // 重置计数器,准备判断正对角线(\方向)
    b = 0;
    w = 0;
    
    // 3. 判断正对角线方向(\)是否有五连子
    // 从第一行开始向右遍历
    for (int i = 1; i < SIZE; i++)
    {
        int x = 1, y = i;  // 起始点(1,i)
        while (x < SIZE && y < SIZE)
        {
            if (arr[x][y] == 'B')
            {
                b++;
                if (b >= 5)
                {
                    flagb = 1;
                    goto result;
                }
            }
            else
            {
                b = 0;
            }
            
            if (arr[x][y] == 'W')
            {
                w++;
                if (w >= 5)
                {
                    flagw = 1;
                    goto result;
                }
            }
            else
            {
                w = 0;
            }
            
            x++;  // 沿对角线移动(行+1,列+1)
            y++;
        }
        b = 0;
        w = 0;
    }
    
    // 从第一列开始向下遍历(排除已检查过的对角线)
    for (int i = 2; i < SIZE; i++)
    {
        int x = i, y = 1;  // 起始点(i,1)
        while (x < SIZE && y < SIZE)
        {
            if (arr[x][y] == 'B')
            {
                b++;
                if (b >= 5)
                {
                    flagb = 1;
                    goto result;
                }
            }
            else
            {
                b = 0;
            }
            
            if (arr[x][y] == 'W')
            {
                w++;
                if (w >= 5)
                {
                    flagw = 1;
                    goto result;
                }
            }
            else
            {
                w = 0;
            }
            
            x++;  // 沿对角线移动(行+1,列+1)
            y++;
        }
        b = 0;
        w = 0;
    }
    
    // 重置计数器,准备判断反对角线(/)方向
    b = 0;
    w = 0;
    
    // 4. 判断反对角线方向(/)是否有五连子
    // 从第一行开始向左遍历
    for (int i = 1; i < SIZE; i++)
    {
        int x = 1, y = i;  // 起始点(1,i)
        while (x < SIZE && y >= 1)
        {
            if (arr[x][y] == 'B')
            {
                b++;
                if (b >= 5)
                {
                    flagb = 1;
                    goto result;
                }
            }
            else
            {
                b = 0;
            }
            
            if (arr[x][y] == 'W')
            {
                w++;
                if (w >= 5)
                {
                    flagw = 1;
                    goto result;
                }
            }
            else
            {
                w = 0;
            }
            
            x++;  // 沿对角线移动(行+1,列-1)
            y--;
        }
        b = 0;
        w = 0;
    }
    
    // 从最后一列开始向下遍历(排除已检查过的对角线)
    for (int i = 2; i < SIZE; i++)
    {
        int x = i, y = SIZE - 1;  // 起始点(i,15)
        while (x < SIZE && y >= 1)
        {
            if (arr[x][y] == 'B')
            {
                b++;
                if (b >= 5)
                {
                    flagb = 1;
                    goto result;
                }
            }
            else
            {
                b = 0;
            }
            
            if (arr[x][y] == 'W')
            {
                w++;
                if (w >= 5)
                {
                    flagw = 1;
                    goto result;
                }
            }
            else
            {
                w = 0;
            }
            
            x++;  // 沿对角线移动(行+1,列-1)
            y--;
        }
        b = 0;
        w = 0;
    }
    
result:
    // 输出判断结果
    if (flagb == 1)
    {
        printf("黑棋获胜!\n");
    }
    else if (flagw == 1)
    {
        printf("白棋获胜!\n");
    }
    else
    {
        printf("暂无获胜者!\n");
    }
    
    return 0;
}
    

大功告成。

BC142 扫雷:

这道题,其实只是实现扫雷这个游戏中的判断一个点周围有几个地雷的功能,我们直接看题目:

题意分析:

这道题是经典的扫雷游戏相关题目,需要根据给定的初始扫雷矩阵,生成完整的扫雷矩阵。

题目核心要求

  • 输入:
    • 两个整数 n 和 m,分别表示矩阵的行数和列数(1≤n,m≤1000)。
    • 接下来 n 行,每行有 m 个字符,字符为 *(表示雷)或 .(表示空白)。
  • 输出:
    • 一个 n 行 m 列的完整扫雷矩阵。对于每个位置:
      • 如果是雷(字符为 *),直接输出 *
      • 如果是空白(字符为 .),输出其周围八个相邻格子中雷的个数(即周围 * 的数量)。

关键概念

  • :用 * 表示的格子。
  • 邻格:与当前位置在八个方向(上、下、左、右、左上、右上、左下、右下)相邻的格子。
  • 完整扫雷矩阵:填充了每个非雷格子周围雷个数后的矩阵。

解题思路:

知道了题意之后,我们就得思考怎么解题了,根据题意,其实就如下几个关键点

  1. 遍历矩阵:对于矩阵中的每一个格子,都要进行判断。
  2. 雷的判断:如果当前格子是雷(*),直接保留 *
  3. 统计周围雷数:如果当前格子是空白(.),则检查其周围八个方向的格子,统计其中雷(*)的数量,然后将该数量作为当前格子的输出。

但是大家要注意第3点:如果当前格子是空白(.),则检查其周围八个方向的格子,统计其中雷(*)的数量,看起来是没什么,但是如果空白的那个格子是在边界呢?那它周围是没有8个格子的,比如这一个​,所以,我们需要注意边界的处理:

边界处理:需要注意矩阵边缘的格子,其周围可能不存在八个方向的格子(比如第一行的格子没有上方的邻格),此时只需统计存在的邻格中雷的数量。

继续解答:

所以,本题的难点在于,我们要怎么去找到一个元素周围8个元素,我们不妨挑选一个幸运儿,看一下它周围8个元素的坐标(以未知数表示代表性更强):

相对位置 坐标偏移(基于中心元素坐标 (i,j)) 说明
上方 (i−1,j) 中心元素正上方的格子
右上方 (i−1,j+1) 中心元素右上方的格子
右方 (i,j+1) 中心元素正右方的格子
右下方 (i+1,j+1) 中心元素右下方的格子
下方 (i+1,j) 中心元素正下方的格子
左下方 (i+1,j−1) 中心元素左下方的格子
左方 (i,j−1) 中心元素正左方的格子
左上方 (i−1,j−1) 中心元素左上方的格子

所以,我们知道了周围8个元素的坐标哦,我们就可以把某个元素的坐标传进计算周围有几个雷的函数中,再进行计算,对于这个,我也有两个方法

方法一:

第一种方法也是很直接粗暴,我们直接去遍历周围的所有八个元素,即如下:

int countMinesAround(int row, int col, char mine[][MAX_COLS], int rows, int cols) {
    int count = 0;
    // 定义周围八个方向的行和列偏移量
    int dr[] = {-1, -1, -1, 0, 0, 1, 1, 1};
    int dc[] = {-1, 0, 1, -1, 1, -1, 0, 1};
    for (int i = 0; i < 8; i++) {
        int newRow = row + dr[i];
        int newCol = col + dc[i];
        // 检查新坐标是否在矩阵范围内
        if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols) {
            // 如果是雷('*'),计数加 1
            if (mine[newRow][newCol] == '*') {
                count++;
            }
        }
    }
    return count;
}

这里函数的rows和cols参数,就是边界大小,用于判断是否越界,同时达到某个位置周围可能不存在八个方向的格子(比如第一行的格子没有上方的邻格),此时只需统计存在的邻格中雷的数量。

不过除此之外,我们也可以直接这么写:

int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
	return (mine[x - 1][y] + mine[x - 1][y - 1] + mine[x][y - 1] + mine[x + 1][y - 1] + mine[x + 1][y] +mine[x + 1][y + 1] + mine[x][y + 1] + mine[x - 1][y + 1] - 8 * '0');//将字符转变为数字
}

这种是直接强制检查某个元素的周围8个元素,没有考虑到某个位置周围可能不存在八个方向的格子,于是,我们就得对扫雷的数组扩大范围,但是又只存储原有的部分,比如要9*9的扫雷大小,我们就创建11*11的大小,这么一来,检查9*9的所有元素,都能实现对其周围8个格子的扫描了。

方法二:

上面一个方法是枚举法,而我们这个方法二则是循环法,我们借助循环进行遍历,因为我们观察某个元素的周围八个元素其实可以发现,无非就是对那一个元素的行坐标和列坐标进行+1、-1罢了,所以我们也可以借助循环来实现,我们这里也是使用双重循环,大家看来下面的代码就能理解了,同时增加判断条件避免扫描自己以及越界,具体代码如下:

// 计算周围地雷数量
int pailei(char arr[][1000], int x, int y, int n, int m) {
	int count = 0;
	// 检查8个方向,增加边界判断防止越界
	for (int dx = -1; dx <= 1; dx++) {
		for (int dy = -1; dy <= 1; dy++) {
			// 跳过自身位置
			if (dx == 0 && dy == 0) continue;

			int nx = x + dx;
			int ny = y + dy;

			// 检查是否在有效范围内
			if (nx >= 0 && nx < n && ny >= 0 && ny < m) {
				if (arr[nx][ny] == '*') {
					count++;
				}
			}
		}
	}
	return count;
}

下面是详细注释版的:

// 计算指定坐标周围地雷数量的函数
// 参数说明:
// - arr[][1000]:存储扫雷初始状态的二维数组,'*' 代表雷,'.' 代表空白
// - x, y:当前要判断的格子的行坐标和列坐标(从 0 开始计数)
// - n, m:扫雷矩阵的总行数和总列数
int pailei(char arr[][1000], int x, int y, int n, int m) {
    // 用于统计当前格子周围雷的数量,初始化为 0
    int count = 0;

    // 外层循环控制行方向的偏移量,从 -1 到 1,覆盖当前格子的上、中、下三行
    for (int dx = -1; dx <= 1; dx++) {
        // 内层循环控制列方向的偏移量,从 -1 到 1,覆盖当前格子的左、中、右三列
        for (int dy = -1; dy <= 1; dy++) {
            // 当行偏移和列偏移都为 0 时,对应的是当前格子自身,不需要统计,直接跳过
            if (dx == 0 && dy == 0) {
                continue;
            }

            // 计算周围格子相对于当前格子的新行坐标 nx
            int nx = x + dx;
            // 计算周围格子相对于当前格子的新列坐标 ny
            int ny = y + dy;

            // 检查新坐标 nx 是否在合法的行范围内(0 到 n - 1 之间)因为数组是从0开始存储
            // 同时检查新坐标 ny 是否在合法的列范围内(0 到 m - 1 之间)因为数组是从0开始存储
            if (nx >= 0 && nx < n && ny >= 0 && ny < m) {
                // 如果周围某个格子是雷(即该位置的字符为 '*')
                if (arr[nx][ny] == '*') {
                    // 则将雷的数量计数器加 1
                    count++;
                }
            }
        }
    }

    // 返回当前格子周围雷的总数
    return count;
}

再给大家看一下这个代码运行的例子,加深大家理解:

假设我们有一个 3x3 的扫雷矩阵:

* . *
. * .
* . .

用数组表示为(行索引 0-2,列索引 0-2):

char arr[3][1000] = {
    {'*', '.', '*'},
    {'.', '*', '.'},
    {'*', '.', '.'}
};

 

示例 1:计算坐标 (0,1) 周围的地雷数量

调用函数:pailei(arr, 0, 1, 3, 3)

计算过程:

  • 当前坐标 (0,1) 是空白格('.')
  • 遍历 8 个方向:
    • (0-1,1-1)=(-1,0) → 越界(跳过)
    • (0-1,1)=(-1,1) → 越界(跳过)
    • (0-1,1+1)=(-1,2) → 越界(跳过)(判断该元素左边一列)
    • (0,1-1)=(0,0) → 是 '*' → count=1
    • (0,1+1)=(0,2) → 是 '*' → count=2 (判断该元素上下)
    • (0+1,1-1)=(1,0) → 是 '.'(不变)
    • (0+1,1)=(1,1) → 是 '*' → count=3
    • (0+1,1+1)=(1,2) → 是 '.'(不变)(判断该元素右边一列)

返回结果:3

示例 2:计算坐标 (1,1) 周围的地雷数量

调用函数:pailei(arr, 1, 1, 3, 3)

计算过程:

  • 当前坐标 (1,1) 是地雷('*'),但函数仍会计算周围地雷
  • 遍历 8 个方向:
    • (0,0) → '*' → count=1
    • (0,1) → '.'(不变)
    • (0,2) → '*' → count=2
    • (1,0) → '.'(不变)
    • (1,2) → '.'(不变)
    • (2,0) → '*' → count=3
    • (2,1) → '.'(不变)
    • (2,2) → '.'(不变)

返回结果:3

示例 3:计算坐标 (2,2) 周围的地雷数量

调用函数:pailei(arr, 2, 2, 3, 3)

计算过程:

  • 遍历 8 个方向:
    • (1,1) → '*' → count=1
    • (1,2) → '.'(不变)
    • (2,1) → '.'(不变)
    • 其他方向均越界(跳过)

返回结果:1

总代码:

经过了上面的分析,我们知道了如何实现计算某个元素周围有几个雷的函数代码,至于主函数,也是不难,我们直接遍历二维数组的所有元素,如果是雷,就直接表达雷,如果不是雷,就表达其周围有几个雷,于是,得出本题的完整代码,也就不难了,如下:

// 计算周围地雷数量
int pailei(char arr[][1000], int x, int y, int n, int m) {
	int count = 0;
	// 检查8个方向,增加边界判断防止越界
	for (int dx = -1; dx <= 1; dx++) {
		for (int dy = -1; dy <= 1; dy++) {
			// 跳过自身位置
			if (dx == 0 && dy == 0) continue;

			int nx = x + dx;
			int ny = y + dy;

			// 检查是否在有效范围内
			if (nx >= 0 && nx < n && ny >= 0 && ny < m) {
				if (arr[nx][ny] == '*') {
					count++;
				}
			}
		}
	}
	return count;
}


int main()
{
	int n, m;
	scanf("%d%d",&n,&m);
	//getchar();//由于后面需要输入字符数组,所以要吸收回车
	char arr[1000][1000] = { 0 };
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < m; j++)
		{
			scanf(" %c", &arr[i][j]);
		}
	}
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < m; j++)
		{
			if (arr[i][j] == '*')
			{
				printf("*");
			}
			else if (arr[i][j] == '.')
			{
				printf("%d",pailei(arr,i,j,n,m));
			}
		}
		printf("\n");
	}
	return 0;
}

再给上一版详细注释:

#include <stdio.h>

/*
 * 函数功能:计算指定位置周围8个方向的地雷数量
 * 参数说明:
 *   arr[][1000] - 存储扫雷地图的二维数组,其中'*'表示地雷,'.'表示空白区域
 *   x           - 当前要检查的格子的行坐标(从0开始计数)
 *   y           - 当前要检查的格子的列坐标(从0开始计数)
 *   n           - 扫雷地图的总行数
 *   m           - 扫雷地图的总列数
 * 返回值:当前格子周围8个方向的地雷总数
 */
int pailei(char arr[][1000], int x, int y, int n, int m) {
    // 初始化计数器,用于统计周围地雷的数量
    int count = 0;

    /*
     * 使用双层循环遍历当前格子周围的8个方向
     * dx表示行方向的偏移量:-1(上一行)、0(当前行)、1(下一行)
     * dy表示列方向的偏移量:-1(前一列)、0(当前列)、1(后一列)
     * 组合起来共9种情况,后续会排除自身位置
     */
    for (int dx = -1; dx <= 1; dx++) {
        for (int dy = -1; dy <= 1; dy++) {
            /*
             * 当dx和dy都为0时,计算出的是当前格子自身的坐标
             * 不需要统计自身位置,所以使用continue跳过
             */
            if (dx == 0 && dy == 0) {
                continue;
            }

            // 计算周围格子的实际行坐标:当前行坐标 + 行偏移量
            int nx = x + dx;
            // 计算周围格子的实际列坐标:当前列坐标 + 列偏移量
            int ny = y + dy;

            /*
             * 检查计算出的周围格子坐标是否在合法范围内:
             *   行坐标nx必须 >= 0 且 < n(因为行从0开始计数,最大行索引为n-1)
             *   列坐标ny必须 >= 0 且 < m(因为列从0开始计数,最大列索引为m-1)
             * 防止数组越界访问(访问不存在的数组元素会导致程序错误)
             */
            if (nx >= 0 && nx < n && ny >= 0 && ny < m) {
                // 如果周围格子是地雷(即字符为'*'),计数器加1
                if (arr[nx][ny] == '*') {
                    count++;
                }
            }
        }
    }

    // 返回统计到的周围地雷总数
    return count;
}

/*
 * 主函数:程序入口点,负责接收输入、处理数据和输出结果
 */
int main() {
    // 定义变量n和m,分别存储扫雷地图的行数和列数
    int n, m;

    // 从标准输入读取行数n和列数m(格式如"3 3"表示3行3列的地图)
    scanf("%d%d", &n, &m);

    /*
     * 定义二维数组arr存储扫雷地图,大小为1000x1000
     * 初始化为全0(即空字符),足够存储题目要求的最大尺寸地图
     */
    char arr[1000][1000] = {0};

    /*
     * 循环读取扫雷地图的每一个格子:
     *   外层循环变量i表示行索引,从0到n-1(共n行)
     *   内层循环变量j表示列索引,从0到m-1(共m列)
     */
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            /*
             * 读取一个字符到arr[i][j]:
             *   格式字符串中的空格" "用于跳过输入中的空白字符(如换行符、空格)
             *   确保正确读取每个格子的内容('*'或'.')
             */
            scanf(" %c", &arr[i][j]);
        }
    }

    /*
     * 循环处理每个格子并输出结果:
     *   外层循环变量i表示行索引,从0到n-1
     *   内层循环变量j表示列索引,从0到m-1
     */
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            // 如果当前格子是地雷('*'),直接输出'*'
            if (arr[i][j] == '*') {
                printf("*");
            }
            // 如果当前格子是空白区域('.'),计算并输出周围地雷数量
            else if (arr[i][j] == '.') {
                // 调用pailei函数获取周围地雷数量,并输出该数字
                printf("%d", pailei(arr, i, j, n, m));
            }
        }
        // 每行所有格子处理完毕后,输出一个换行符,确保结果按行显示
        printf("\n");
    }

    // 主函数返回0,表示程序正常结束
    return 0;
}

即如上

结语:

到这里,牛客网二维数组模块中 BC141 井字棋与 BC142 扫雷这两道核心题目就已经拆解完毕了。这两道题看似是 “简化版小游戏”,实则是对二维数组操作逻辑的集中考察 —— 无论是井字棋中 “行、列、对角线” 的多维度判断,还是扫雷中 “邻域遍历与边界处理” 的细节把控,都是编程入门阶段必须吃透的核心能力。

回顾解题过程,我们能发现一个共性逻辑:面对二维数组相关问题,“找准规律” 和 “规避边界风险” 永远是关键。比如井字棋中,我们从 “固定坐标硬判断” 的方法一,过渡到 “通用遍历逻辑” 的方法二,本质是学会了用 “变量偏移” 替代 “手动枚举”,这让代码从 “只适用于 3x3” 升级为 “可扩展到五子棋、十子棋”;而扫雷中,无论是 “枚举 8 个方向偏移量” 还是 “双层循环遍历邻域”,核心都是通过 “坐标合法性判断” 避免数组越界,这也是所有矩阵类题目必须坚守的底线。

对初学者来说,这两道题的价值不止于 “做出题目”,更在于 “培养编程思维”:当我们面对一个问题时,先从最简单的场景入手(比如 3x3 井字棋),再思考如何抽象出通用逻辑(比如用循环替代固定坐标),最后考虑边界情况(比如矩阵边缘格子的邻域缺失)—— 这个思考路径,能帮我们应对更多复杂的二维数组问题,比如后续可能遇到的 “矩阵旋转”“图像模糊处理” 等。

当然,代码没有 “最优解”,只有 “更适配场景的解”。比如井字棋的 goto 语句虽简洁,但在大型项目中需谨慎使用;扫雷的 “直接遍历邻域” 虽直观,在超大矩阵(如 10000x10000)下,二维前缀和的效率会更优。后续大家可以尝试优化这些代码,比如给井字棋加上 “落子功能”,给扫雷加上 “展开空白区域” 的逻辑,逐步向完整小游戏靠近。

最后,二维数组作为 “多维数据存储” 的入门载体,其考察的逻辑思维会贯穿整个编程学习过程。希望大家通过这两道题的拆解,不仅能掌握具体题目的解法,更能形成 “分析规律→抽象逻辑→处理细节” 的解题思路,为后续更复杂的算法学习打下基础。下一个模块,我们将继续攻克更多编程入门阶段的经典题目,保持思考,持续进步,咱们下篇博客见!

 


网站公告

今日签到

点亮在社区的每一天
去签到