ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

蛇形填数:C++二维数组与模拟算法的入门经典题详解

蛇形填数:C++二维数组与模拟算法的入门经典题详解 1. 这道“蛇形填数”为什么值得单独掰开讲如果你刚学到二维数组十有八九会在《信息奥赛一本通·编程启蒙》的例63.1题库编号3365这道“蛇形填数”上卡一下。题目描述很简单在 n×n 的方阵里填入 1,2,...,n×n要求填成蛇形。比如 n4 时输出是下面这个样子10 11 12 1 9 16 13 2 8 15 14 3 7 6 5 4先盯着这个输出看十秒找一找数字是按什么路线填下去的。1 在右上角2、3、4 一路向下5、6、7 往左8、9、10 往上11、12 往右13、14 再往下15 往左16 再往上——一圈一圈往里绕越绕越小。题目本身不难不涉及任何高深算法但它几乎是每个初学者的第一道“模拟题”。什么叫模拟题就是你要在脑子里把填数的全过程完整演一遍然后把每一步的规则用循环和数组表达出来。二维数组的坐标能不能写明白循环的终止条件能不能想清楚全在这一道题里见真章。不管你是在备战 CSP-J原来的 NOIP 普及组还是在刷蓝桥杯省赛题或者纯粹带娃学 C这道题都是绕不过去的“门槛”。很多学生第一次 AC 这道题之后后面再碰上旋转矩阵、螺旋遍历、方向模拟这类题目思路会顺很多。这篇就把它彻底拆开从路径规律到两版代码从手把手追踪到高频错误排查最后再聊几个它延伸出去的变形。样例输入长这样4样例输出长这样10 11 12 1 9 16 13 2 8 15 14 3 7 6 5 4注意每个数字占 4 个字符宽度、右对齐这属于输出格式的一部分后面会专门说。2. 蛇形路径的规律把路线拆成“四条边一组”做模拟题的第一原则先想清楚过程再动手写代码。所以先别看代码先把路线看穿。在这道题里起点在方阵的右上角也就是第 0 行、第 n-1 列。数字 1 填在那里。接下来蛇头按照“下、左、上、右”的顺序循环移动每次沿着一个方向一直走直到走不动然后换下一个方向。整条路径其实就是四段走法循环往复“向下走到底、向左走到底、向上走到底、向右走到底”然后回到“向下”只不过下一圈的每条边都更短了。什么叫“走不动”只有两种情况第一种是越界比如已经走到第 0 行还要继续往上或者已经走到第 n-1 列还要往右那就跑出矩阵了第二种是前方那个格子已经有数字了相当于自己的尾巴把路堵死了。因为填过的格子数值非 0而没填过的是 0所以判断条件统一写成“下一格在界内且值为 0”。以 n4 为例把每一步的位置和方向列成一张表规律一目了然步数填入数字坐标行,列当前方向停下来的原因11(0, 3)起点—22(1, 3)下—33(2, 3)下—44(3, 3)下下一格(4,3)越界55(3, 2)左—66(3, 1)左—77(3, 0)左下一格(3,-1)越界88(2, 0)上—99(1, 0)上—1010(0, 0)上下一格(-1,0)越界1111(0, 1)右—1212(0, 2)右下一格(0,3)已有数字11313(1, 2)下—1414(2, 2)下下一格(3,2)已有数字51515(2, 1)左下一格(2,0)已有数字81616(1, 1)上下一格(0,1)已有数字11这里有一个特别重要的思维转变判断能不能走看的是“下一格”不是“当前格”。也就是说先把脚往那个方向试探一步发现走不了就退回来换方向。代码上体现为“先判断、再移动”这个顺序一旦反了要么数组越界要么漏数字。我把这种写法叫做“贪吃蛇思维”蛇沿着一个方向游前面是墙矩阵边界或者自己的身子已填数字就拐个弯。内层每一段“走到底”对应一个 while 循环四段 while 拼成一层完整绕圈外层再用一个“总数字没填满就继续”的大条件包住问题就解决了。3. 两版实现四段式写法与方向数组写法先给出最适合初学者的写法二维数组加四个 while 循环。每个 while 只负责一个方向走不动就自动停下来让下一个 while 接管。#include iostream #include iomanip using namespace std; int a[35][35]; int main() { int n; cin n; int x 0, y n - 1; // 起点右上角 int tot 1; a[x][y] tot; // 先把起点数字填好 while (tot n * n) { // 向下走行号增大 while (x 1 n a[x 1][y] 0) { a[x][y] tot; } // 向左走列号减小 while (y - 1 0 a[x][y - 1] 0) { a[x][--y] tot; } // 向上走行号减小 while (x - 1 0 a[x - 1][y] 0) { a[--x][y] tot; } // 向右走列号增大 while (y 1 n a[x][y 1] 0) { a[x][y] tot; } } for (int i 0; i n; i) { for (int j 0; j n; j) { cout setw(4) a[i][j]; } cout endl; } return 0; }几个关键点拆开说数组a[35][35]定义成全局变量全局数组默认全部初始化为 0。0 正好表示“这个格子还没填”所以判断a[x1][y] 0等价于“下一格空着”。n 最大只有 30开到 35 留出余量别开成一个正好 n×n 的小数组那样写起来束手束脚。起点必须设成x 0, y n - 1也就是右上角。先把 1 填进去tot从 1 开始。如果你从左上角开始那画出来的是另一种螺旋不是本题要的答案。外层while (tot n * n)表示“没填满就继续绕圈”。每一轮循环体里依次执行向下、向左、向上、向右四段填充。四段都走完之后如果还没填满就再来一轮此时路径自然往里缩一圈。tot先自增再作为当前数字使用--y、x也是先改变坐标再访问数组。这种写法必须配合前面的边界判断顺序是“先判断、再移动”别写成a[x][y]那样容易越界。再看第二种写法方向数组。把“下、左、上、右”四个方向翻译成坐标增量存进两个数组#include iostream #include iomanip using namespace std; int a[35][35]; int dx[4] {1, 0, -1, 0}; // 下、左、上、右的行的增量 int dy[4] {0, -1, 0, 1}; // 下、左、上、右的列的增量 int main() { int n; cin n; int x 0, y n - 1; // 起点右上角 int dir 0; // 初始方向向下 int tot 0; while (tot n * n) { a[x][y] tot; // 先填当前格 if (tot n * n) { // 最后一个数填完就收工 break; } // 试探下一步位置 int nx x dx[dir]; int ny y dy[dir]; // 走不了就右转方向 1 取模再试一次 if (nx 0 || nx n || ny 0 || ny n || a[nx][ny] ! 0) { dir (dir 1) % 4; nx x dx[dir]; ny y dy[dir]; } x nx; y ny; } for (int i 0; i n; i) { for (int j 0; j n; j) { cout setw(4) a[i][j]; } cout endl; } return 0; }两版代码都能通过差别在于思维方式。四段式写法直观每一段在干什么读代码的人一眼就懂方向数组写法把“方向变化”抽象成了“转向”代码更精简扩展起来也更容易。等你后面遇到机器人走迷宫、贪吃蛇这类题目方向数组几乎是标配所以两种都值得写一遍。特别是dir (dir 1) % 4这句它相当于把四个方向串成一个环走投无路就右转理解它比背代码重要得多。复杂度上两种写法都是 O(n²)每个格子恰好被填一次每个格子最多被额外试探一两次。n≤30空间开一个 35×35 的数组绰绰有余性能和内存都不是问题。输出格式这儿再强调一遍setw(4)让每个数占 4 个字符宽且右对齐不满 4 位左边补空格所以样例输出里一位数前面有三个空格、两位数前面有两个空格。用setw自动对齐不要自己手动补空格否则很容易在行尾多出一个空格被判 Presentation Error。别忘了#include iomanip。4. 手把手追踪 n4每一格数字是怎么落位的前面那张表是从人的视角看路线这一节我们从代码的视角把 n4 完整过一遍重点看“先判断再移动”的时机。程序启动a[0][3] 1tot 1进入外层循环。第一段“向下”检查x 1 n当前 x0x11 小于 4通过再看a[1][3]等于 0通过。于是x到 (1,3)tot得 2填入。重复同样的判断a[2][3]为 0填 3a[3][3]为 0填 4。再往下x 已经是 3x 1等于 4不小于 n条件失败这段 while 结束。注意循环结束时 x 停在第 3 行也就是底边界并没有真的越界访问a[4][3]。判断条件把越界访问挡在了门外。接着“向左”从 (3,3) 看a[3][2]为 0填 5a[3][1]填 6a[3][0]填 7。再往左y - 1等于 -1小于 0条件失败停。“向上”从 (3,0) 一路向上a[2][0]、a[1][0]、a[0][0]分别填 8、9、10。再往上x-1 等于 -1停。“向右”从 (0,0) 看a[0][1]填 11a[0][2]填 12。再往右看a[0][3]那里已经是 1不等于 0条件失败停。这是第一次因为“前方有数字”而不是“越界”导致的转向。此时 tot12还没到 16外层 while 继续新一轮绕圈。这时候矩阵中间四格还是 010 11 12 1 9 0 0 2 8 0 0 3 7 6 5 4第二轮“向下”从 (0,2) 看a[1][2]为 0填 13a[2][2]填 14。再看a[3][2]那里已经是 5停。这一小段只走了两格。“向左”看a[2][1]为 0填 15。再看a[2][0]已是 8停。“向上”看a[1][1]为 0填 16。再看a[0][1]已是 11停。“向右”看a[1][2]已是 13直接停。至此 tot16外层 while 条件tot n * n不成立循环结束输出最终矩阵10 11 12 1 9 16 13 2 8 15 14 3 7 6 5 4这样一帧一帧过完你应该能体会到一个关键点每次移动之前都先检查“下一格”检查不过就原地转向绝不多跨半步。这也是为什么内层 while 条件里必须把“界内”和“为0”同时写清楚缺一个都会出问题。5. 高频翻车现场常见错误与排查三板斧代码看懂了自己写还是会栽跟头。下面是我陪学生刷题过程中见到频率最高的几个坑每一个都有人踩你提前知道就能避开。第一个坑起点和方向搞反。总有学生把起点放在 (0,0) 左上角然后往右走画出来的是“顺时针螺旋”不是本题的“右上角起、下左上右逆时针绕”。输出完全不一样。拿到题目先框出起点坐标和第一个方向这两个定了整体骨架就定了。第二个坑数组里有垃圾值。如果数组定义在 main 函数内部且没有初始化里面是随机数据a[nx][ny] 0这个判断会随机失灵。症状很典型数字走到一半突然停住或者路径莫名其妙提前拐弯。解决办法很简单数组放全局或者定义时写int a[35][35] {};也可以用memset(a, 0, sizeof(a));。全局数组默认清零这件事竞赛里经常用来省事但要记得它只在全局生效。第三个坑tot 的边界弄错。常见两种一是外层写成while (tot n * n)填完最后一个数之后还会再试图填一次直接越界二是忘了给起点赋值从 1 变成从 0 开始整个矩阵错位。我习惯的写法是“下一格填tot”起点先单独填 1这样从逻辑上不容易漏。选一种写法全程序保持一致别一会儿先加后填、一会儿先填后加。第四个坑方向顺序写反。“下、左、上、右”的顺序不能换。如果先向左再向下看起来只是换了顺序实际上每一条边的“墙”位置取决于之前走过的路径一个方向错后面全错。方向数组写法里尤其注意 dx、dy 数组和 dir 初始值要配对。第五个坑输出格式被扣分。setw(4)没写或者忘了#include iomanip编译直接报错有人自己用循环补空格多补少补都容易被判 Presentation Error。用 setw 最稳它只在左边补空格不会在行尾留下多余空格所以每一行结尾不用额外处理。如果你拿到的题目要求每个数之间空一格而不是定宽那就要改成“前 n-1 个数输出后带一个空格最后一个不带”这也是常见格式陷阱。至于“结果不对但看不出哪错”的排查我的三板斧是这样的第一缩小规模。把 n 改成 4、5 这种小数字可以在纸上验证每一步。第二打印中间状态。在循环里每隔几步输出当前坐标和 tot或者在每次转向之后输出一次矩阵。小矩阵直接全部打印对比预期路径很快能定位是在哪一段方向走偏的。比如临时在循环里加一句cout tot tot 填入 ( x , y ) endl;第三对照检查表逐条核对起点对不对方向顺序对不对边界判断有没有写反数组是不是全局清零外层循环条件到底是还是很多时候答案就藏在最后这条里。6. 从蛇形填数延伸出去的变形与通用骨架AC 一道题不算完“蛇形”在算法题里是一大家子换个起点、换个方向顺序就是一道新题。先说几个最常见的亲戚。第一类是“从左上角开始、先右后下顺时针绕圈”的螺旋矩阵比如 LeetCode 59 螺旋矩阵 II。n4 时输出长这样1 2 3 4 12 13 14 5 11 16 15 6 10 9 8 7核心思路一模一样只是起点变成 (0,0)方向顺序变成“右、下、左、上”。把方向数组的四个增量换一下顺序就能写出来。第二类是“行与行之间蛇形往返”的蛇形矩阵这个和本题的“螺旋蛇形”不是同一个路线1 2 3 4 8 7 6 5 9 10 11 12 16 15 14 13这道题的规律是每一行内部连续填数奇数行从左到右、偶数行从右到左。写起来比螺旋还简单很多教材里也叫它“蛇形矩阵”和本题的“蛇形填数”名字相近、含义不同搜索资料的时候要注意区分免得看错题解。第三类是“从矩阵中心开始由内向外绕”的螺旋这个稍微难一点因为起点、终点和圈数都需要自己推导边界条件更多。事实上我见过不少竞赛里的“小变态题”都是从这些基础变形加上几个限制条件加工出来的。这几类题有一个完全通用的解题骨架开好二维数组并清零定起点坐标用方向数组或者四段 while 循环不断尝试移动遇到越界或已填格子就转向直到总填数量达到 n×n 收工。方向数组里的 dx、dy 就是这套骨架的“方向盘”想让它怎么绕全看你怎么定义这四个方向。最后分享一点个人经验刷模拟题千万别满足于 AC 就收工。试着把这道题改成顺时针版本试着从中心开始填试着把数字换成字符路径甚至步数记录每改一次你对数组下标和边界条件的理解就深一层。蛇形填数之所以能成为《信息奥赛一本通·编程启蒙》里的经典例题不是因为它的算法有多难而是因为它用最小成本逼着你把二维数组、循环边界和模拟思维这三样基本功练扎实。这三样练好了后面学什么矩阵、搜索、状态转移都会轻松得多。
返回列表