开篇介绍:
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
),持有该棋子的玩家就获胜。
通过这一句,我们便可知道,可是具体要怎么做呢?
要解决这个问题,我们需要检查所有可能的获胜情况,包括:
- 行检查:遍历每一行,看是否有一行的三个元素都为
K
或者都为B
。 - 列检查:遍历每一列,看是否有一列的三个元素都为
K
或者都为B
。 - 对角线检查:检查两条对角线(从左上到右下、从右上到左下),看是否有一条对角线上的三个元素都为
K
或者都为B
。(这一点需要注意,有两条对角线)
如果在上述任何一种检查中,发现有玩家的三个棋子连成一线,就可以判定该玩家获胜;如果所有检查都完成后,没有玩家满足获胜条件,就判定为没有赢家。
知道了如何判定获胜者,那么接下来我们就得解决如何用代码实现这一目的的问题了,在这里,我给大家提供两个方法。
方法一:
这个方法,就是最简单粗暴的一个方法,因为题目中已经指明了是3*3大小的棋盘,那最直接的方法,便是我们去把每行每列以及两条对角线进行逐个判断即可,因为我们确定了大小,那我们肯定也就能知道棋盘中每个旗子的坐标,我们便可以直接去进行逐个检查,第一行、第二行、第三行、第一列、第二列、第三列、两条对角线,去分别对元素进行判定(借助坐标),详细步骤如下:
- 输入棋盘数据:使用嵌套循环,读取三行三列的字符,这些字符代表棋盘状态,其中
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列。 - 检查对角线获胜情况(注意我们是从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
,确定获胜方。
- 检查主对角线(从左上到右下):判断
- 检查行和列获胜情况
- 检查每一行:遍历每一行,判断该行的三个元素是否相同。若相同,再看是
K
还是B
,确定获胜方。 - 检查每一列:遍历每一列,判断该列的三个元素是否相同。若相同,再看是
K
还是B
,确定获胜方。
- 检查每一行:遍历每一行,判断该行的三个元素是否相同。若相同,再看是
- 判断无获胜情况:如果经过上述所有对角线、行、列的检查,都没有出现三个相同的
K
或B
,则输出"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 列的完整扫雷矩阵。对于每个位置:
- 如果是雷(字符为
*
),直接输出*
。 - 如果是空白(字符为
.
),输出其周围八个相邻格子中雷的个数(即周围*
的数量)。
- 如果是雷(字符为
- 一个 n 行 m 列的完整扫雷矩阵。对于每个位置:
关键概念
- 雷:用
*
表示的格子。 - 邻格:与当前位置在八个方向(上、下、左、右、左上、右上、左下、右下)相邻的格子。
- 完整扫雷矩阵:填充了每个非雷格子周围雷个数后的矩阵。
解题思路:
知道了题意之后,我们就得思考怎么解题了,根据题意,其实就如下几个关键点
- 遍历矩阵:对于矩阵中的每一个格子,都要进行判断。
- 雷的判断:如果当前格子是雷(
*
),直接保留*
。 - 统计周围雷数:如果当前格子是空白(
.
),则检查其周围八个方向的格子,统计其中雷(*
)的数量,然后将该数量作为当前格子的输出。
但是大家要注意第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)下,二维前缀和的效率会更优。后续大家可以尝试优化这些代码,比如给井字棋加上 “落子功能”,给扫雷加上 “展开空白区域” 的逻辑,逐步向完整小游戏靠近。
最后,二维数组作为 “多维数据存储” 的入门载体,其考察的逻辑思维会贯穿整个编程学习过程。希望大家通过这两道题的拆解,不仅能掌握具体题目的解法,更能形成 “分析规律→抽象逻辑→处理细节” 的解题思路,为后续更复杂的算法学习打下基础。下一个模块,我们将继续攻克更多编程入门阶段的经典题目,保持思考,持续进步,咱们下篇博客见!