news 2026/10/14 6:45:50

蛇形方阵入门:模拟算法与方向数组的经典实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
蛇形方阵入门:模拟算法与方向数组的经典实战

P5731 蛇形方阵,是我刷题时见过的特别适合练“模拟”的入门题之一。题面很干脆:给一个正整数 n,要求输出一个 n×n 的方阵,数字从 1 开始,像蛇一样从外圈一圈一圈往里面盘。比如 n=4 时,结果是这样:

1 2 3 4 12 13 14 5 11 16 15 6 10 9 8 7

我第一次看到这道题,觉得不就是填数吗,结果自己写完之后跑样例全对,一交上去却 WA。后来才发现,蛇形方阵考的不是你有多聪明,而是你能不能把四个方向的转向、边界判断和输出格式一次性处理对。这篇文章就把我从题意理解、核心思路、完整代码到调试踩坑的整个过程都拆开讲清楚,适合刚学数组和模拟的新手,也适合想快速复习的同学。

1. 题目理解:从输出反推行走路径

1.1 输入输出到底长什么样

题目只有一个输入:正整数 n。输出是 n 行,每行 n 个数字,数字之间不是用普通空格分隔,而是每个数字固定占 3 个字符宽度、右对齐。这一点特别重要,因为很多人只顾着填数,忘了格式要求,结果输出数组内容完全正确,却因为每行开头多了一个空格或者少了一个空格被判错误。

以 n=3 为例,正确输出应该是:

1 2 3 8 9 4 7 6 5

可以看到,个位数前面都补了两个空格,两位数前面补了一个空格,这样所有数字都顶到右边的第 3 列。如果不按这个格式输出,哪怕方阵本身的数字顺序全对,OJ 也会给你一个红红的 WA。

1.2 数字的行走路线:一圈一圈往里绕

很多同学看到“蛇形”两个字,第一时间想到的是那种一行从左到右、下一行从右到左的 S 形走法。但 P5731 里的蛇形方阵并不是那个东西,它实际是“回字形螺旋”:

  • 从左上角出发,先向右走到这一行的最右端;
  • 然后向下走到最下端;
  • 再向左走到最左端;
  • 最后向上走回起点下方的位置;
  • 然后继续进入里面一圈,重复“右、下、左、上”的过程。

拿 n=4 的矩阵来说,数字 1 到 4 是第一行,5 到 7 是最右边一列,8 到 10 是最下面一行,11 到 12 是最左边一列,这就算走完外圈。接下来从数字 13 开始,进入内圈的 2×2 区域继续绕。整个填数过程,本质上就是一条“碰壁就右转”的贪吃蛇。

1.3 为什么很多人第一次会写错

这道题看着简单,真正写错的人非常多。最常见的错误是把“蛇形”理解成了左上、右上、右下、左下的斜向走法,或者写成了从上到下、再从下到上交替填列。这两种都和题目要求的回形路径不是一回事。

另一个容易错的地方是起点和方向顺序。题目要求最外圈先向右,然后顺时针绕。如果你把方向顺序写成“右、上、左、下”,出来的矩阵就是逆时针的,虽然看起来也是一种“蛇形”,但和样例对不上。

所以我拿到这类题目,第一件事永远是先在草稿纸上把 n=3 或 n=4 的行走路线画出来,标上箭头,再开始写代码。画清楚之后,代码反而简单很多。

2. 核心思路拆解:模拟法为什么是首选

2.1 用“方向数组”模拟当前前进方向

解决蛇形方阵最直接的思想就是模拟:从第一个格子开始,每次把当前数字填进去,然后往当前方向前进一步。如果发现下一步会越界,或者下一步的格子已经被填过了,就把方向顺时针旋转 90°,再走到新的位置。

那么如何用程序表达“方向”?最标准的方法是方向数组。把四个方向分别表示成坐标的增量:

  • 向右:行不变,列加 1,对应(0, 1);
  • 向下:行加 1,列不变,对应(1, 0);
  • 向左:行不变,列减 1,对应(0, -1);
  • 向上:行减 1,列不变,对应(-1, 0)。

写成数组就是:

int dx[4] = {0, 1, 0, -1}; int dy[4] = {1, 0, -1, 0};

再用一个变量dir标记当前方向是第几个。当需要转向时,执行dir = (dir + 1) % 4就能循环切换,非常干净。这个技巧在迷宫、扫雷、冰壶推箱子等一大堆题目里都能用,值得单独记下来。

