第 36 课:二维数组模拟
二维数组模拟是通过二维数组模拟现实或抽象的二维场景,如矩阵、棋盘、网格地图等,其核心是用行列索引 (i, j) 对应场景中的位置,并通过数组元素存储状态(如数值、访问标记、属性等)。
36.1 核心考查
1) 索引与边界
a. 明确数组的下标起点(0 或 1),避免越界(比如当 n = 3 时,行尾初始为 3,收缩到 2 → 1)。
b. 边界判断(如外围元素的 i == 1)、螺旋的边界收缩(最后一列或最后一行收缩后,下一轮循环不包含已填充列和行)。
2) 遍历策略
a. 行优先/列优先遍历(稀疏矩阵、矩阵检测)。
b. 定向遍历(螺旋的四方向)、条件过滤(仅处理非零元素、边缘元素)。
3) 状态记录与压缩
稀疏矩阵的三元组压缩、矩阵检测的奇偶性标记。
4) 模拟复杂场景
螺旋填充的方向切换、外围元素的去重累加、奇偶性匹配的逻辑推理。
36.2 实例讲解
例 1:收集露珠
有一只小青蛙在一片荷叶矩阵上玩耍,每片荷叶上都凝结着露珠。
妈妈告诉它:"池塘最外围的荷叶(第一行、最后一行、第一列、最后一列)上的露珠最甜,但要注意角落的荷叶只能算一次哦!"小青蛙需要计算这些外围荷叶的露珠总和,证明自己是"露珠收集小能手"。
这个池塘是一个 m 行 n 列的网格,每片荷叶上的露珠数是整数。小青蛙经过努力,最终收集到了所有的最甜露珠,问小青蛙一共收集了多少露珠呢?
【输入格式】输入 m+1 行。
第 1 行为两个正整数,分别表示池塘的行数 m 和列数 n,中间以一个空格隔开。
接下来的 m 行为该矩阵 m 行 n 列的露珠数量,都为整数,每行相邻元素之间以一个空格隔开。
【输出格式】输出一行,一个整数,表示小青蛙收集的所有最甜露珠之和。
【输入样例】
3 4
1 2 2 1
5 6 7 8
9 3 0 5【输出样例】
36【数据范围】1 ≤ m ≤ 100,1 ≤ n ≤ 100,每片荷叶上的露珠数量不超过 100。
解析
本题其实是要求在一个 m 行 n 列矩阵中,位于第一行、最后一行、第一列和最后一列的所有元素之和,也就是矩阵外围元素的总和。
可以通过二维数组来存储矩阵元素,在输入每个元素时,判断其是否位于矩阵的外围(第一行、最后一行、第一列、最后一列),如果是则将其累加到总和中,最后输出总和即可,主要步骤如下。
- 判断当前元素是否位于矩阵的外围。如果 i 等于 1(第一行)或者 i 等于 m(最后一行),或者 j 等于 1(第一列)或者 j 等于 n(最后一列),则说明该元素位于矩阵的外围,即:
if (i == 1 || i == m || j == 1 || j == n)- 如果当前元素位于矩阵的外围,则将其值累加到 sum 中,即:
sum += a[i][j];参考代码
#include <iostream>
using namespace std;
int a[110][110];
int main()
{
int m, n, sum = 0;
cin >> m >> n;
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
cin >> a[i][j]; // 输入元素后即可判断
if (i == 1 || i == m || j == 1 || j == n)
sum += a[i][j]; // 满足任一条件求和
}
}
cout << sum;
return 0;
}例 2:稀疏矩阵
大部分元素是 0 的矩阵称为稀疏矩阵。假设有 n 个非零元素,则可把稀疏矩阵用 n × 3 的矩阵简记下来,其中第一列是行号,第二列是列号,第三列是该行、该列下的非零元素的值。
如:
0 0 0 3
0 2 0 0
0 5 0 0简记成:
1 4 3 表示第 1 行第 4 列有个数是 3。
2 2 2 表示第 2 行第 2 列有个数是 2。
3 2 5 表示第 3 行第 2 列有个数是 5。试编程读入一稀疏矩阵,转换成简记形式,并输出。
【输入格式】输入 a + 1 行。
第 1 行输入两个正整数,表示原始矩阵的行数 a 和列数 b。
之后的 a 行为 a 行 b 列的矩阵元素。
【输出格式】输出为化为简记形式之后的矩阵(行数不确定,列数为 3)。
【输入样例】
3 4
0 0 0 3
0 2 0 0
0 5 0 0【输出样例】
1 4 3
2 2 2
3 2 5【数据范围】
原始矩阵的行数 a 和列数 b 满足:1 ≤ a ≤ 20,1 ≤ b ≤ 20。
对于存储非零元素的简记矩阵,其行数 n 满足:1 ≤ n ≤ 10000。
解析
题目要求读取一个稀疏矩阵,将其转换为简记形式,然后输出该矩阵的简记形式。简记形式为一个 n × 3 的矩阵,这里的 n 代表非零元素的数量,第一列是行号,第二列是列号,第三列是该位置的非零元素的值。
首先读取原始矩阵,我们可以运用两层嵌套的 for 循环来遍历原始矩阵的每一个元素并存储到 a 数组中。
然后判断当前元素是否为非零元素,若为非零元素,则将其行号存储到 b 数组第一列中,列号存储到 b 数组第二列中,元素值存储到 b 数组第三列中。
最后再输出矩阵的简记形式,可以利用一个 for 循环遍历 b 数组的每一行,按顺序输出 b 数组每一行的第一、二、三列,各列用空格分隔,每行结束后换行。
参考代码
#include <iostream>
using namespace std;
int a[110][110], b[10010][4];
int main()
{
int m, n, t = 1;
cin >> m >> n;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
{
cin >> a[i][j];
if (a[i][j] != 0)
{
b[t][1] = i; // 把非零元素的行号保存到新数组第一列
b[t][2] = j; // 把列号保存在新数组第二列
b[t++][3] = a[i][j]; // 把元素值保存在新数组第三列,且新数组的行号加 1
}
}
for (int i = 1; i < t; i++) // 按顺序输出新数组每一行的第一、二、三列
cout << b[i][1] << " " << b[i][2] << " " << b[i][3] << endl;
return 0;
}例 3:矩阵检测
给定由 0 和 1 组成的 n × n 矩阵,如果矩阵的每一行和每一列中 1 的数量都是偶数,则认为符合条件。
你的任务就是检测矩阵是否符合条件,或者在仅改变一个矩阵元素的情况下能否符合条件。"改变矩阵元素"的操作定义为 0 变成 1 或者 1 变成 0。
【输入格式】输入 n + 1 行。
第 1 行输入一个正整数 n,表示矩阵的行数和列数。
之后的 n 行为 n 行 n 列的矩阵元素。
【输出格式】输出一行。
如果矩阵符合条件,则输出 yes!。
如果矩阵仅改变一个矩阵元素就能符合条件,则输出需要改变的元素所在的行号和列号,以一个空格分开。
如果不符合以上两条,输出 no!。
【输入样例】
4
1 0 1 0
0 0 1 0
1 1 1 1
0 1 0 1【输出样例】
2 3【数据范围】1 ≤ n ≤ 20。
解析
样例分析
输入 4 行 4 列的元素,如表 8-1 所示。
表 8-1 输入 4 行 4 列的元素
| 1 | 0 | 1 | 0 |
|---|---|---|---|
| 0 | 0 | 1 | 0 |
| 1 | 1 | 1 | 1 |
| 0 | 1 | 0 | 1 |
先看行,第 1 行有 2 个 1,1 的数量为偶数,满足条件;第 2 行有 1 个 1,1 的数量为奇数,不满足条件;第 3 行和第 4 行,1 的数量分别为 4 个和 2 个,均为偶数,满足条件。
再看列,第 1 列和第 2 列都有 2 个 1,1 的数量为偶数,满足条件;第 3 列有 3 个 1,1 的数量为奇数,不满足条件;第 4 列有 2 个 1,1 的数量为偶数,满足条件。
总结:根据样例,我们发现第 2 行和第 3 列不满足条件,仅有一行一列不满足,将第 2 行第 3 列的数字 1 变成 0 既能满足行上 1 的数量均为偶数,也能满足列上 1 的数量均为偶数,如表 8-2 所示。
表 8-2 改变第 2 行第 3 列的数字可满足要求
| 1 | 0 | 1 | 0 |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 1 |
| 0 | 1 | 0 | 1 |
统计奇数行和奇数列的数量,若没有,直接输出 yes!。
如果奇数行和奇数列的数量都为 1,则改变它们的交点处的元素即可满足条件。
若奇数行或奇数列的数量大于 1,则无法通过改变一个元素来满足条件,输出 no!。
参考代码
#include <iostream>
using namespace std;
int main()
{
int m, t[21][21], h, l, hs = 0, ls = 0, a = 0, b = 0;
cin >> m;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= m; j++)
cin >> t[i][j];
for (int i = 1; i <= m; i++)
{
a = 0; // 每行找 1,到下一行清零
for (int j = 1; j <= m; j++)
if (t[i][j] == 1)
a++;
if (a % 2 == 1) // 如果 1 的数量是奇数
{
hs++; // 行数增加
h = i; // 保存行下标
}
}
for (int i = 1; i <= m; i++)
{
b = 0; // 每列找 1,到下一列清零
for (int j = 1; j <= m; j++)
if (t[j][i] == 1) // 按列查找元素
b++;
if (b % 2 == 1)
{
ls++; // 列数增加
l = i; // 保存列下标
}
}
if (ls > 1 || hs > 1) // 行数或者列数大于 1 就不满足
cout << "no!";
else if (hs == 0 && ls == 0) // 都没有就满足
cout << "yes!";
else // 修改保存的行下标和列下标位置元素后满足条件
cout << h << " " << l;
return 0;
}例 4:蛙蛙的惩罚
蛙蛙上课哼着"爱的魔力转圈圈"被老师听见,老师为了"惩罚"他,出了一道与转圈圈相关的题目,需要大家帮忙解决。
题目是要构建一个 n × n 的二维数组,然后按照顺时针转圈的方式,从数组的最右上角开始,依次填入数字 1、2、3、4……直至填满整个数组。
最后,将这个数组中的所有数字以三个位宽、左对齐的格式输出。
这就好像是数字在数组这个"大舞台"上顺时针"转圈圈",每个数字都有其固定的位置和顺序,而我们的任务就是找到这个顺序并按照要求输出结果。
【输入格式】输入一行,一个正整数 n,表示二维数组的行数和列数。
【输出格式】输出 n 行,顺时针转圈输出二维数组内的所有元素,从 1 开始,到 n × n 结束。
【输入样例】
3【输出样例】
7 8 1
6 9 2
5 4 3【数据范围】1 ≤ n ≤ 20。
对齐方式
左对齐:cout << left << setw(n) << a;
left 操作符的作用是设定对齐方式为左对齐,也就是说,后续输出的数据会在指定的字段宽度内靠左排列。
setw(n) 操作符用于设置下一个输出项的宽度为 n,如果输出项的实际宽度小于 n,那么会按照指定的对齐方式(这里是左对齐)来填充剩余的空间。
右对齐:cout << setw(n) << a;
setw(n) 操作符同样是设置下一个输出项的宽度为 n,如果输出项的实际宽度小于 n,默认会采用右对齐的方式来填充剩余的空间。
无论是左对齐还是右对齐,都需要使用 iomanip 头文件,即:#include <iomanip>。
解析
从右上角开始,从 1 开始数数,所以这个数字需要放入一个变量中,在循环中不停地重复自加。
在数组中走来走去,其实只有四种走法,分别是:
a. 从行头到行尾(向下)
b. 从列尾到列头(向左)
c. 从行尾到行头(向上)
d. 从列头到列尾(向右)
还要注意的是走完一行或者一列,行和列的数量要减少 1。
参考代码
#include <iostream>
#include <iomanip>
using namespace std;
int a[21][21];
int main()
{
int n;
cin >> n;
int t = 1; // 定义初始值为 1 的变量 t,用于计数
int ht = 1, hw = n, lt = 1, lw = n; // 行头 1,行尾 n,列头 1,列尾 n
while (t <= n * n)
{
for (int i = ht; i <= hw; i++)
a[i][lw] = t++; // 从行头到行尾,在最后一列上行走
lw--; // 列尾减 1(少一列)
for (int i = lw; i >= lt; i--)
a[hw][i] = t++;
hw--; // 行尾减 1(少一行)
for (int i = hw; i >= ht; i--)
a[i][lt] = t++;
lt++; // 列头加 1(少一列)
for (int i = lt; i <= lw; i++)
a[ht][i] = t++;
ht++; // 行头加 1(少一行)
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
cout << left << setw(3) << a[i][j] << " ";
cout << endl;
}
return 0;
}