这几天在弄数据结构的课程设计,选了个经典题目:N 皇后问题。在一个 N×N 的棋盘上摆 N 个皇后,谁也不能吃谁——不同行、不同列,也不能在同一条对角线上。题目本身不难描述,一句话就能讲清楚,可真正坐下来写代码的时候,我才发现自己对"递归"这两个字,其实一直是一知半解的。

卡住我的那一步
写 solve_n_queens 函数的时候,我在一个很傻的问题上卡了很久:递归回来之后,怎么把刚才放的那个皇后"撤销"掉?我脑子里一直想着要不要专门写一行代码,把 board[row] 清空,或者标记成"未使用",不然下一次尝试的时候岂不是会被上一次的痕迹干扰?
我在草稿纸上把 4 皇后的搜索过程一步一步画出来,第 0 行放第 0 列,进入第 1 行,试了几列都冲突,函数返回;回到第 0 行,换到第 1 列,再往下走……画着画着我突然意识到:根本不需要那一行"清空"的代码。函数一返回,就自动回到了上一层的 for 循环,下一次循环会直接把 board[row] 覆盖成新的值。所谓"回溯",不是我需要主动去做的一个动作,而是函数调用栈展开、收拢时自带的效果。
这个瞬间挺奇妙的——原来课本上那句"回溯就是撤销选择,尝试下一种可能",不是一句需要我去实现的话,而是递归本身自带的性质。想明白这一点之后,再看 is_safe 里那个 abs(board[i] - col) == abs(i - row) 的对角线判断,忽然也顺眼了很多。

一条不起眼的警告
代码写完,跑起来一切正常,N 从 1 试到 10,输出的解的总数——1、0、0、2、10、4、40、92、352、724——跟网上查到的标准答案分毫不差,我当时挺得意的。
直到编译的时候瞥见一行警告,才发现自己高兴得太早了:
warning: format '%d' expects argument of type 'int',
but argument 2 has type 'long long int'
我用 long long 定义了 solution_count,是为了防止 N 比较大时解的数目超出 int 的范围,可是最后打印的时候,我图省事直接写了 %d。结果是能跑,数字也是对的——但这是"恰好"对的。因为在我的电脑、我用的编译器上,参数传递的方式凑巧没出问题,换一个平台或编译器,这行代码完全可能打印出一堆乱码。
这件事让我对"对的"这两个字多了一层警惕。以前觉得程序跑出正确结果就算完成任务了,现在会多问一句:这个结果是"必然"对,还是"碰巧"对?后者往往才是最容易在别人的机器上翻车的地方。

数组为什么这么"聪明"

这道题另一个让我觉得有意思的地方,是存储结构的选择。一开始我下意识地想用二维数组表示整个棋盘,每个格子标记有没有皇后,感觉更"直观"。但仔细一想,每一行只能放一个皇后,用二维数组去存,一大半空间都是浪费的,而且判断冲突还要去扫描一整个二维平面。

后来换成一维数组 board[row],用它记录"第 row 行的皇后在第几列",整个问题突然变得清爽起来:判断同列,只是比较两个数字是否相等;判断同对角线,也只是比较两个数字的差的绝对值。原本感觉需要"遍历棋盘"才能完成的操作,变成了几行简单的算术比较。
这让我第一次真切体会到,"数据结构"这门课讲的不只是"数组、链表、树"这些名词本身,而是一种思维方式:先想清楚问题里真正变化的信息是什么,再去找一种存储方式,让这些信息之间的关系变得容易表达。选对了存储结构,算法会自己变简单。

写在最后
从需求分析、画流程图,到一行一行敲代码,再到看着编译器的警告去抠细节,这次课程设计前前后后花的时间比我预想的要长不少。但比起最后交出去的那份报告,我更在意的是那几个"卡住又想通了"的瞬间——想明白回溯不需要额外代码的那一下,看懂警告信息背后是未定义行为的那一下。这些瞬间才是真正留下来的东西,报告合上之后,它们还在。
下一次遇到需要"搜索所有可能性"的问题,我大概会更从容一点——至少我知道,回溯这件事,函数自己会替我做。

posted on 2026-09-01 16:08  chenyun_922  阅读(6)  评论(0)    收藏  举报