2.2 边界判断:越界或已填都算“撞墙”

方向数组定下来以后,每次计算下一步的位置:

int nx = x + dx[dir]; int ny = y + dy[dir];

然后判断这个(nx, ny)还能不能走。有两种情况必须转向:

  1. 超出 n×n 的方阵范围:也就是nx < 0 || nx >= n || ny < 0 || ny >= n;
  2. 这个格子已经被填过数字了:因为蛇形方阵要往里绕,内圈的格子之前可能已经赋值,不能重复走。

判断“被填过”有一个很省事的办法。因为矩阵初始值全部为 0,而填入的数字从 1 开始,所以只要检查a[nx][ny] != 0,就知道这个格子是不是已经走过了。注意这个技巧依赖“数字从 1 开始”这个前提,如果题目改成从 0 开始编号,这个方法就要换成初始化成 -1 或使用独立的访问标记数组。

2.3 填数循环的通用骨架

整体流程可以压缩成这样的伪代码:

设置起点为 (0, 0) 初始方向为向右 for cur = 1 to n*n: 把 cur 填到当前格子 计算下一步位置 如果下一步越界或者已经填过: 方向旋转 90 度 重新计算下一步位置 移动到下一步

这个循环次数是固定的n*n次,所以不会出现“不知道该什么时候停”的问题。每填一个数,必然走一个格子,循环结束矩阵也就填满了。

我个人的经验是,这种写法最好理解,也不太容易出逻辑错误。你不需要额外计算每一圈的边界,只需要记住“撞到墙就右转”,剩下的交给方向数组。它唯一的缺点是每次都要算一次下一步可不可走,但这在 n 很小的入门题里完全不是问题。

3. 完整代码实现:C++ 和 Python 各写一遍

3.1 C++ 标准写法

下面这段是我比较推荐的 C++ 写法,配合vector动态开二维数组,避免静态数组大小开错的问题:

#include <iostream> #include <vector> #include <iomanip> using namespace std; int main() { int n; cin >> n; vector<vector<int>> a(n, vector<int>(n, 0)); int dx[4] = {0, 1, 0, -1}; int dy[4] = {1, 0, -1, 0}; int dir = 0; int x = 0, y = 0; for (int cur = 1; cur <= n * n; ++cur) { a[x][y] = cur; int nx = x + dx[dir]; int ny = y + dy[dir]; 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(3) << a[i][j]; } cout << "\n"; } return 0; }

有几个细节要解释一下。

第一,setw(3)来自<iomanip>头文件,作用是让后面的输出占 3 个字符宽度,默认右对齐。所以输出1会变成两个空格加一个1,输出12会变成一个空格加12。这正是题目要求的格式。

第二,最后一次循环里,cur已经等于n*n,填完后还会计算下一步的nx, ny。这时算出来的坐标可能是越界的,也可能指向已经填过的格子,但没关系,因为循环马上结束,x和y虽然被更新了,却再也不会被使用。如果你心里不踏实,也可以在填完立即break,比如:

if (cur == n * n) break;

第三,判断越界时用了||短路。C++ 从左往右判断,当nx < 0成立时,后面的nx >= n、a[nx][ny]都不会再执行,所以不用担心数组下标访问越界。

3.2 Python 写法:适合快速验证思路

如果你平时用 Python 刷题,可以写得更紧凑:

def solve(): n = int(input()) a = [[0] * n for _ in range(n)] dx = [0, 1, 0, -1] dy = [1, 0, -1, 0] d = 0 x = y = 0 for cur in range(1, n * n + 1): a[x][y] = cur nx = x + dx[d] ny = y + dy[d] if nx < 0 or nx >= n or ny < 0 or ny >= n or a[nx][ny] != 0: d = (d + 1) % 4 nx = x + dx[d] ny = y + dy[d] x, y = nx, ny for row in a: print("".join(f"{v:3d}" for v in row)) if __name__ == "__main__": solve()

Python 里的f"{v:3d}"等价于把整数v格式化成占 3 位的右对齐字符串。比如f"{1:3d}"的结果是" 1",前面有两个空格。因此"".join之后,每一行开头都会有一个或两个空格,这正是题目想要的效果。

用 Python 做题的好处是代码量少、逻辑一眼就能看穿。不过需要注意,如果题目给的 n 比较大,Python 的二维列表会比 C++ 稍微慢一些,但 P5731 这种入门题完全不用纠结性能。

