☰
国际象棋局面数量之谜:博弈树、状态空间与搜索剪枝的真相
2026/10/14 2:59:33 网站建设 项目流程

棋盘只有64格,棋子总共也就32颗,但如果你在开发者群里问一句“国际象棋到底有多少种局面”“为什么写不出一个逻辑正确的国际象棋程序”,评论区能吵出几百条。有人搬组合数学,有人搬复杂度理论,还有人直接说“因为棋盘是有限的,所以理论上一定能写出来,只是太慢了”。这个说法其实对了一半,而正是那没说清楚的一半,才是问题的真正核心。

这篇文章想把这些掰扯清楚:局面数量应该怎么估算、为什么估算结果和“能不能写正确程序”直接挂钩,以及在实际工程里,那些顶尖引擎又是在靠什么“不可能正确”的前提下逼近正确的。适合想搞懂博弈树、状态空间和搜索剪枝的人看,也适合自己写过一点棋类程序但总觉得“哪里不对劲”的朋友。

1. 局面数量:从“上界”到“合法局面”

1.1 先算一个极端宽松的上界:把64格当64个座位

人类对“局面”最朴素的理解,就是“棋子站在哪儿”。如果不管规则,只看摆放方式,问题可以简化成:每个格子有几种状态。棋盘上任意一格可能是空的,可能是白方六种棋子之一,也可能是黑方六种棋子之一,每格一共 13 种状态。64 个格子各自独立,于是总数就是 13 的 64 次方,约等于 10 的 71 次方。

10^71 是什么概念?一块 1TB 的硬盘能存大约 8×10^12 bit,10^71 个比特需要 10^58 块硬盘。这显然是一个夸张到没有工程意义的上界,因为它把所有看起来滑稽的排列都算进去了,比如棋盘上出现 64 个白王,比如双方棋子乱飞到不该在的位置。但它的价值在于给人一个直觉:就算只做“着色”,棋盘的花样已经爆炸了。

比这个稍微“合理”一点的算法,是给每颗棋子贴身份标签,然后往 64 个座位里放。比如最极端情况下棋盘上有 32 颗棋子,考虑“哪一颗棋子坐在哪一个格子”,排列数大约是 64^32,也就是 10^58 量级。这个数字看着小了 13 个数量级,但它依然包含大量非法布局,比如王被对面吃掉了、兵在第一排、双方同时被将军之类。这个上界的意义不是给出答案,而是告诉我们:组合爆炸从“摆放”这一步就已经开始了。

1.2 规则是一把筛子:从 10^71 筛到 10^46 附近

真正合法的局面要满足一系列条件:双方各有一个王;王不能被将军(或者说被将军的局面仍然合法,但将军方必须能回应);兵不能留在自己最后底线(正常规则下升变后就不是兵了);一方最多 8 个兵;兵不能跑到对面最后的横排还保持兵的身份;双方棋子的数量受升变和吃子影响等等。

这些规则把 10^71 这个数量级过滤到大概 10^43 到 10^50 之间。不同估算结果有差异,取决于怎么处理“轮到谁走”“王车易位权是否存在”“吃过路兵是否可用”这些附加状态。棋盘上同样的 32 颗棋子,白方行棋和黑方行棋就是两个不同局面;白王还有没有易位权,又是两个不同局面。所以局面不仅是“棋子站在哪里”,还包含双方的权利信息。这也是很多初学博弈树的人容易漏掉的一点:状态编码里必须额外存这些东西,否则同一个摆法在规则层面会被当成多个状态。

这里要澄清一个常见误解:这 10^46 只是“合法局面”的数量,不是“从开局走到终局的所有路径”的数量。合法局面是静态的,是棋盘可以呈现的可能形态;而对局路径是动态的,是从初始局面出发,一步一步走出所有可能路线。这两者完全是两套计数体系。

1.3 另一个量级:博弈树节点的数量级远超局面数量

说到路径,就绕不开那个经典的数量级估算。国际象棋平均每步可走的分支数大约在 30 到 35 之间,一局棋按双方各走 40 步来算,就是 80 层。每层 30 个分支,全部展开大约是 30 的 80 次方,约等于 10^118。这个数字常被当作“博弈树规模”的代表。

对比一下:合法局面 10^46,博弈树路径 10^118。为什么路径数能比局面数多出 72 个数量级?因为同一局面可以由大量不同路径到达。开局前三步,双方各走一步后可能已经出现重复,路径越来越多以后,不同走法顺序汇聚到同一个棋盘状态的现象极其普遍。博弈树记录的是“路径”,而合法局面统计的是“去重后的节点”。路径数是路径的计数,局面数是节点的计数。

