
最近在梳理面试辅导素材的时候我又翻出了这批逻辑思维题。说实话程序员这个群体对逻辑思维题的态度一直很分裂有人认为这东西跟日常工作八竿子打不着有人则在刷题和反复推演里练出了一套“建模式思考”的习惯。我属于后者——刷算法题练的是“拿到明确输入输出实现解法”的能力而逻辑思维题练的是另一种东西在条件残缺、信息矛盾、甚至看起来无解的场景里建立模型并推出一条可靠结论的能力。这两件事在实际工程里都缺一不可。这是“程序员逻辑思维题拆解”系列的第二篇。上一篇我讲了刷逻辑题的整体思路以及一些偏基础入门的题型。这一篇我直接挑了几类出现频率最高的经典题型每一道都会给完整的推理路径再用程序员熟悉的代码思维去映射一遍。你会发现很多“烧脑题”本质上就是一个布尔表达式、一组约束条件、或者一次状态搜索。话不多说直接进入正题。1. 真假话推理两道题练会“布尔取反”真假话问题是逻辑思维题里最经典的题型之一几乎每个题库都会收。这类题目表面上是文字游戏本质上是离散数学里的布尔逻辑。1.1 经典岔路口题怎么一句话问出正确方向题目场景是这样的你站在一个岔路口面前有两条路一条通往安全的目的地一条通往危险区域。路口站着两个人其中一个只说真话另一个只说假话但你不知道谁是谁。你只能问其中一个人一个问题怎么问才能确定哪条路是安全的很多人第一反应是问“你是说真话的人吗”这个问题两个人都会回答“是”毫无区分度。还有人会问“哪条路安全”说真话的人指对说假话的人指错你还是无法判断。正确答案是随便选一个人指着任意一条路问“如果我问另一个人‘这条路是否安全’他会怎么回答”如果对方回答“是”那么这条路不安全如果回答“不是”那么这条路安全。我用真值表拆一遍你就明白为什么了。这里假设“安全”为真“危险”为假实际路况被问者身份被问者的回答答案指向安全说真话者不安全否代表安全安全说谎者不安全否代表安全危险说真话者安全是代表危险危险说谎者安全是代表危险无论你问到的是谁只要对方的回答是“是”路就是危险的回答是“不是”路就是安全的。问题的关键在于“嵌套反问”抵消了两个身份带来的不确定性。1.2 编程思维映射这就是一个XOR问题为什么这个问题对程序员特别有价值因为它可以用布尔运算精确建模。设守门人A的身份为xx0表示说谎者x1表示说真话者。设“另一条路是安全的”为yy1为真。当你问“如果我问另一个人这条路是否安全他会怎么回答”时另一个人给出的答案其实是在原始命题y上做了一次取反操作因为另一个人一定会撒谎或说真话这里的核心是“被问者回答之前脑子里已经替另一个人转了一道弯”。用代码表达就是被问者最后的回答值等于not y这里“回答的是‘是’”代表结果值为真。而这条公式恰好不包含x说明无论问谁答案都只取决于路况本身。这就是布尔代数里“双重否定抵消”的直观表现。我试过用这个思路去追查线上一个偶发 bug一个请求经过两个中间层时各自的成功/失败返回逻辑都各带一次不该有的取反最后表现为“明明接口正常上报的监控却显示失败”。排查到根因那天我突然意识到这跟岔路题几乎是一个模型——两个连续取反把真值翻回了原来的样子。逻辑题刷多了这种敏感度是能带到工作里的。这道题还有一个变种“三个人中一个只说真话、一个只说假话、一个随机回答最少问几个问题能确定身份”那是三值逻辑题难度直接上一个台阶建议先把基础版吃透。2. 排序约束推理找出条件里的确定项排序题在逻辑思维题里占比很高形式也多样名次排序、年龄排序、身高排序、物品排放位置等等。这类题对程序员来说本质上就是“约束满足”问题。2.1 一个“条件不足”的排序题实例我先给一道看起来很“标准”的排序题五个人参加比赛分别是A、B、C、D、E赛后得到以下信息A不是第一名也不是最后一名。B比C快但B不是第一名。D比B快。E比D快。求这五个人的名次。我当年第一次做这道题第一反应是“这不是白给吗”。先把条件2、3、4合成一条链路E比D快D比B快B比C快于是有E D B C这里的“”表示名次更靠前。A不是第一也不是最后。在这个链路里E是这条链上最快的A又不是第一那第一名只能是谁如果按这个方向推E必须是第一。但条件里并没有“E不是第一”这个限制所以一切顺利。再看最后一名。这条链里最慢的是CA又不是最后最后一名只能是C。现在确定E第一、C第五剩下第二、第三、第四名由A、D、B三个人占据。已知D比B快那么D一定在B前面。但A的位置完全没有限制于是出现多种可能A第二、D第三、B第四或者D第二、A第三、B第四。这道题的标准答案不是唯一的。很多题集里会强行给一个答案但如果你真的做约束分析会发现它不成立。这恰恰是我今天想讲的重点逻辑题里“推不出来唯一解”也是一种结果程序员最忌讳的就是在约束不足的情况下强行拍一个答案。用工程类比就是你的SQL查询带了一堆WHERE条件但遗漏了关键过滤项返回的结果集自然比预期大。此时正确做法不是硬从结果里挑一个“看起来顺眼”的而是回头检查约束是否完整。2.2 增加约束后如何锁定唯一答案那怎么才能让答案唯一我们只需要在原有基础上增加一个条件比如“A比D快”已知链路E D B C现在A D三个人A、D、B的排序就变成A D B。于是最终唯一名次是E第一、A第二、D第三、B第四、C第五。你看一个约束的加入直接让结果从“三种可能”收敛到“唯一答案”。在做这类题时我的个人习惯是先把所有条件翻译成偏序关系再找“确定项”——所谓确定项就是被条件卡得死死的位置比如这里的E和C。确定项抓出来之后剩下的就变成“在剩余空格里填排列”的问题。做题的时候有个小技巧遇到排序题先别急着心算拿笔画一张空位图五位就画五个横线。每推出一个确定位置就填进去。我见过很多人在排序题上栽跟头不是逻辑不会推而是全靠脑内模拟条件一多就串。这类约束推理能力和需求评审是相通的。产品给你提需求时经常会有隐藏条件缺失的情况如果你不主动去“补条件”最后实现出来的方案大概率方向有偏差。逻辑题练出来的习惯能让你本能地去追问“你这句话的边界条件是什么”3. 数字规律推理别硬猜用差分和假设检验数字规律题看起来像数学题但它考察的更多是归纳能力、模式识别能力和排除能力。数列题的答案本质上不是一个“计算值”而是一个“最合理的模式延伸”。3.1 数列题的基本分析方法先看最基础的一类。下面这些数列答案基本是送分的1, 1, 2, 3, 5, 8, ?2, 3, 5, 7, 11, ?1, 4, 9, 16, 25, ?1, 2, 6, 24, 120, ?第一题是斐波那契数列答案是13。第二题是质数列答案是13。第三题是平方数列答案是36。第四题是阶乘数列答案是720。从这几道题你能总结出最基本的数列分析方法先看是否常见定义斐波那契、质数、平方、立方、阶乘然后看“相邻项的关系”是加、减、乘、除还是混合。如果一眼看不出规律我推荐列差分表。比如斐波那契数列1, 1, 2, 3, 5, 8的一阶差分是0, 1, 1, 2, 3又重现了一组类斐波那契结构而平方数列1, 4, 9, 16, 25的一阶差分是3, 5, 7, 9这是公差为2的等差数列。差分表能帮你把隐藏的等差、等比关系显式化。还有一类数列需要“跳着看”也就是拆开奇数位和偶数位分别分析。比如1, 2, 4, 4, 9, 6, 16, 8奇数位是1, 4, 9, 16偶数位是2, 4, 6, 8两套规律并行。做数列题要注意一点很多题的规律并不唯一任何人都可以为同一个数列构造出无穷多种“合理”的延伸。所以这类题的正确姿势是“选出出题人最想要的规律”而不是“证明自己的规律唯一正确”。这和需求评审一样——不要钻牛角尖优先理解对方意图。3.2 外观数列一道考察观察力而非计算力的题我觉得数列题里最有程序员气质的是“外观数列”Look-and-say sequence它就是LeetCode上那道Count and Say。题目是这样的给定第一项是1后面的每一项都是“对前一项的描述”。1可以读作“1个1”所以第二项是1111读作“2个1”所以第三项是2121读作“1个2、1个1”所以第四项是1211。继续推问第六项是多少。第一次看到这题的人很多会往数学规律上想——斐波那契指数结果发现都对不上。其实这个数列的生成规则不是“算”出来的而是“读”出来的第一项1第二项111个1第三项212个1第四项12111个2、1个1第五项1112211个1、1个2、2个1第六项3122113个1、2个2、1个1用代码实现这个生成过程恰好就是一次字符串扫描def look_and_say(s): result [] i 0 while i len(s): count 1 while i 1 len(s) and s[i] s[i 1]: count 1 i 1 result.append(str(count) s[i]) i 1 return .join(result) s 1 for _ in range(5): s look_and_say(s) print(s)运行结果是11、21、1211、111221、312211和手推完全一致。这道题我特别喜欢推荐给工作三五年的人做因为它的陷阱在于“用惯性思维套公式”但真正的解法是审视题目本身的操作定义。日常开发里很多被过度设计的功能根因就是没有先理解业务规则而是直接往自己熟悉的技术方案上套。4. 状态搜索型题目当逻辑题变成算法题有一类逻辑题看起来是智力题其实是计算机科学里的状态搜索问题。这种题对程序员来说特别有“主场优势”因为你完全可以用BFS、DFS或者贪心策略去求解。4.1 狼羊菜过河与状态空间先说最经典的一道农夫要带狼、羊、菜过河船每次只能载农夫和其中一样东西。如果农夫不在场狼会吃羊羊会吃菜。问农夫怎么安全地把三样东西都运到对岸。刚接触这道题的人喜欢硬想步骤但用程序员的视角看这是一个标准的状态空间搜索问题。农夫、狼、羊、菜各自有两个位置左岸或右岸所以总状态数只有2的4次方也就是16种。我们需要在这16种状态中找到从初始状态都在左岸到目标状态都在右岸的合法路径。合法状态的定义很简单狼和羊不能在没有农夫看管的情况下共处一岸羊和菜同理。写成代码就是from collections import deque def valid(state): farmer, wolf, sheep, cabbage state if wolf sheep and farmer ! wolf: return False if sheep cabbage and farmer ! sheep: return False return True def solve(): start (0, 0, 0, 0) goal (1, 1, 1, 1) queue deque([[start]]) seen {start} while queue: path queue.popleft() state path[-1] if state goal: return path farmer, wolf, sheep, cabbage state # 农夫可以单独过河也可以带一个同岸的对象过河 moves [(1 - farmer, wolf, sheep, cabbage)] if wolf farmer: moves.append((1 - farmer, 1 - wolf, sheep, cabbage)) if sheep farmer: moves.append((1 - farmer, wolf, 1 - sheep, cabbage)) if cabbage farmer: moves.append((1 - farmer, wolf, sheep, 1 - cabbage)) for next_state in moves: if valid(next_state) and next_state not in seen: seen.add(next_state) queue.append(path [next_state]) return None path solve() for step in path: print(step)BFS跑出来的最优解是7步过程如下农夫带羊过河农夫空船返回农夫带狼过河农夫带羊返回农夫带菜过河农夫空船返回农夫带羊过河每一步之后左岸和右岸的物资组合都是安全的。这个解法巧妙在第4步把羊带回来是因为必须腾出空间让狼和菜单独留在对岸的同时处理好“羊的归属”问题。这道题给程序员最大的启发是遇到复杂流程问题时不要试图用脑内模拟所有可能路径先把状态抽象出来把非法条件定义清楚然后交给搜索算法处理。把问题形式化比“硬想出来的聪明解法”可靠得多。4.2 过桥问题的贪心陷阱另一道类似的经典题是四人过桥A、B、C、D四个人过桥速度分别是1分钟、2分钟、5分钟、10分钟。桥每次最多容纳两人只有一个手电筒过桥时必须有人携带手电筒过桥用时按较慢的人计算。问四个人全部过桥最少需要多少分钟。最常见的第一反应是“让最快的人来回送手电筒”A和B过桥2分钟A返回1分钟A和C过桥5分钟A返回1分钟A和D过桥10分钟总用时19分钟。但这并不是最优解。更优策略是A和B过桥用时2分钟A返回用时1分钟C和D过桥用时10分钟B返回用时2分钟A和B过桥用时2分钟总用时2 1 10 2 2 17分钟。关键在于让最慢的两个人10分钟和5分钟一起过桥从而把“10分钟”的成本只付一次而不是让10分钟的人分别和两个快的人过桥导致10分钟的成本出现两次。这就是局部最优和全局最优的冲突。从代码层面看这其实是一个动态规划或最短路径问题每一步的“桥上有谁”都是状态手电筒位置也是状态最终目标是所有人过桥且手电筒在对岸。理解了状态定义你就不会再被直觉答案带偏。这类题刷多了之后你再看复杂系统的性能调优会多一层“全局视角”某个模块单独优化到极致不代表整个链路最优有时候让慢任务合并执行、牺牲一点局部效率整体延迟反而更低。5. 常见问题与避坑清单逻辑题刷多了我发现大家在刷题和面试中踩的坑其实高度重合。整理一份高频问题清单权当“排雷手册”。5.1 答题时的五个高频坑第一忽视否定词。题干里的“不是第一”“不在左边”“所有人都没迟到”这类信息经常在快速阅读时被大脑自动忽略。我的建议是读题时直接把否定信息转成布尔表达式比如“A不是第一”转成A ! 1减少歧义。第二假设分支混在一起。做假设推理时很多人会在纸上写着写着就把不同假设的推论混到一个结果里。正确的做法是每个假设单独展开用列表或树形结构维护状态遇到矛盾就把该分支划掉不要在同一行里改来改去。第三条件不足时硬凑答案。前文那个排序题就是典型例子。给出的条件明明不足以确定唯一解却有相当多的人会脑补一个条件把答案“补”出来。面试官如果问“你能确定唯一答案吗”你要敢说“不能现有条件不够”这体现的是严谨性。第四真值表漏行。真假话问题里如果你在推理时漏掉了“问到说谎者”和“问到说真话者”的其中一种情况结论很容易出错。老老实实把四种组合写全是最稳妥的办法。第五把数列题当数学题硬算。很多数列题的逻辑是文字规则比如外观数列不是纯数学规律。遇到乍一看没有切入点的数列重新读一遍题干想一下“这个数列的生成动作是什么”而不是直接做差分。面试或笔试中遇到这些题建议先花30秒把条件和已知信息结构化不要急着出答案。我见过太多人开口就答答完之后自己推翻反而不如那些“想清楚了再说”的候选人。5.2 如何把刷题收获落进日常工作逻辑思维题不是刷完就完里面的思考方式可以迁移到实际工作里。我自己感受最深的三个迁移点一是需求分析阶段先列约束条件用布尔表达式翻译自然语言能提前发现产品需求里的自相矛盾二是排查故障时用真值表检验假设把每个可疑因素单独置真置假避免被“三个嫌疑同时存在”的复杂场景绕晕三是在方案评审时主动考虑约束不足的情况学着判断当前信息量是否能支撑唯一结论如果支撑不了就明确找产品要“追加条件”。我个人在实际刷题过程中还有一个习惯每道题做完之后用一句话总结“这道题考的是什么能力”。比如“假话者问题考的是取反逻辑”和“过桥问题考的是全局成本”下一次遇到同类题目时你会发现自己识别题眼的速度明显变快。这个方法推荐给大家比按数量刷题有效得多。这期的题目整体比上一篇难一些适合有一定刷题基础的人慢慢啃。逻辑思维的提升靠的不是记住某一题的答案而是反复练习“把模糊问题翻译成精确模型”的肌肉记忆。看完之后如果觉得哪道题的解法跟你想的不一样建议自己动手推一遍或者把代码跑一遍——手和脑一起用比单纯用眼记扎实太多。