3.3 复杂度分析:时间 O(n²),空间 O(n²)

整个算法最核心的工作是填满 n×n 个格子。填每个格子时,方向判断和坐标更新都是常数时间,所以总时间复杂度是 O(n²)。空间上需要存一个 n×n 的矩阵,所以空间复杂度也是 O(n²)。

这个复杂度在 n 很小的时候没有任何压力。哪怕你以后遇到变体题,把 n 提升到几百甚至上千,O(n²) 的模拟通常也能过,只是要注意输出是否会超时。如果 n 超过几千,反复调用输出函数也可能成为瓶颈,那时候再考虑整行字符串拼接或者快写优化。

3.4 输出格式的隐藏细节

我见过不少人因为输出格式 WA 到怀疑人生。题目说“每个数字占 3 个字符宽度、右对齐”,不等于“数字之间加一个空格”。如果你的代码写成:

cout << a[i][j] << " ";

那么每个数字之间是普通空格,宽度完全不对,样例输出会变成:

1 2 3 4 12 13 14 5

这样一看就和题目要求不一样。正确的思路是把每个数字看成一个宽度为 3 的字符串,所有数字紧挨着输出,靠前导空格补足宽度。C++ 的setw(3)和 Python 的f"{v:3d}"都是这个意思。

4. 新手最常踩的坑与排查技巧

4.1 数组越界:静态数组开太小

有些人图省事,直接写:

int a[10][10];

如果题目数据保证 n <= 9,那当然没问题。但万一你拿到的是 n=10 的数据,a[10][10]就已经越界了。我的建议是直接用vector动态申请,按输入 n 的大小开数组。这样既能避免越界,代码也更通用。

还有一个小坑:很多人用一维数组加下标换算来模拟二维矩阵,结果方向变化时下标算错。新手阶段我还是推荐直接用二维数组,逻辑清晰,调试也方便。

4.2 方向数组写反:顺时针还是逆时针

方向数组的顺序决定了螺旋方向。想要顺时针,顺序必须是“右、下、左、上”,对应:

int dx[4] = {0, 1, 0, -1}; int dy[4] = {1, 0, -1, 0};

如果你写成了“右、上、左、下”,也就是:

int dx[4] = {0, -1, 0, 1}; int dy[4] = {1, 0, -1, 0};

出来的就是逆时针螺旋。虽然看起来也是螺旋,但样例对不上。记住一个小技巧:把方向数组的“转向”理解为顺时针换方向,即右转 90°,因为当前进方向是右时,右转后是下;是下时,右转后是左;是左时,右转后是上。

4.3 判断已填格子:初始值一定要是 0

如果矩阵初始化时用vector<vector<int>> a(n, vector<int>(n)),默认初始化为 0,这时用a[nx][ny] != 0判断是没问题的。但如果你初始化时给所有格子赋了别的值,或者用memset填了-1,这个判断就失效了。

另外要注意,判断“下一步能不能走”的时候,一定要在计算完nx, ny之后判断,不能在填当前格子之前判断。方向数组的核心是“下一步撞墙才转向”,而不是“预判整条路”。

4.4 死循环和停不下来的原因

方向数组模拟法理论上不可能死循环,因为循环次数固定是n * n。但如果你把循环条件写成while (true),并且忘了在里面判断填满退出,就有可能出现越界后仍然继续填的情况。

还有一种情况是,转向后没有重新计算nx, ny,导致原地打转。很多人写转向时只改了dir,忘了重新算下一步坐标。最后的x = nx; y = ny;仍然是转向之前的坐标,于是下一次继续撞墙,循环就乱了。解决办法是,检测到需要转向后,马上重新计算一次:

dir = (dir + 1) % 4; nx = x + dx[dir]; ny = y + dy[dir];

这一步非常关键。

4.5 手动调试:把每一步路径打出来

如果写完代码样例都过了,但提交还是错,我建议你把路径打印出来。比如在填数循环里临时加一句输出:

cout << x << " " << y << " " << cur << endl;

然后跑 n=4,看看数字 1 到 16 对应的坐标是不是:

(0,0) -> (0,1) -> (0,2) -> (0,3) (1,3) -> (2,3) -> (3,3) (3,2) -> (3,1) -> (3,0) (2,0) -> (1,0) (1,1) -> (1,2) (2,2)

如果坐标路线正确,那就是输出格式的问题;如果坐标路线本身乱了,那就检查方向数组和转向逻辑。这个排查方法比盯着屏幕看矩阵更高效。