所以,如果有人问你“国际象棋有多少种局面”,比较负责任的说法是:没有一个精确的公认整数,但主流量级在 10^46 左右。而如果问“博弈有多少条可能的对局”,那数量级又跳到 10^118 附近。这两者都大得离谱,但它们“大”的方式不一样。一个是你把棋盘摆一摆能摆出多少种合法样式,一个是你拿着规则从开局一路走下去能走出多少条不同的故事线。

2. 就算列出所有局面,程序也拿不动

2.1 一个局面最少占多少比特:不能只记棋子位置

很多人想,既然局面总数是 10^46,那每个局面给一个比特标记“这边赢还是那边赢”,总共也就 10^46 bit,听上去还不到 10^80 的宇宙原子数。可问题没那么便宜。

一个可用的局面编码,至少需要记录:每颗棋子的位置和种类、轮到谁走、双方王车易位权、是否有吃过路兵目标格、半回合计数(五十步规则需要)。如果只做静态局面列表,这些信息加起来,一个状态通常要几十比特到上百比特。别的不说,棋子在 64 个位置里的坐标就需要 6 比特,30 颗棋子就是 180 比特,还包括哪些格子是空的。用更聪明的编码可以压到 160 到 200 比特左右,但不会下降到个位数。所以一个“包罗所有合法局面、每个局面附带结论”的完美策略表,保管量至少在 10^46 个条目乘以 200 比特,这已经比“每个局面一比特”再大两个数量级。

2.2 用硬盘堆出来的天文数字

别急着换算成硬盘,我们先只看数量级。即便乐观到每个局面只存 1 比特,只要想把 10^46 个局面全部存下来,就需要 10^46 bit。一块 1TB 硬盘约等于 8×10^12 bit,所以要 10^46 除以 8×10^12,大约是 10^33 块 1TB 硬盘。

1.25×10^33 块硬盘是什么概念?一块普通硬盘就算只有 100 克,总质量也到了 10^32 千克量级。地球质量是 6×10^24 千克,也就是说需要大约两千万个地球质量的硬盘。这不是工程优化的问题,这是物理资源的问题。即便未来存储密度提升一万倍、一亿倍,也只是把“两千万个地球”变成“两百个地球”,依然不可行。

这里顺便说一句,宇宙原子总数 10^80 看着比 10^46 大得多,但这个对比没有太大意义。我们讨论的不是“能容纳这么多信息”,而是“人类有没有能力制造并维护这种规模的存储系统”。工程能力受限于能源、散热、传输速度,不可能逼近这种天文数字。

2.3 为什么压缩也救不了:信息熵是下限

最后一个看起来可行的办法是压缩。会不会合法局面之间有很大的冗余,可以用某个巧妙算法压到很小?

无损压缩的下限由信息熵决定。合法局面的分布不均匀,确实存在冗余,但压缩能压到多少,取决于合法局面的真实分布。如果合法局面个数是 10^46,那么随便一个合法局面要唯一标识出来,平均至少需要 log2(10^46) 比特,算下来大约是 153 比特。压缩可以把 200 比特左右的原始编码压到 153 比特附近,再往下就不行了,因为信息量就这么大。假如合法局面数量是 10^50,那下限更高,接近 166 比特。也就是说,压缩帮不了根本性的忙——它只能把存储需求从一个天文数字,降到另一个略小一点的天文数字。

看到这里你应该已经明白,想靠“把全部局面背下来”写一个绝对正确的程序,资金上不可行,物理上也不可行。真正厉害的引擎从来不这么干,它们走的是另一条路,但那条路也注定不是“逻辑必然正确”的路。

3. “逻辑正确”到底难在哪里:不是规则冲突,而是搜索太深

3.1 规则是有限的,但有限不等于可穷尽

国际象棋规则里,有五十步规则:连续五十步没有吃子也没有兵动,可以申请和棋。加上重复局面判定、三次重复可判和,规则的共同作用让所有合法对局的长度都有了一个理论上限。既然对局长度有限、每步分支有限,那么从固定初始局面出发的博弈树整体是有限的。既然是有限的,理论上就存在一个完美策略:任何局面都对应唯一的理论结果,要么先手有必胜走法、要么后手有必胜走法、要么双方最佳应对后必然和棋。这是博弈论里很早就被证明过的结论。

但这个结论只保证了“存在”,没保证“人类能把它挖出来”。有限树的规模是 10^118 量级,树还带着 10^46 量级的不同节点。就算每个节点只做一次简单计算,数字也已经远超任何机器的预算。有限在这里不等于可执行,就像“宇宙中原子数是有限的”不等于“你能把宇宙原子挨个数一遍”一样。

3.2 如果把棋盘放大,事情会从“很难”变成“理论上的难”

