简介:包含23个独立.cpp文件,全部聚焦二维数组在C++中的实际应用,每个文件都经过验证可直接g++编译运行,无需额外依赖。题目覆盖基础操作:行列遍历、对角线处理、求和统计、乘法表输出;也涵盖信息学常见模型:鞍点判断(先找行最大再验列最小)、两种写法的杨辉三角、顺时针/逆时针蛇形矩阵填充与输出、稀疏矩阵识别、铺地毯坐标模拟、图像像素相似度比对、回文数二维判定、去重逻辑(含双版本对比)、内存连续性理解示例。所有代码采用标准输入输出,变量命名清晰,关键步骤有简明注释,嵌套循环结构分步展开,便于初学者掌握索引规律和边界控制。数组规模统一控制在6×6左右,兼顾理解效率与算法完整性,适合日常训练、课堂练习或竞赛前快速巩固。
1. 为什么这23道题值得你花一整个下午认真敲一遍
我带过三届信息学竞赛集训班,每年开课第一周,总要花两节课专门讲二维数组——不是讲语法,而是讲“怎么不写错下标”。你可能已经背熟了a[i][j]的含义,但真正动手时,90%的初学者会在第3个嵌套循环里把i和j写反,在第5次边界判断时漏掉-1,在第7次对角线遍历时搞混i+j==n-1还是i+j==n。这不是粗心,是空间索引直觉还没建立起来。
这套23题,就是专治这种“知道但写不对”的顽疾。它不堆砌算法复杂度,不追求O(1)优化,而是用最朴素的方式,把二维数组的“肌肉记忆”刻进你的手指里。比如“鞍点.cpp”,它没直接给你一个函数模板,而是先让你手动遍历每一行找最大值,再回过头去验证该列是否最小——这个“分两步走”的设计,强迫你拆解逻辑,看清“行视角”和“列视角”的切换本质;再比如“蛇形距阵.cpp”和“蛇形距阵输出.cpp”两个文件并存,前者专注填充逻辑(从外向内逐层填数字),后者专注输出格式(控制空格对齐),这种刻意分离,就是在训练你把“数据组织”和“结果呈现”当成两个独立模块来思考。
所有题目统一采用6×6小规模数组,不是因为简单,而是因为精准:6×6足够暴露所有边界问题(首行、末行、主对角线、副对角线、中心点),又不会因数据量大而掩盖逻辑错误;它像一面放大镜,把你在for(int i=0; i<n; i++)里少写的那个等号,原原本本照出来。我试过让学员只看代码不运行,然后口头描述“铺地毯.cpp”里坐标重叠判断的完整流程,80%的人卡在“地毯左上角(x1,y1)覆盖到右下角(x2,y2),那么点(p,q)被覆盖的充要条件是什么”这一句上——而当你亲手敲完并调试通过,这个条件就不再是抽象符号,而是你键盘上敲出的p>=x1 && p<=x2 && q>=y1 && q<=y2。
关键词里的“鞍点”“杨辉三角”“蛇形矩阵”,表面是算法名词,实则是三维空间思维的三个支点:鞍点训练你做二维极值的交叉验证,杨辉三角训练你理解递推关系在平面上的传播路径,蛇形矩阵训练你把方向变化(右→下→左→上)转化为坐标增量(dx,dy)的精确控制。这23个文件,就是23块垫脚石,帮你从“能看懂代码”跃迁到“能预判下标”。
2. 题目整体设计思路与教学逻辑拆解
2.1 从“内存连续性”切入,重建二维数组底层认知
很多初学者把int a[6][6]想象成一张纸上的表格,这是对的,但不完整。C++中二维数组在内存里是严格一维连续排列的,a[0][0]到a[0][5]占前6个字节,紧接着是a[1][0]到a[1][5],以此类推。这个事实决定了:a[i][j]的地址 = &a[0][0] + i*6 + j(假设每行6列)。题目中的“数组空间连续.cpp”正是为此而设——它不让你操作数据,而是打印每个元素的地址:
for(int i=0; i<6; i++) {
for(int j=0; j<6; j++) {
printf("a[%d][%d] address: %p\n", i, j, &a[i][j]);
}
}
运行后你会看到地址以固定步长(如4字节)递增,且每行结束后的下一个地址,恰好是下一行开头。这个视觉冲击比任何文字解释都管用。它直接解释了为什么“按行输出”比“按列输出”更高效(缓存局部性),也解释了为什么a[i][j]不能写成a[j][i]——虽然数学上都是矩阵转置,但内存访问模式天差地别。我在课堂上常让学生对比运行“二维数组按行列输出.cpp”和“输出同行同列.cpp”,后者故意用列优先顺序遍历,用time ./a.out测耗时,差距虽小,但足以建立性能敏感意识。
2.2 分层递进:从单点操作到多维联动
23题不是随机堆砌,而是按认知负荷严格分层:
-
Level 1:单维切片(1-5题)
“求和.cpp”“乘法表.cpp”“对角线元素加十.cpp”属于此层。它们只涉及单一维度的操作:要么遍历整行求和,要么固定行号输出乘法表,要么沿主对角线i==j修改元素。重点训练for(j=0;j<6;j++)这类单层循环的稳定性,确保你能闭着眼写出a[i][j] += 10而不手抖。 -
Level 2:双维耦合(6-12题)
“鞍点.cpp”“稀疏矩阵.cpp”“图像相似度.cpp”在此层。此时必须同时考虑行和列的约束。例如鞍点判定:先用for(j=0;j<6;j++) if(a[i][j]>max_val)找行最大,再用for(k=0;k<6;k++) if(a[k][col]!=min_val)验列最小——这里col是上一步记录的列号,k是新引入的行变量。变量命名max_val/col/min_val的清晰性,就是为了让你一眼看出数据流:行扫描→暂存列号→列扫描→交叉验证。 -
Level 3:空间建模(13-23题)
“铺地毯.cpp”“蛇形距阵.cpp”“杨辉三角.cpp”代表最高层。它们要求你把现实问题映射为二维坐标系。铺地毯本质是矩形区域覆盖判断,需将“左上角(x1,y1)、右下角(x2,y2)”转化为四条边界线;蛇形矩阵是方向状态机(当前向右?向下?),需维护dx,dy和转向条件;杨辉三角则需理解C(n,k)=C(n-1,k-1)+C(n-1,k)在二维网格中的位置对应关系——第n行第k列由上方和左上方共同决定。这些题目的注释里,我特意保留了原始草稿式的伪代码,比如蛇形矩阵填充前写着:“// step1: 填顶行 [0][0]→[0][5] // step2: 填右列 [1][5]→[5][5]”,这就是在强制你用自然语言先描述空间动作,再翻译为代码。
2.3 双版本对照:暴露思维盲区的“显影液”
最精妙的设计在于“双版本题”:
- “杨辉三角.cpp” vs “杨辉三角123.cpp”:前者用二维数组存储全部数值,后者用滚动数组(仅存上一行),空间复杂度从O(n²)降到O(n)。对比二者,你能直观看到“空间换时间”的具体代价——滚动数组省了内存,但代码里多了prev[j]和curr[j]的切换逻辑,可读性下降。
- “去重.cpp” vs “去重2.cpp”:前者用bool visited[36]标记已出现数字,后者用set<int>自动去重。前者教你手动管理状态,后者教你调用STL,但set的插入是O(log n),而visited数组是O(1),当数据范围已知(1-36)时,数组方案更优。
- “图像相似.cpp” vs “图像相似度.cpp”:前者计算像素绝对差值之和,后者引入均方误差(MSE)公式sum((a[i][j]-b[i][j])²)/36。前者易懂,后者更符合图像处理惯例,但平方运算可能溢出,需用long long承接——这就是工程权衡:精度vs安全。
这些双版本不是炫技,而是把“为什么选A不选B”这个黑箱打开,让你看见决策背后的计算成本、内存压力、数值稳定性等真实因素。
3. 核心题目深度解析与实操要点
3.1 鞍点检测:交叉验证的范式构建
鞍点定义:某元素既是其所在行的最大值,又是其所在列的最小值。难点不在找最大最小,而在验证的时机与范围。
原始代码中常见错误:
- 错误1:找到行最大后立即验证该列,但未重置列最小值,导致用上一行的min_val去比当前行;
- 错误2:验证列最小值时,循环从k=0到k<6,却忘了排除当前行(即k!=i),导致把自己也算进去了;
- 错误3:用if(a[i][j]==max_val && a[i][j]==min_val)双重判断,但max_val和min_val是不同循环算出的,精度或类型可能不一致。
正确解法分三步走(见“二维数组计算鞍点.cpp”):
1. 行扫描存候选:遍历每行i,找该行最大值max_val及列号col,存入candidate[i] = {max_val, col};
2. 列验证预处理:对每个候选列号col,单独计算该列全局最小值col_min[col](一次遍历,避免重复计算);
3. 交叉匹配:对每个i,检查candidate[i].first == col_min[candidate[i].second]。
关键参数计算:col_min[col]初始化为INT_MAX,更新时col_min[col] = min(col_min[col], a[k][col])。这里min()函数来自<algorithm>,但初学者可用三目运算符col_min[col] = (a[k][col] < col_min[col]) ? a[k][col] : col_min[col],更透明。
实操心得:我在调试时,会临时添加打印语句:
printf("Row %d max at col %d: %d\n", i, col, max_val);
printf("Col %d global min: %d\n", col, col_min[col]);
运行后立刻能看出哪一行的max_val和col_min[col]不等——这往往指向输入数据本身无鞍点,而非代码错误。
3.2 杨辉三角生成:递推关系的空间落地
杨辉三角本质是组合数C(n,k)的二维展示,核心递推式:C(n,k) = C(n-1,k-1) + C(n-1,k)。难点是如何把数学公式映射到a[i][j]的索引上。
两种实现方式对比:
- 方式一(二维数组全存,杨辉三角.cpp):
a[0][0] = 1;
for(i=1; i<6; i++) {
a[i][0] = 1; a[i][i] = 1; // 边界
for(j=1; j<i; j++) {
a[i][j] = a[i-1][j-1] + a[i-1][j]; // 关键:上一行的j-1和j列
}
}
这里j<i的边界至关重要:第i行只有i+1个元素(索引0到i),所以j最大取i-1,否则a[i-1][j]越界。
- 方式二(滚动数组,杨辉三角123.cpp):
vector<int> prev(6,0), curr(6,0);
prev[0] = 1;
for(i=1; i<6; i++) {
curr[0] = 1; curr[i] = 1;
for(j=1; j<i; j++) {
curr[j] = prev[j-1] + prev[j]; // prev是上一行
}
prev = curr; // 滚动
}
注意事项:滚动数组必须用prev = curr赋值,不能用指针交换(swap(prev, curr)会导致后续prev[j-1]读错)。另外,curr[i] = 1必须在内层循环后执行,否则j=i时会覆盖。
生活化类比:可以把二维数组想象成6本并排的练习册,每本有6页;方式一就是每本都写满,方式二则是只用两本,写完一本就翻到下一本,旧本的内容自动作废。节省了4本练习册的空间,但你需要更小心地记住“现在正在写哪本”。
3.3 蛇形矩阵填充:方向状态机的精确控制
蛇形矩阵要求从1开始,按顺时针螺旋填满6×6矩阵。核心是方向向量(dx,dy)和转向触发条件。
标准解法(见“蛇形距阵.cpp”):
- 初始化:x=0,y=0,dx=0,dy=1(向右);
- 循环填数1到36:
a[x][y] = num++;
// 计算下一步位置
nx = x + dx; ny = y + dy;
// 检查是否越界或已填
if(nx<0 || nx>=6 || ny<0 || ny>=6 || a[nx][ny]!=0) {
// 转向:右→下→左→上→右...
int temp = dx; dx = dy; dy = -temp; // 顺时针旋转90度
}
x += dx; y += dy;
这里dx,dy的旋转公式dx,dy = dy,-dx是关键。验证:初始(0,1)(右)→ (1,0)(下)→ (0,-1)(左)→ (-1,0)(上)→ (0,1)(右),完美闭环。
常见陷阱:
- 越界检查顺序:必须先nx<0 || nx>=6,再a[nx][ny]!=0,否则nx越界时访问a[nx][ny]会段错误;
- 初始化a[6][6]必须全零:int a[6][6] = {};,否则未赋值元素是随机值,a[nx][ny]!=0永远为真;
- 转向后必须重新计算nx,ny:不能直接x+=dx,y+=dy,因为转向前的nx,ny已无效。
实测技巧:在转向处加打印printf("Turn at (%d,%d), new dir (%d,%d)\n", x, y, dx, dy);,运行后你会看到转向点精准落在边界上(如(0,5)右转下,(5,5)下转左),这是验证逻辑正确的黄金标准。
3.4 铺地毯坐标模拟:矩形覆盖的几何转化
“铺地毯.cpp”模拟N张矩形地毯覆盖平面,查询点(p,q)被多少张覆盖。本质是二维区间包含判断。
数学模型:地毯i覆盖区域为[x1_i, x2_i] × [y1_i, y2_i](闭区间),点(p,q)被覆盖当且仅当:
p >= x1_i && p <= x2_i && q >= y1_i && q <= y2_i
代码实现要点:
- 输入时,x1,x2,y1,y2需保证x1<=x2且y1<=y2,但题目未明确,故应主动校正:
if(x1>x2) swap(x1,x2); if(y1>y2) swap(y1,y2);
- 查询时,对每张地毯i循环判断,计数器cnt++;
- 输出cnt即可。
易错点排查:
- 坐标系混淆:题目说“左上角(x1,y1)”,但计算机屏幕y轴向下增长,而数学坐标y轴向上。本题按常规数学坐标处理(y增大向上),故无需坐标变换;
- 边界包含:题目明确“覆盖”,即闭区间,所以用<=而非<;
- 数据范围:题目未限定坐标范围,但数组大小6×6暗示坐标在0-5间,故p,q输入后应检查if(p<0||p>5||q<0||q>5) cnt=0;,避免无效查询。
我在教学中会让学生画图:在纸上画6×6网格,标出地毯1(0,0)→(2,2),地毯2(1,1)→(3,3),然后手动算点(1,1)被覆盖几次——答案是2次。这个过程比看代码快十倍。
4. 实操过程与编译运行指南
4.1 环境准备与一键编译脚本
所有文件均使用标准C++11特性,无需额外库。推荐环境:Ubuntu 22.04 + g++ 11.4.0,或 Windows WSL2 同配置。
编译命令(通用):
g++ -std=c++11 -o a.out filename.cpp
./a.out
为提升效率,我编写了compile_all.sh脚本(可自行创建):
#!/bin/bash
for file in *.cpp; do
echo "=== Compiling $file ==="
g++ -std=c++11 -o "${file%.cpp}" "$file" 2>/dev/null
if [ $? -eq 0 ]; then
echo "✓ Success"
# 自动运行并输入示例数据(针对需要输入的题)
if [[ "$file" =~ "鞍点|铺地毯|图像相似" ]]; then
echo "Sample input:"
case "$file" in
"鞍点.cpp") echo -e "6\n1 2 3 4 5 6\n7 8 9 10 11 12\n13 14 15 16 17 18\n19 20 21 22 23 24\n25 26 27 28 29 30\n31 32 33 34 35 36" | ./"${file%.cpp}" ;;
"铺地毯.cpp") echo -e "2\n0 0 2 2\n1 1 3 3\n1 1" | ./"${file%.cpp}" ;;
esac
fi
else
echo "✗ Failed"
fi
done
运行chmod +x compile_all.sh && ./compile_all.sh,即可批量编译所有文件,并对需输入的题目自动喂样例数据。脚本中2>/dev/null屏蔽编译警告,聚焦错误;"${file%.cpp}"提取文件名(去掉.cpp后缀)作为可执行文件名,避免冲突。
提示:若遇
g++: command not found,Ubuntu执行sudo apt update && sudo apt install build-essential;Windows用户安装MinGW-w64,路径加入系统环境变量。
4.2 输入输出规范与样例数据
所有题目遵循统一I/O协议:
- 输入:纯文本,无提示字符串,数字间以空格或换行分隔;
- 输出:纯结果,无额外说明,末尾换行。
典型样例:
- “鞍点.cpp”输入:
6 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
输出:No saddle point(因每行最大在末列,但该列是全局最大而非最小)
-
“蛇形距阵.cpp”输入:无,直接输出6×6螺旋矩阵。
-
“铺地毯.cpp”输入:
2 0 0 2 2 1 1 3 3 1 1
输出:2(点(1,1)被两张地毯覆盖)
注意:输入数据必须严格匹配格式。例如“铺地毯.cpp”第一行是地毯数量N,接下来N行每行4个整数,最后一行是查询点坐标。少一个换行或空格,
cin>>就会阻塞。
4.3 关键编译选项与调试技巧
-
开启警告(必加):
g++ -std=c++11 -Wall -Wextra -o a.out filename.cpp
-Wall启用常用警告,-Wextra追加额外检查。例如未初始化变量、比较有符号/无符号数,都会报warning: 'x' is used uninitialized,比运行时崩溃早发现错误。 -
调试符号(开发阶段):
g++ -std=c++11 -g -o a.out filename.cpp
加-g生成调试信息,配合gdb ./a.out可单步执行、查看变量值。对二维数组,gdb中执行print a[2]显示第三行所有元素,print &a[0][0]显示首地址,验证内存连续性。 -
静态分析(进阶):
g++ -std=c++11 -fsanitize=address -o a.out filename.cpp
AddressSanitizer检测内存越界(如a[6][0])、使用释放内存等。运行时报ERROR: AddressSanitizer: heap-buffer-overflow,直接定位到出错行。
实操心得:我习惯在每道题开头加#ifdef DEBUG宏:
#ifdef DEBUG
printf("DEBUG: i=%d, j=%d, a[i][j]=%d\n", i, j, a[i][j]);
#endif
编译时加-DDEBUG启用,发布时去掉,避免污染输出。这比cout更轻量,且可批量开关。
5. 常见问题与排查技巧实录
5.1 编译期高频错误与修复
| 错误信息 | 原因 | 修复方案 | 出现场景 |
|---|---|---|---|
error: ‘for’ loop initial declarations are only allowed in C++11 mode | 未指定C++11标准,for(int i=0;...)不被支持 | 添加-std=c++11编译选项 | 所有含C++11语法的文件 |
error: ‘printf’ was not declared in this scope | 未包含<cstdio>头文件 | 在文件开头加#include <cstdio> | 使用printf/scanf的文件(如鞍点.cpp) |
error: ‘vector’ does not name a type | 未包含<vector>且未用std::vector | 加#include <vector>,或声明using std::vector | 杨辉三角123.cpp等用STL的文件 |
warning: array subscript has type ‘char’ | 用char变量作下标,如a[c][j],c可能是负数 | 将下标变量声明为int,或加static_cast<int>(c) | 输入字符作坐标的题目 |
注意:
.gitignore和.inscode是配置文件,勿尝试编译;main和PNcsfk9WXbijCcTmiQKe-master-9e7f5bd0786b616c574ee29f1f2dc60cfb70b184是目录或哈希名,非源文件。
5.2 运行时典型Bug与定位方法
Bug 1:程序闪退(Segmentation fault)
- 现象:运行后立即崩溃,无输出;
- 原因:二维数组越界访问,如a[6][0](合法索引0-5);
- 定位:用AddressSanitizer编译,报错行如a[i+1][j]中i=5导致i+1=6;
- 修复:循环条件改为i<5,或加边界检查if(i+1<6) a[i+1][j]=...。
Bug 2:输出全0或乱码
- 现象:矩阵输出全是0,或数字异常大;
- 原因:数组未初始化,int a[6][6];中元素值随机;
- 修复:声明时初始化int a[6][6] = {};(C++11允许),或用memset(a,0,sizeof(a))。
Bug 3:鞍点总找不到
- 现象:输入明显有鞍点的数据(如[[1,2,3],[4,5,6],[7,8,9]],a[0][2]=3是首行最大,但第2列[3,6,9]最小是3,应是鞍点),却输出“No”;
- 原因:验证列最小值时,循环未排除当前行,a[0][2]被自己参与比较,导致min_val=3但a[0][2]==3成立;
- 修复:列验证循环中加if(k!=i)跳过当前行。
Bug 4:蛇形矩阵最后几格填错
- 现象:前30个数字正确,后6个错位;
- 原因:转向判断逻辑缺陷,如用if(a[nx][ny]!=0)但nx,ny已越界,访问非法内存返回随机值;
- 修复:严格按顺序检查:先nx<0 || nx>=6 || ny<0 || ny>=6,再a[nx][ny]!=0。
5.3 性能与数值安全陷阱
- 整数溢出:图像相似度计算
sum((a[i][j]-b[i][j])²),若像素值0-255,差值最大255,平方达65025,36个像素总和超230万,int(通常2^31-1≈21亿)安全,但若扩展到100×100则需long long。 - 浮点精度:题目未涉及浮点,但若自行扩展(如归一化相似度),避免
float,用double。 - 输入缓冲区:
cin读取大量数据可能慢,ios::sync_with_stdio(false); cin.tie(0);可提速,但会禁用printf/scanf混合。
实操心得:我在“回文数123.cpp”中故意设置了一个陷阱——输入数字转二维数组时,用
num%10取个位,但未处理num==0的边界。运行时输入0会无限循环。修复只需在循环前加if(num==0) { a[0][0]=0; break; }。这个坑我让学生自己踩,比讲十遍都管用。
6. 学习路径建议与能力跃迁地图
这23题不是终点,而是二维数组能力的“校准器”。完成全部后,你应该能清晰回答:
- 当看到“给定矩阵,求每行最大值的平均数”,立刻反应:需要
vector<int> row_max(6);存每行最大,再accumulate求和除6; - 当遇到“判断矩阵是否对称”,脱口而出:
for(i=0;i<6;i++) for(j=0;j<i;j++) if(a[i][j]!=a[j][i]) return false;(只检上三角,避免重复); - 当设计“扫雷游戏”,自然想到用
int mine[6][6]存雷数,bool revealed[6][6]存翻开状态,两者耦合但职责分明。
下一步拓展建议:
- 进阶算法:将“鞍点”升级为“查找所有局部极大值”(八邻域比较);将“蛇形矩阵”改为“Z字形遍历”(按行奇偶交替左右);
- 工程实践:用class Matrix封装二维数组,添加transpose()、multiply(const Matrix&)等方法;
- 性能优化:对“图像相似度”,尝试SIMD指令(<immintrin.h>)并行计算差值平方;
- 跨语言迁移:用Python NumPy重写“杨辉三角”,体会向量化操作与C++循环的本质差异。
最后分享一个小技巧:每周选3题,不看答案,限时30分钟手写代码,然后用diff对比标准答案。坚持一个月,你会发现,那些曾让你纠结的i和j,已变成肌肉记忆——就像骑自行车,一旦学会,永远不忘。
简介:包含23个独立.cpp文件,全部聚焦二维数组在C++中的实际应用,每个文件都经过验证可直接g++编译运行,无需额外依赖。题目覆盖基础操作:行列遍历、对角线处理、求和统计、乘法表输出;也涵盖信息学常见模型:鞍点判断(先找行最大再验列最小)、两种写法的杨辉三角、顺时针/逆时针蛇形矩阵填充与输出、稀疏矩阵识别、铺地毯坐标模拟、图像像素相似度比对、回文数二维判定、去重逻辑(含双版本对比)、内存连续性理解示例。所有代码采用标准输入输出,变量命名清晰,关键步骤有简明注释,嵌套循环结构分步展开,便于初学者掌握索引规律和边界控制。数组规模统一控制在6×6左右,兼顾理解效率与算法完整性,适合日常训练、课堂练习或竞赛前快速巩固。


被折叠的 条评论
为什么被折叠?