5. 更稳的写法:上下左右边界收缩法

5.1 四个边界变量的思路

方向数组虽然好懂,但有些人更喜欢另一种经典写法:维护四个边界变量top, bottom, left, right,表示当前还没填的内圈范围。每填完一条边,就把对应的边界往里缩一格。

伪代码如下:

top = 0, bottom = n-1, left = 0, right = n-1 cur = 1 while cur <= n*n: 从 left 到 right 填 top 这一行 top++ 从 top 到 bottom 填 right 这一列 right-- 从 right 到 left 填 bottom 这一行 bottom-- 从 bottom 到 top 填 left 这一列 left++

这个写法不需要方向数组,思路直观,但对 n 为奇数或偶数时容易漏填内层。比如 n=1 时只有一格,填完 top 行之后top++,如果继续执行填 right 列,会重复访问已经填过的格子。所以要特别注意每个 for 循环之后是否已经填满 n² 个数字。

5.2 边界收缩法的完整代码

我给出一版带防越界判断的代码:

#include <iostream> #include <vector> #include <iomanip> using namespace std; int main() { int n; cin >> n; vector<vector<int>> a(n, vector<int>(n, 0)); int top = 0, bottom = n - 1; int left = 0, right = n - 1; int cur = 1; while (cur <= n * n) { for (int j = left; j <= right && cur <= n * n; ++j) { a[top][j] = cur++; } top++; for (int i = top; i <= bottom && cur <= n * n; ++i) { a[i][right] = cur++; } right--; for (int j = right; j >= left && cur <= n * n; --j) { a[bottom][j] = cur++; } bottom--; for (int i = bottom; i >= top && cur <= n * n; --i) { a[i][left] = cur++; } left++; } for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { cout << setw(3) << a[i][j]; } cout << "\n"; } return 0; }

这里的cur <= n * n条件是我个人比较喜欢的写法,它保证即使只剩一行或一列,也不会重复填充。比如 n=5 时最内层只有一个格子,填完 top 行后top++,此时cur已经变成 26,循环条件cur <= n*n不成立,后面三个 for 循环根本不会执行,安全退出。

5.3 哪种写法更适合你

方向数组模拟法和边界收缩法的本质是一样的:都是“从外圈往内圈填”。区别在于,方向数组法把“方向”显式化,逻辑统一;边界收缩法把“范围”显式化,代码更对称。

我的建议是新手优先掌握方向数组法,因为它更能锻炼坐标变化的思维,而且后面遇到迷宫、搜索类题目时,方向数组会反复出现。边界收缩法可以作为第二种解法理解,两者互相印证,印象更深。

6. 从这道题延伸出去:变体和通用能力

6.1 不要把“蛇形方阵”和“蛇形矩阵”搞混

在很多题目里,“蛇形矩阵”指的是矩阵按行蛇形填充:

1 2 3 6 5 4 7 8 9

也就是奇数行从左到右,偶数行从右到左。它和 P5731 的回形螺旋完全不一样。刷题时一定要看清题目给的样例,不要凭名字判断。P5731 的“蛇形方阵”更准确的说法是“螺旋方阵”或“回形方阵”,只是题目名字叫蛇形而已。

6.2 可以尝试的变体

如果觉得这道题太简单,可以尝试改一改:

  • 改成逆时针螺旋,起点还是左上角;
  • 改成从中心开始往外绕;
  • 改成输出每个位置到中心的距离;
  • 改成 m 行 n 列的矩形螺旋,而不是正方形;
  • 要求不额外开矩阵,直接在输出时计算每个位置是什么数字。

其中最后一种最考验数学功底。每个位置所在的“圈层”可以用:

int layer = min(min(i, j), min(n - 1 - i, n - 1 - j));

计算出来,然后根据当前圈层左上角的起始数字和当前边位置推出该格子的值。感兴趣的同学可以自己推一下,写出来会非常有意思,也能加深对螺旋结构的理解。

6.3 这类题真正锻炼的能力

我始终觉得,P5731 这类题考察的不是某个高深算法,而是三件小事:能不能把自然语言描述的规则转化成程序逻辑,能不能用坐标和方向数组管理状态,能不能在边界条件下保持稳定。这三个能力在后续刷动态规划、图论搜索、模拟题时都是底层能力。