有个更容易看清问题本质的角度:把国际象棋推广到 n×n 棋盘,问“给定一个局面,能不能判断某一方有必胜策略”。这个问题被归类为指数时间完全问题。这类问题的共同点是,在可预见的算力下,最坏情况需要指数级时间才能解出来,而且短期内看不到多项式算法的希望。它不是那种“稍微优化一下就能跑”的问题,而是复杂度理论的硬骨头。

标准 8×8 棋盘没法直接套用 n×n 的结论,因为 8 是常数,常数规模的博弈当然可以枚举。但这个复杂度结论给出一个直觉:国际象棋真正可怕的部分不是棋盘大小,而是“双方策略互相博弈”这件事本身。局面数量指数级增长,搜索深度又跟着策略交互走,很快就超出了所有工程手段能覆盖的范围。

3.3 把“逻辑正确”拆成三个层次来看

标题里的“逻辑正确”其实很模糊。拆开看,至少有三种理解。

第一层是规则实现正确:程序不走出非法着法,能正确处理易位、吃过路兵、升变、将军、和棋判定。这种正确完全能做到,很多开源引擎的规则模块都经得起长期随机对弈验证,没有违反规则的现象。

第二层是决策正确:每个局面下都能走出理论最优的那步棋,让胜势不丢、败势最少输。这一层目前做不到,而做不到的原因正是第 2 节讲的:没有足够的空间存储完美策略表,也没有足够的时间在每次走棋时重新搜索完整树。

第三层是可验证正确:即使某个引擎真的每步都走出最优,我们也很难给它开一张“对任意局面都最优”的数学证明,因为它内部是启发式搜索和大量经验参数的组合,不是可以被简单推理的数学表达式。

所以“不可能写出逻辑正确的国际象棋程序”这句话,严格讲是把第二层和第三层搞混了。程序可以有完美无缺的规则实现,也可以有强大的搜索能力,但它给出的“最优”大多是基于有限深度的近似,不是数学意义上的保证。

要注意,这件事和“停机问题不可判定”并不是一回事。国际象棋的一个给定局面,在固定规则下是一个明确有限的计算问题;不存在“这个局面到底算不算合法”在逻辑上自相矛盾的情况。真正的问题不是不可判定,而是判定起来所需资源大到不现实。

4. 现实世界的引擎是怎么活下来的:近似、剪枝与记忆

4.1 极小极大搜索与 Alpha-Beta 剪枝:用聪明方式砍树

既然没法展开整棵博弈树,那就只展开一部分:设想双方都从当前局面往下走几步,每一步都假设对方会走对它最有利的棋,然后从中选出对自己最有利的一步。这就是极小极大搜索。

但完整极小极大照样指数爆炸。假设平均分支数 30,搜索 6 层需要评估 30^6,约 7.29 亿个叶子节点。单机每秒评估几百万个节点,这已经接近一台电脑的极限。Alpha-Beta 剪枝就是在极小极大基础上,发现某个分支已经不可能影响最终选择时,直接把它砍掉。在理想排序下,能把节点数压到大约 b 的 d/2 次方,也就是 2.7 万量级;实际效果受着法排序影响,通常会多不少,但已经能让同样的算力多搜出几层深度。

剪枝并没有证明“砍掉的部分不影响最终结果吗”这种哲学问题,它只是利用上下界信息,排除掉那些在当前搜索窗口内不可能被选中的分支。它依然是一种近似,只是近似得非常有技巧。

4.2 评估函数:把不可判定变成可量化

搜索到了叶子节点,程序怎么知道这个局面是好是坏?它需要一个评估函数,把棋盘状态量化成一个分数。最简单的评估是子力:后值 9、车值 5、马和象值 3、兵值 1。稍微好一点的会加上位置分:中心兵比边兵好、开局阶段王在中心危险、残局阶段王要主动参战。再往上还有兵形结构、开放线、弱格、子力协调度等特征。

这些分数不是从规则里“逻辑推导”出来的,而是从人类棋理、统计数据和大量自对弈中调出来的。换句话说,引擎对局面的判断本质上是一种经验模型。这也是为什么引擎强不等于引擎正确:它可以走出人类很难应对的棋,但没法告诉你“这一步是被数学证明的唯一最优”。

4.3 置换表、开局库和残局表:能记住的先记住

完全记忆所有局面不可能,但人类还是找到了几个“局部记忆”的突破口。

置换表是搜索过程中用到的哈希缓存。同一个局面可能从不同路径到达,算过一次之后,把它的搜索结果存进哈希表。下次再遇到相似局面,直接查表返回。它相当于给搜索引擎开了一个小型的“近期记忆”,能避免大量重复计算。

开局库则是把“最初十几个回合该怎么走”这个子问题外包给数据库。作者通过大规模对局统计和深度分析,预先准备常见开局变化,引擎直接从库里面选着法,省掉开局阶段的搜索时间。

