你有没有过这种感觉:一个看起来特别简单的问题,你写了三版代码,每一次都感觉“这次肯定对了”,结果一跑,要么答案离谱,要么直接超时。你盯着屏幕,开始怀疑是不是电脑在跟你开玩笑。
今天要说的这道题,就是这样一个“简单陷阱”。题目叫“生成括号”,给你一个数字 n,让你写出所有合法的括号组合。比如 n=3,正确答案是:((()))、(()())、(())()、()(())、()()()。听起来不难,对吧?我也这么觉得。直到我打开编辑器,写下第一行代码。
第一次尝试,我的思路直来直去:只要长度还不够 2*n,就随便加左括号或右括号,把所有可能都列出来。结果输出了一堆乱七八糟的东西,什么 “((((((” 都出来了,完全没有任何规则感。那一刻我才意识到,生成括号根本不是“随便加”这么简单。合法的括号序列,必须保证在任何前缀里,右括号的数量不能超过左括号,而且最后两者要相等。我忽略了这一点,代码自然就放飞自我了。
第二次,我加上了计数器,一个记左括号用了几个,一个记右括号用了几个。心想:这下总该控制住了吧?于是,只要左括号数量没到 n,就继续加左括号;只要右括号数量没到 n,就继续加右括号。结果,程序直接跑到停不下来,连一个测试用例都过不了。我盯着屏幕上的 Time Limit Exceeded,脑子里全是问号。后来慢慢才想明白:我只控制了总数,却没控制顺序。我可以一直加右括号,哪怕前面一个左括号都没有,这样生成的序列从一开始就是非法的,但我的代码还在傻乎乎地往后跑,白白浪费计算。那种感觉,就像明明看着地图走,却总在同一个路口转圈。
第三次,我终于拿出了最笨但最稳妥的办法:把所有长度为 2*n 的序列都生成出来,然后一个一个检查它合不合法。检查的方法也简单,拿一个计数器,见到左括号就加一,见到右括号就减一,如果计数器到过负数或者最后不是零,就直接淘汰。这一版跑出来了,n=3 的输出完全正确,运行时间 17 毫秒。但我知道,这不是最优解。它先生成了大量注定不合法的序列,再挨个过滤,就像你要从一屋子混在一起的豆子里挑出红豆,先得把豆子全倒出来,然后再一颗颗挑。虽然结果对了,但看着别人那些更干净、更快的写法,心里还是有点不甘。
这时候,我才真正去看那些被很多人反复提到的“标准解法”。一个仅用深度优先搜索、连过滤函数都不需要写的版本,0 毫秒就出结果。它的思路特别简单,但特别精准:在递归的每一步,只有当左括号数量还不到 n 时,才加左括号;而右括号呢,只有在当前左括号数量已经比右括号多的时候,才允许加。也就是说,它从一开始就不会走上非法路径。你每走一步,都确保余下的路一定是通往合法终点的。这种写法没有一丁点浪费,就像有一个隐形向导,在你每次要犯错之前轻轻把你拉回来。
还有一个更少见的解法,跑出来只要 6 毫秒,据说只出现在 0.21% 的提交里。它用的是更细致的剪枝逻辑,连一些合法的但生成顺序不够优化的分支都提前避开了。看到这些,我才突然意识到,这道题教会我的远不止回溯算法本身。它更像是一个隐喻:在生活里,我们很多时候也像那个第三次尝试的程序,只顾把所有的可能性都试一遍,再花力气去筛选对的。但如果早早想清楚规则,在念头冒出来的那一刻就问自己一句——“这个选择,会不会让接下来的每一步都更难走?”——可能根本不用白费那么多力气。
从一个简单的括号生成,到看见自己在错误中绕圈,再到被别人的优雅解法轻轻点醒,这也许就是坚持打卡 250 天的意义:不是为了写出最完美的代码,而是为了在这些实打实的错误里,越来越快地认出那条不绕远路的方向。
热门跟贴