很多人看到“模拟题”就觉得没有技术含量,其实恰恰相反。模拟题最容易暴露一个人对细节的控制力。边界判断多一个等号、方向顺序反了、输出宽度不对,任何一个地方出错,整道题都会变成红色 WA。能把一道模拟题做到一遍过,说明你的代码基本功是扎实的。

7. 我的一点实际体会

最后分享一个我常用的答题习惯:拿到任何“填矩阵”的题目,不管样例多简单,我一定会先手动把 n=3 的完整输出写在草稿纸上,然后对照代码逐格模拟前 9 个数字。这个过程很像在脑子里跑一次程序,能提前发现很多边界问题。

遇到输出格式不确定时,我会故意用 n=1 去测试。n=1 的蛇形方阵只有一个数字1,没有转向、没有内圈,但能快速验证越界和输出宽度是否正确。n=2 则是另一个关键用例,它让程序第一次面临“走完外圈后只剩空”的情况,方向数组法在 n=2 时也能正常收尾。把这两个小用例先跑通,再跑 n=4 的样例,基本就不会出大问题。

如果你是用 C++ 刷题,一定记得加#include <iomanip>,setw函数不在默认的头文件里。如果你是用 Python,记得区分print默认的换行和空格,多用一个join能减少不少麻烦。

做这类题,最忌讳的是“看起来差不多就行了”。填数顺序错一格,整个矩阵都崩;输出宽度错一格,整个答案都不认。把这些细节练成肌肉记忆,以后再遇到任何模拟类题目,你都会比别人稳得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 6:45:28

AIGC创作训练营深度拆解:从提示词到工作流的技能成长指南

1. 为什么“闪学”模式火了&#xff1a;训练营背后的需求逻辑今年以来&#xff0c;身边越来越多非技术背景的朋友开始问同一个问题&#xff1a;AI到底怎么用来赚钱或者提升工作效率&#xff1f;这个问题看起来简单&#xff0c;但真要系统回答&#xff0c;往往会把人绕晕。大模型…

作者头像 李华
网站建设 2026/10/14 6:44:53

树莓派+GPIO:从零搭建家庭能耗监控面板

这篇博文需要你提供具体的项目信息才能开工。目前四个输入参数——项目标题、项目正文、关键词、摘要描述——都是空的&#xff0c;我没法凭空捏造一个项目出来&#xff0c;那样写出来的东西既不符合你的需求&#xff0c;也违背了我做内容的原则。请把真实要写的标题、原始素材…

作者头像 李华
网站建设 2026/10/14 6:44:46

Cookie安全四大配置项:Secure、HttpOnly、SameSite与Domain实战指南

1. 为什么今天还必须聊Cookie安全——它不是“小饼干”&#xff0c;而是身份凭证的裸奔现场你有没有遇到过这样的情况&#xff1a;刚在某购物平台下单付款&#xff0c;转头就收到几条精准到商品型号的推销短信&#xff1b;或者登录一个老账号时&#xff0c;系统突然提示“检测到…

作者头像 李华
网站建设 2026/10/14 6:44:30

S7-1200 PLC流水灯控制系统设计与仿真全流程实战解析

流水灯这题目&#xff0c;乍看像是单片机课的入门作业&#xff0c;但放到PLC里做&#xff0c;味道完全不一样。最近好几份《可编程控制器原理与应用》的综合实验报告都选了“基于S7-1200的PLC流水灯控制系统设计与仿真”这个题目。我接手这类问题时发现&#xff0c;它几乎把S7-…

作者头像 李华
网站建设 2026/10/14 6:44:26

JSON转Sketch:手写插件实现设计稿自动化导入与生成

简介&#xff1a;面向Sketch设计师与前端开发者的实验性插件json-sketchapp&#xff0c;可将JSON文件直接转换为Sketch可编辑的设计文件&#xff0c;解决从数据源快速生成UI设计稿、实现设计自动化的问题。压缩包共9个文件&#xff0c;以JSON配置、JavaScript脚本为主&#xff…

作者头像 李华
网站建设 2026/10/14 6:44:17

听到某个人说话就烦躁?声音敏感性触发的心理机制与应对策略

你有没有这种时刻&#xff1a;明明对方没说什么过分的话&#xff0c;但只要一开口&#xff0c;你心里的烦躁就“腾”地一下烧起来。越是想压&#xff0c;越压不住&#xff0c;最后可能连对方正常说话都变成一种折磨。这种现象在心理学上有个很常见的名字——声音敏感性触发&…

作者头像 李华