残局表则把“逻辑正确”落到实处。当棋盘上剩余棋子很少时,比如不超过七颗棋子,所有合法局面的数量从 10^46 掉到万亿量级,现代机器和压缩算法已经能够完整枚举。残局表会用逆向搜索从必胜/必和的终端局面往回推,把每个局面标注为“红方胜、黑方胜、还是和棋”。在表覆盖范围内,引擎给出的结论是精确的、可验证的。这大约是全国际象棋里唯一可以自称“逻辑正确”的部分,代价是问题域被压缩到了极少残局。

4.4 别把“引擎很强”理解成“引擎证明了自己正确”

看高水平引擎对局,你会觉得它的着法像手术刀一样精准。但这是无数搜索节点和评估函数拟合出来的结果,不是逻辑推理的产物。一个引擎可以在中局找到非常有创意的弃子攻杀,却可能在某个冷门残局里走出理论错误,因为它的搜索深度和评估函数在那条分支上失真了。

真正稳健的做法是:搜索负责“找”,评估负责“判断”,残局表负责“收尾”,开局库负责“起步”。每一层都在降低不确定性,但没有任何一层能消除全部不确定性。

5. 我自己写引擎时踩过的坑:接受不完美,才是第一步

5.1 别一上来就追求“数学正确”

我最早写国际象棋程序时,想的是“至少要写出一个理论上不输的程序”。结果折腾几个月,发现连“不违反规则”的完整实现都比想象中复杂:吃过路兵必须在对方兵刚走两格后立刻处理,王车易位要同时检查王和车之间的格子是否被攻击,重复局面和棋要用历史状态判断,五十步计数要区分哪些动作重置计数器。

这些规则细节一多,程序就很容易在边界条件上出 bug。真正动手后我才意识到,与其追求“每一步都最优”的幻想,不如先把规则实现做成可测试的模块,然后让引擎不断和自己下棋、随机走子,去暴露规则错误。等规则稳定了,再去谈搜索和评估。

5.2 重复局面判定:一个特别容易翻车的细节

有一次我写的引擎在循环将军时不愿意认输,换个局面又会在本来可以强制和棋的地方傻乎乎地乱走。排查了很久,最后发现问题出在哈希表上:我用了路径哈希,而不是局面哈希。同一个棋盘状态,因为到达它的走法顺序不同,算出来的哈希值竟然不一样,导致程序根本认不出“这个局面刚才出现过”。按规则,三次重复可以申请和棋,程序却因为认不出重复,白白把优势局走成输棋。

这个坑给我的教训是:状态编码必须只依赖棋盘上可观测的信息,也就是棋子位置、轮走方、易位权、吃过路兵目标格和半回合计数,不能依赖“怎么走到这里来的历史路径”。路径信息只影响和棋判定规则里的重复计数,但不能污染局面本身的身份。

5.3 深度、时间和评估:追求“更深”不如追求“不蠢”

我后来做了一个简单实验:同一台机器上,固定用评估函数不变,搜索深度从 4 层加到 8 层,单步耗时涨了一个数量级不止,棋力提升却越来越小。6 层的时候,引擎已经能发现大多数两步以下的战术吃子;8 层以上,很多局面里增加的深度只是在重复计算不相关的分支。

真正让引擎从“会走棋”变成“有点强”的,反而是评估函数的质量。一个评估函数如果对轻子出动、兵形弱点、王的安全没有反应,搜索再深也看不见这些。反过来,评估函数只要把子力、中心控制、王安全性这几项配平,即使只搜到 5 层,也能下出像样的棋。所以我后来花在调评估函数上的时间,远多于追求增加搜索深度的时间。

5.4 给后来者的一点实在建议

如果你是被“能不能写出绝对正确程序”这个问题吸引进来的,我的建议是:先去写一个能跑通规则、能搜索、能评估的最小引擎,然后用它和另一个基准引擎对战,看问题出在搜索、评估还是规则。把“正确”的目标从“全局最优”拆成“尽可能不犯错”,工程才真正开始。

如果仍然想要体验一把“逻辑正确”的快感,那就去做一个残局求解器:把棋子限制在五六个以内,用逆向搜索建立完美表站,这比在完整棋盘上追求完美可行得多,也更有成就感。我在实际做这个过程中发现,当你亲眼看到那些边角局面被一张表精准地标成胜负和时,你对“正确程序”的理解已经悄悄变了:正确不是一句大而化之的口号,而是对某个明确问题域给出可验证的精确答案。完整国际象棋太大,大到人类目前只能用近似和高超的工程技巧去逼近它,而正是这种“明知不可能,却拼命逼近”的劲儿,让这个领域一直这么迷人。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询