单身狗算法题解:从异或位运算到稳定匹配的编程思考 又是一个人的节日夜。群消息里成双成对的合照和红包我都没点开习惯性登录洛谷翻到题解区盯着题目列表里那一个个等着被找出来的落单数字忽然想写一篇“单身狗题解”。在算法题解的世界里“单身”从来不是贬义词。它是题面里“只出现一次”的元素是稳定匹配算法中主动方被拒绝后重新入队的一轮试探也是每个独自坐在电脑前、对着WA报错反复调试的人最诚实的底色。这篇东西我早就想写了一直拖到今天才动笔。适合谁看适合那些一个人刷题、一个人复盘、一个人把题解写到深夜的程序员和竞赛选手也适合所有好奇“算法视角下的单身到底是什么样”的人。放心我不是来灌鸡汤的。下面讲的都是我在LeetCode、洛谷和ICPC题解区泡出来的真实观察外加几道和“单身”“配对”高度相关的算法题它们本身就是很好的题解素材。1. 为什么最后是我成了那个写题解的单身狗很多朋友问过我同一个问题你们搞算法竞赛的为什么单身率这么高这个问题其实很好回答。一场正经的区域赛要打一整个下午赛后还有补题复盘日常训练一天少说四小时周末直接翻倍。把一个小伙子的时间表摊开来看白天是数据结构和图论晚上是动态规划和数论睡前还要刷一遍排名和题解区。剩下那点精力连点开社交软件都觉得浪费。另一个原因是圈子结构。算法竞赛这个圈子的性别比例大家心里都有数。我在机房里一待就是三年多一起调试、一起比赛的搭档全是清一色的男生。当你的社交图谱里全是代码同好时单身的概率确实会被环境拉高这没什么不好意思承认的。还有一个容易被忽略的点写题解会让你的沟通方式越来越“讲逻辑”。遇到分歧你先想到的是“来我们看看谁的推导有问题”。可情绪场景里对方要的不是复杂度证明而是被理解。你递过去一篇正确性证明在WA面前你是对的在感情面前你直接RE。但我现在不太想把这些事讲得很惨。单身对我最大的好处是整块的时间。一个人吃饭不用等周末不用陪深夜写题解不会有人催你睡觉。把三五年时间投进同一件事成长是肉眼可见的。我认识有人靠一摞刷题记录和题解在校招季拿到心仪offer也有人用几年的独处把Codeforces打到了很高段位。单身不是原因是结果决定你去向的是这段独处时间被用来做了什么。写题解也一样写给自己看的时候你是唯一的读者坚持写下去它总能变成别人也能读懂的东西。2. 从LeetCode 136说起落单的那个元素是你也是我2.1 异或解法成对的会湮灭落单的留下先请出我最喜欢的一道入门题LeetCode 136Single Number。题面很短给定一个非空整数数组除了某个元素只出现一次以外其余每个元素都出现两次。找出那个只出现一次的元素。要求线性时间复杂度并且不使用额外空间。我第一次做这题时第一反应是哈希表扫一遍统计次数再扫一遍找奇数。O(n)时间O(n)空间工程上没问题。但题目要的“不使用额外空间”才是关键。这时候就轮到异或上场。异或运算三句话能讲完a ^ a 0自己和自己异或等于0a ^ 0 a和0异或等于自己异或满足交换律和结合律顺序随便。把整个数组的所有数异或一遍成对的数字会像正负粒子对撞一样相互抵消变成0最后剩下的就是那个从头到尾孤零零的元素。四行代码解决int singleNumber(vectorint nums) { int ans 0; for (int x : nums) ans ^ x; return ans; }面试时我问过不少人这道题能秒答的不算少但能讲清楚“异或为什么能做到O(1)空间”的没几个。代码短背后的位运算思想可不廉价。2.2 进阶变体137与260的延伸这题还有很多亲戚。LeetCode 137除了一个元素只出现一次其余都出现三次。异或在这个场景下会失效你要按二进制位统计每一位上1出现的次数再对3取模模完剩下的位就是那个落单者的二进制表示。LeetCode 260有且只有两个落单数字其余都成对。解法是先把全员异或得到两个数字的异或值xy因为这两个数字不同xy至少有一位是1取最低的那个1位lowbit xy (-xy)把原数组分成两组——成对的数字在分组后依然成对不会干扰结果——两组分别异或就同时揪出a和bvectorint singleNumber(vectorint nums) { int xy 0; for (int x : nums) xy ^ x; int lowbit xy (-xy); int a 0, b 0; for (int x : nums) { if (x lowbit) a ^ x; else b ^ x; } return {a, b}; }lowbit这个技巧也是树状数组的看家本领取出二进制最右的1往往能把一个集合干净地劈成两部分。为什么拿这题当“单身狗题解”的第一题因为戏剧性够强整个数组都在成双成对地出现只有那一个数字独自出现了一次。异或甚至不给它辩解的机会所有相遇都抵消所有同行都清零它却依然留在结果里。这大概就是单身狗在人群中的状态你存在过你没有缺失你只是没有被配对。算法告诉你这不影响你成为那个“答案”。3. 稳定匹配问题配对题里最经典的一份答卷3.1 问题定义什么才算“稳定”如果说“单身”是一道题那稳定匹配问题就是题解教材里的开篇例题。1962年由Gale和Shapley提出的稳定匹配Stable Matching后来成了市场设计研究的基石之一相关工作拿了诺贝尔经济学奖。问题本身模型化得很干净有n个主动方和n个接受方每个人对另一方阵营的每个人都有一个完整的偏好排序。要构造一个完美匹配且保证稳定。所谓稳定就是不存在这样两个人——我这里用“主动方”和“接受方”来称呼谁看了都能代入——互相在偏好榜上把对方排得比现任更靠前。只要存在这种组合他们就有“私奔”的动机匹配就不稳定。3.2 算法流程求偶、比较、换人Gale-Shapley算法流程朴素到不像一个拿了诺奖级别的思想所有主动方进入单身队列每轮取队首主动方按他自己的偏好顺序向排名最高且还没求过婚的对象发起一次求婚对方如果单身暂时接受对方如果已经有搭档就对比新人和现任谁在偏好榜上更靠前就选谁被比下去的那位重新恢复单身回到队列里继续试重复2和3直到所有主动方都脱单。为什么一定能结束每一轮求婚都固定消耗一对“主动方-接受方”组合的首次尝试这样的组合一共n^2个所以最多n^2轮队列一定会空。为什么一定稳定反证如果最终匹配里还存在不稳定对那主动方在算法过程中一定向对方求过婚而对方没有选他最终选的搭档在偏好榜上理应更靠前矛盾假设不成立。3.3 一个3×3的小规模手工模拟光讲流程有点干我跑一个3×3的例子。三位主动方A、B、C三位接受方X、Y、Z偏好如下主动方偏好顺序接受方偏好顺序AX, Y, ZXB, C, ABY, X, ZYA, B, CCX, Z, YZA, C, B按队列顺序严格执行第1步A向X求婚X单身先收下。 第2步B向Y求婚Y单身先收下。 第3步C向X求婚。X在现任A和新人C之间比较X的偏好是B C A于是X接受CA被甩回队列。 第4步A向Y求婚。Y现在和B在一起但Y的偏好是A B C于是Y换人接受AB被甩。 第5步B向X求婚。X现在和C在一起但X的偏好是B C A于是X再次换人接受BC被甩。 第6步C向Z求婚。Z单身接受。最终匹配是A-Y、B-X、C-Z。逐个检查所有潜在不稳定对你会发现没有任何一对互相更青睐的组合存在结果是稳定的。3.4 主动方占优最反直觉的一条结论这个算法还有一个值得玩味的结果主动权在谁手里谁就吃最大红利。主动求婚的那一方最终得到的是全体稳定匹配中他们最满意的一个被动等待的那一方得到的则是全体稳定匹配中他们最差的一个。我第一次读到这个结论时后背发凉。它用数学语言告诉你在双向选择里早早把自己的偏好排序理顺、主动出手的人占优被动等着被挑选哪怕最终配对成功也可能拿到最不理想的那份选项。现实当然比这个模型复杂一万倍偏好会变、信息会缺、外部约束一堆但这个结论至少是一句有用的提醒单着的时候别光等安排先动手构建自己的序列。行动本身就在改写结果。4. 如果把脱单当成算法题我试过的四种建模4.1 贪心每次选当前最优崩得非常稳定我第一反应是贪心每次遇到当前评分最高的人就锁定不考虑后续。这个策略在现实中表现为两种极端要么遇到一个自认为满分的人直接梭哈错过后面更契合的要么永远信奉“下一个更好”把候选池耗干最后两手空空。贪心错在默认选择之间彼此独立。可配对问题的本质是双向选择——你在选别人时别人也在按自己的偏好选你。一个局部最优的进攻会因为对方的偏好排序而整体失效。4.2 动态规划转移方程写得出来数据集一塌糊涂我也认真建过动态规划模型比如经典背包式转移dp[i][j] max(dp[i-1][j], dp[i-1][j-1] value[i])i是认识的人数j是接受过的关系数value是综合评分。形似得很一实测就穿帮两个人的关系价值根本不是两个独立value相加而是一个非线性函数。情绪支持、生活习惯、长期目标每一项都会重塑总分。动态规划要求子问题最优能组合成全局最优这个模型里连状态定义都站不稳。它的WA不是代码写错是建模阶段就错了方向。4.3 爆搜与剪枝状态空间大到连枚举的机会都没有再暴力一点的思路是枚举所有可能的见面顺序用“不如当前最优就回溯”做剪枝。n小的时候能跑n过两位数就是n!级别直接爆炸。更何况现实中你连完整的候选集合都拿不到搜索算法需要全局信息现实给你的永远是残缺的局部视图。想靠深度优先遍历人生没等搜到答案先把自己TLE了。4.4 在线算法与37%法则最优停时只活在理想假设里理论上最靠谱的其实是在线决策视角。经典的秘书问题给出一个漂亮结论如果候选人是随机顺序到达你只能当场决定接不接受最优策略是先拒绝前37%的人当作参照系之后一旦遇到比之前所有人都好的果断拿下。这个策略能把选中最佳候选人的概率维持在约37%已经是最优。听着很科学但它的前提苛刻到几乎不可能在现实复现到来顺序得随机决策不可撤回而且价值判断要完全一致。所以我更愿意把它当决策框架不是操作手册。它真正想说的是初期多看少动先把参照系建好中期碰到明显高出参照系的就别优柔寡断。我见过太多单身的焦虑要么出自参照系还没建立就想出手要么出自参照系早已建立却始终不敢出手。把这四种模型放在一起看你会发现问题全出在同一个地方所有模型都在假设人是可观测的而现实交互的核心就是信息缺失。建模再漂亮喂进去的也是残缺数据。因此我后来的态度反而简单了——把确定性留给刷题和写题解把不确定性留给生活。该WA就WA该RE就RE。单身不是一个需要被优化的bug而是一个尚未匹配状态下依然合法的解。5. 写题解的实操心得怎么把一道题从WA憋成有人看的文章5.1 动笔之前先过自己这一关既然这篇也叫“题解”最后聊聊题解本身。我在洛谷和LeetCode看过不下几百篇题解自己也写了不少被夸过也被骂过。题解的神奇之处在于表面上是把解法写出来实际上是逼着大脑把思考流程重新跑一遍。能写出清楚题解的人不一定是最强选手但他一定把题目吃透了。我的习惯是给每道认真做过的题建一个草稿先写给自己看隔几天再回读。如果连自己都看不懂当时的推导那说明那次AC不过是玄学。做对一道题和讲清一道题之间隔着一条鸿沟写题解是唯一的桥。5.2 我最常用的六段式结构一篇信息量完整的题解我一般按六段走题目大意用自己的话复述题面这一步最能暴露你对约束条件理解有没有漏错误想法与卡点把最初那个错误直觉写下来读者大概率会掉进同一个坑正确思路从暴力到优化的完整推导不一步跳到最优解正确性证明与复杂度分析不证明等于没学会复杂度不清楚等于白算代码与注释变量名直白注释里不写“显然”更不贴无意义的大段模板易错点与测试数据边界、溢出、重复输入、空集这些才是后来人最需要的部分。有了这六段文章自然有骨有肉。很多人写题解只是贴代码那叫代码备份不叫题解。5.3 对拍、复杂度与评论区写题解最容易踩三个坑只贴代码不解释上来就甩最优化结论复杂度分析含糊。尤其是复杂度均摊摊在哪、空间有没有算递归栈都要写明白。我特别想强调对拍的价值。所谓对拍就是写一个暴力程序当裁判再去和你的优化程序比赛随机生成海量小数据两边同时跑结果一致才算通过。小数据下暴力不会超时所以它可靠。很多人高呼“玄学AC”多半只是没对拍、没造边界靠测试数据太弱蒙混过关。对拍这名字听起来孤独实际上是我在深夜最信任的工具。还有一条容易被忽略的经验题解区的评论区往往比正文还有价值。有人会贴出更强做法有人会问到你完全没想过但很关键的边界有人一句话把你绕了三页纸的东西点透。我现在写完题解有个习惯隔三天读一遍评论把有价值的意见补进文章。这个过程很像在独处时写日记但发出去之后你会发现一段落单的思考其实能遇到很多同行者。6. 一个人的训练场也可以长出答案回到开头那个问题为什么最后是我成了那个写题解的单身狗答案已经分散在前面每一章我只是把绝大多数时间投在了一件事上。这说不上悲惨也说不上光荣只是一种选择。算法题给我的不止是解题技巧它教我在什么都没把握时也能动手教我把大问题拆成可计算的小问题教我在连续WA之后不要推倒一切重来——先加一行输出看看中间量到底长什么样。如果你也是一个在深夜里对着题解区发呆的人我建议你做一件小事每周挑一个没做过的中档题认真写一篇只有自己能看懂的题解写完关掉页面不看点赞不看评论。坚持一年再回头打开这些文章你会看到一条完整的技术成长曲线会看到某个深夜的自己是怎么一点一点把乱麻捋成答案的。这种真实感比任何社交动态都扎实。至于感情它不是一个标准在线算法问题我至今也没给出最优解。但在刷题里我学会了一件事提交慢了会超时犹豫久了会错过但只要你还坐在键盘前下一题永远可以做。单身狗也好题解也罢落单的时间并不是空白的——它只是正在被编译成某个未来时刻能运行的版本。最后说点个人体会。写这篇“单身狗题解”的过程中我没觉得惨反而觉得踏实。因为WA、TLE、AC这些状态从来不骗人你投入多少调试时间它就回报你多少正确性。生活不承诺复杂度上界但电脑前你至少能跑出一个自己的答案。愿每个一个人写题解的夜晚最后都能编译通过。