news 2026/10/14 15:31:09

理解极大极小值与Alpha-Beta剪枝:从博弈树到五子棋AI实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
理解极大极小值与Alpha-Beta剪枝:从博弈树到五子棋AI实战

如果能穿越回大学时第一次写五子棋AI的那天,我最想对自己说的一句话是:别死背Alpha-Beta剪枝的伪代码,先搞明白极大极小值到底在选什么。很多同学学这个算法,卡住其实不是因为代码难,而是脑子里没有一个“整棵博弈树在做什么”的画面。

这个算法解决的是典型的对抗决策问题:你和对手轮流走棋,每一步你都选择让自己收益最大的着法,同时假设对手每一步都会选择让你收益最小的着法。极大极小值(Minimax)就是在描述这种“回合制对抗”下的最优决策过程;Alpha-Beta剪枝则是在这个搜索过程中,把“明知道不可能被选中”的分支提前砍掉,让同样的搜索深度下花费的时间少一大截。

这篇文章我会从极大极小值的本质讲起,逐步拆解Alpha-Beta剪枝的两个核心参数alpha和beta,给出可以直接照着抄的伪代码,再用一个具体例子把完整解题流程走一遍。无论你是刚接触人工智能课程的学生,还是想给自己的小游戏加一个对战AI的开发者,这篇文章的目标就是让你合上屏幕之后,能自己从头把这段递归写出来。

1. 极大极小值算法:对抗搜索的基础框架

1.1 博弈问题如何抽象成树

棋类游戏天然适合用树来表达。当前盘面是树的根节点,你每走一步,会得到一批新的盘面,它们是根节点的子节点;对手再走一步,继续往下扩展。于是整盘棋就变成了一棵不断向下生长的博弈树。

在这棵树里,节点之间只有两种关系:要么轮到“我方”走,我千方百计让自己有利;要么轮到“对手”走,对手会千方百计让我倒霉。为了区分这两种节点,通常把前者叫MAX节点,把后者叫MIN节点。之所以叫MAX,是因为我方在搜索时会对所有子节点的价值取最大值,也就是选择对自己最有利的分支;MIN则相反,对手会让局面价值取最小值,也就是对我最不利的分支。

这个抽象很关键。它不需要你理解具体游戏规则,只需要你能回答三件事:第一步,当前局面下有哪些合法走法;第二步,一个走法执行后会得到什么新局面;第三步,这个局面在双方都不犯错的情况下,最终对“我方”有多有利。只要这三件事能定义清楚,任何两人零和博弈都可以套进这个搜索框架。

1.2 MAX层与MIN层的递归规则

那“价值”是怎么从叶子节点向上传递的呢?规则其实非常简洁:

  • 如果当前节点是叶子节点,也就是游戏已经结束,或者搜索深度已经到上限,那么直接返回评估函数算出的分值。
  • 如果当前节点是MAX节点,那么它的价值等于所有子节点价值的最大值。
  • 如果当前节点是MIN节点,那么它的价值等于所有子节点价值的最小值。

可以用一个生活化的类比:你在一家店里挑沙发,你希望花最少钱买到最舒服的款;但店员希望抬高价。如果你们轮流出价,最终成交价就是在双方策略都合理的条件下,两个目标相互压制的结果。MAX层就像你挑商品时“取最优”的心态,MIN层就像店员压你预算时的“取最坏”心态。

好,那这样一个递归公式,求解的是什么呢?是“当前局面在我方先走、双方都采取最优策略”的前提下,最终对开局一方的收益。这个值就是整个博弈树的根节点值,对应着我方在这一局里“最理性”的结局评估。

1.3 评估函数:给棋盘“打分”的尺子

递归公式里最容易被轻视的是评估函数。它决定了叶子节点返回什么数。以五子棋为例,评估函数通常会考虑连子数、活三、冲四、活二等棋形;以井字棋为例,最简单的评估函数就是:如果我方赢了返回正分,对方赢了返回负分,平局返回零。

评估函数的意义不止于结束局面判断。当搜索深度有限,博弈树不能一直展开到终局时,每个被切在中间的叶子节点也需要一个分数,否则整棵树的往上传递就断了。这个分数不需要绝对精确,但必须能大致反映这个局面的优劣。如果你的评估函数把“对方已经形成双活三”的局面打了一个正分,那后面整个搜索都会跟着跑偏。

所以那句老话是对的:搜索算法决定你能“看多远”,评估函数决定你在看得到的地方“看不看得懂”。两者是配合关系,不要只优化其中一个。

1.4 为什么必须“倒推”而不是直接枚举

有人可能会问:既然最终目标只是让根节点拿到最优值,为什么不干脆把所有走法直接模拟到终局,再挑一个最好的呢?理论上当然可以,这就是穷举。问题在于复杂度:一个国际象棋局面平均大约有35种合法走法,如果思考12步,就是双方各走6步,那就需要搜索35的12次方个节点。这个数字大到没有任何机器能直接扛住。

所以真实棋类AI几乎都会限制搜索深度。深度限制意味着搜索到某一层就不再往下,直接调用评估函数打分。这一步背后的逻辑是:你不需要看完整盘棋,只需要在当前可接受的时间范围内,找到一条“在可见范围内最优”的路径。极大极小值负责把这条路径找出来,Alpha-Beta剪枝负责让这条路径尽早被找到。

2. Alpha-Beta剪枝:把“不值得看”的分支提前砍掉

2.1 alpha和beta到底是什么意思

许多初学者一看到alpha、beta两个参数就头大,尤其是递归中它们不断被传来传去。其实这两个数描述的是“双方各自已经锁定的底线”。

  • alpha表示MAX方已经找到的最优结果的下界。也就是说,MAX方知道只要走某个分支,自己至少能拿到alpha这么多。它只会随着搜索不断变大,因为越往深处搜索,MAX方找到的好走法越多,底线越抬越高。
  • beta表示MIN方已经找到的最优结果的上界。也就是说,MIN方知道只要避开某些分支,它最多只让MAX方拿到beta这么多。它只会随着搜索不断变小,因为对手会不断发现让你更难受的限制方式。

初始状态下,MAX方什么走法都还没看,所以alpha是负无穷;MIN方什么走法都还没看,所以beta是正无穷。随着递归一层层推进,alpha不断变大,beta不断变小,直到某一处两个值交叉。一旦出现交叉,就说明双方底线矛盾了:MAX方至少能拿alpha的收益,而MIN方最多只会允许beta的收益,且alpha已经不低于beta。这时候,后面的分支无论怎么搜,都不可能影响父节点的决策,于是剪枝发生。

2.2 剪枝发生在哪一层,条件是什么

剪枝的判断条件在代码里是统一的:如果 alpha >= beta,就提前退出当前循环,不再搜索当前节点的剩余子节点。

但你要分清两种剪枝分别发生在哪个层:

  • 在MAX节点,搜索完一个子节点后,会更新alpha。如果出现 alpha >= beta,说明MAX方已经找到了一条至少让收益达到alpha的路径,而MIN方在上层已经给出了beta这个上限。MIN方不会让局面走上这条路,所以MAX方后续再搜其他兄弟节点也没有意义。这种情况叫beta剪枝,本质上剪的是“超过了MIN方底线所以不会被选择”的分支。
  • 在MIN节点,搜索完一个子节点后,会更新beta。如果出现 alpha >= beta,说明MIN方已经找到了一条最多让收益不超过beta的路径,而MAX方在上层已经保证了自己至少能拿到alpha。这条线对MAX方来说太亏,MAX方根本不会选这条路。这种情况叫alpha剪枝。

说直白一点:alpha是站在MAX方立场上的最佳保证值,beta是站在MIN方立场上的最佳保证值。当两者碰头,说明这台戏已经不需要再往下演了。

2.3 一个手工追踪的例子,把过程走一遍

我拿一棵很小的博弈树来演示。根节点A是MAX节点,它有三个子节点B、C、D,都是MIN节点。B下面有两个叶节点,分值分别是3和5;C下面有三个叶节点,分值是2、4、1;D下面有两个叶节点,分值是9和7。搜索顺序按从左到右。

搜索过程如下:

第一步,在A处初始化 alpha 为负无穷,beta 为正无穷。

第二步,进入B,B是MIN节点,收到 alpha=-∞,beta=+∞。B先看第一个叶节点3,此时B当前最佳值=3,beta更新为3。接着看第二个叶节点5,取min,最小值仍是3,beta保持不变。B返回3。

第三步,回到A,A是MAX节点,当前最佳值从-∞变成3,alpha更新为3。此时A已经知道,走B这条线至少能拿到3。

第四步,进入C,C是MIN节点,收到 alpha=3,beta=+∞。C看第一个叶节点2,当前最佳值=2,beta更新为2。这时检查条件:alpha >= beta,也就是3 >= 2,剪枝条件满足,C剩下的子节点4和1完全可以不再看,C直接返回2。

为什么能直接返回?因为C是个MIN节点,它已经发现一个子节点能给到2,而MIN节点最终只会在子节点里选最小值,所以C的最终值最多就是2。A反正已经有3打底,走C这条线对A来说必定不如B好,选了也是亏。所以后面不用搜了。

第五步,回到A,A比较一下来自C的2和自己已有的3,发现3更好,alpha保持在3。

第六步,进入D,D是MIN节点,收到 alpha=3,beta=+∞。D先看叶节点9,当前最佳值=9,beta=9,不满足剪枝条件;再看叶节点7,取min,变为7,beta=7,仍不剪枝。D返回7。

第七步,回到A,A用max取最大值,比较3和7,最终选择7,走D这条线。

如果不用剪枝,C节点后面4和1都要看;用了剪枝,这两个节点直接被跳过。小树只有几个节点所以节省不明显,但放到真实棋类里,每次省下的分支叠加起来非常可观。

2.4 为什么剪枝不会改变最终决策

这是Alpha-Beta剪枝最让初学者不放心的地方:主动不看一些分支,会不会错过更优解?

答案是不会。关键在于剪枝发生时,我们已经对那个分支的价值范围有了足够准确的判断。在MAX节点剪枝时,是因为当前子树的收益上限已经不可能超过父层MIN方愿意接受的beta;在MIN节点剪枝时,是因为当前子树的收益下限已经不可能达到父层MAX方已经拿到的alpha。无论被剪掉的子树内部藏着多出乎意料的值,它都不会成为双方在最优策略下的选择。

所以Alpha-Beta剪枝改变的只是搜索量,不改变搜索结果。只要剪枝条件写对,最后根节点的最佳走法与不剪枝的极大极小值算法完全一致。这也是我后来调试代码时最常用的验证手段:先写一个不剪枝的版本,再加剪枝,然后对比两者在随机局面上的返回走法是否一致,不一致就说明剪枝写错了。

3. 伪代码逐行拆解:照着写不会出错的版本

3.1 先写不带剪枝的极小极大

一切剪枝优化都要先建立在一个正确的基础版本上。下面是经典的极小极大算法伪代码,我这里用Python风格的写法表示:

def minimax(node, depth, maximizing_player): # 到达叶子节点,或者搜索深度用完 if depth == 0 or node.is_terminal(): return evaluate(node) if maximizing_player: best_value = -float('inf') for child in node.get_children(): value = minimax(child, depth - 1, False) best_value = max(best_value, value) return best_value else: best_value = float('inf') for child in node.get_children(): value = minimax(child, depth - 1, True) best_value = min(best_value, value) return best_value

这段代码的每一行都有明确含义。首先,depth用于控制搜索深度,当depth归零时停止下钻。其次,maximizing_player用一个布尔值表示当前节点该取max还是min,这样一套递归函数可以同时处理MAX层和MIN层。最后,get_children返回当前局面所有合法走法产生的子局面。

这段代码虽然正确,但效率并不高。它会把每一个分支都搜到底,哪怕某些分支已经明显对当前决策没有影响。

3.2 Alpha-Beta剪枝的完整伪代码

在上面的基础上加入alpha和beta两个参数,就得到完整版剪枝算法:

def alphabeta(node, depth, alpha, beta, maximizing_player): if depth == 0 or node.is_terminal(): return evaluate(node) if maximizing_player: value = -float('inf') for child in node.get_children(): value = max(value, alphabeta(child, depth - 1, alpha, beta, False)) alpha = max(alpha, value) if alpha >= beta: break # beta剪枝 return value else: value = float('inf') for child in node.get_children(): value = min(value, alphabeta(child, depth - 1, alpha, beta, True)) beta = min(beta, value) if alpha >= beta: break # alpha剪枝 return value

对比一下就会发现,它只比最小极大多做了三件事:多接收两个参数、在搜索完每个子节点后更新alpha或beta、每次更新后检查是否剪枝。逻辑上完全顺着极大极小的骨架来,所以只要你理解极大极小,这个版本就不难理解。

值得强调的一点是,break之前return的那个value其实是“当前这个节点已知的最佳评估值”,不一定要是理论上的最终值。因为当剪枝发生时,准确的最终值已经不影响上层决策了,返回当前已知值就足够。

3.3 入口函数怎么调用递归

实际做棋类AI时,光拿到根节点的分值不够,最终要输出的是“走哪一步棋”。所以需要一个入口函数,对根节点的每一个合法走法分别调用alphabeta,选出得分最高的那个走法返回。

def get_best_move(board, depth): best_score = -float('inf') best_move = None for move in board.get_legal_moves(): new_board = board.apply_move(move) # 模拟我方走这一步 score = alphabeta(new_board, depth - 1, -float('inf'), float('inf'), False) if score > best_score: best_score = score best_move = move return best_move

注意这里有几个容易踩的细节:

第一,根节点本身是MAX节点,所以get_best_move里是“取分数最大的走法”。第二,传给alphabeta的maximizing_player是False,因为这已经是模拟我走一步之后、轮到对手走的局面,下一层是MIN层。第三,每一轮调用重新初始化alpha为负无穷、beta为正无穷,这说明一次完整的搜索是从“什么都不知道”开始的。

3.4 写代码时容易翻车的几个细节

第一,忘了恢复棋盘状态。很多新手喜欢用同一个棋盘对象递归模拟落子,但递归返回后棋盘已经被改掉,后面的兄弟节点搜索会拿到错误局面。要么用不可变对象,要么在回溯时明确撤销这一手。

第二,把alpha和beta的更新写反。MAX节点更新alpha,MIN节点更新beta,这是铁律。如果MAX节点更新了beta,MIN节点更新了alpha,剪枝条件可能会被错误触发,最后返回的结果就不再等价于完整极大极小。

第三,没有处理“无合法走法”的情况。游戏结束或者棋盘已满时,get_legal_moves返回空列表,循环直接跳过,best_move就可能是None。通常在入口函数开头检查terminal状态,或者在没有合法走法时返回一个表示认输/平局的值。

第四,注意评估函数的符号方向。整个算法默认MAX方希望分数大,MIN方希望分数小。如果你的评估函数写的角度是“当前轮到谁就站在谁的立场打分”,那符号方向很容易来回漂。最简单可靠的做法是:评估函数永远站在MAX方立场返回分数,正数对MAX有利,负数对MIN有利。

4. 解题流程:从棋盘输入到落子决策

4.1 完整流程分七步走

我建议把整个Alpha-Beta剪枝的求解流程固定成下面这个模板,不管是做作业还是自己写小游戏,直接套用就行。

第一步,定义局面表示。井字棋可以用二维字符数组,五子棋可以用二维整型数组,复杂的棋类可能需要自己设计局面类。关键是能表达当前轮到谁走、棋盘上的棋子分布。

第二步,实现走法生成函数get_legal_moves。这个函数返回当前局面下所有合法落子位置。

第三步,实现走法执行函数apply_move,或者直接在棋盘对象上用落子和撤销进行状态更新。

第四步,编写评估函数evaluate。先做一个简单的版本,比如考虑胜负、平局和当前局部棋形。

第五步,实现is_terminal判断,即判断游戏是否结束,是否出现胜负或平局。

第六步,写出alphabeta递归函数,这是整棵搜索树的核心。

第七步,用get_best_move入口函数遍历根节点走法,收集分数,返回最优落子位置。

流程中最核心的递归函数是第六步,但前四步任何一步没写对,整个结果都会错。

4.2 流程中的关键决策点

第一个决策点是选择搜索深度。三子棋或者井字棋,深度设为9就已经能覆盖整盘棋;五子棋往往设到4到6层就很吃力;国际象棋类游戏就要依赖更复杂的搜索结构。深度越大,棋力通常越强,但耗时会指数级上升。

第二个决策点是叶子节点返回的评估分是否需要归一化。搜索深度如果很浅,评估函数只看到局部棋形,可能会怂恿AI贪吃一个子而丢掉全局局势;深度足够深时,终局判断会修正这个偏差。当你发现AI“贪小便宜”时,不一定要急着调评估函数,试着加大深度可能就解决了。

第三个决策点是走法排序。Alpha-Beta剪枝非常依赖子节点的搜索顺序,最理想情况下先搜索到最优走法,剪枝效果最佳;最差情况下所有走法从坏到好排序,剪枝几乎是空转。所以专业棋类AI都会有一个启发式排序:把吃子、将军、活三等“看起来更急迫”的走法排在前面。

4.3 用一个小例子验证完整调用链

我拿井字棋来做示范,局面如下,轮到X方走:

X . . . O . . . .

X是MAX方,目标是找一个位置落子,让最终盘面评分最高。假设搜索深度设为2,也就是X走一步,O走一步。评估函数简单规定:X连成三线得10分,O连成三线得-10分,平局得0分,未结束时按当前棋形粗略打分。

get_best_move会先枚举X的所有合法落子。以X走到左上角和右上角的情况为例,应用走法后,程序进入MIN层,模拟O对当前局面的响应。O会尝试堵截X的潜在连线,也会试图给自己创造连线条件。每个O的落子都会再生成一个深度为0的节点,此时评估函数直接打分并返回。

假设X落在左上角,O可能会选择堵住第一行或者其他关键位置;假设X落在右上角,O的最佳选择又不同。alphabeta会把每个分支的值沿着MIN取小、MAX取大的规则往上传,最后get_best_move收集到所有X合法走法的分数,取最大值对应的落子位置输出。

这个例子里我故意不把整棵树展开,因为井字棋即使深度为2,分支也已经不少。你只要理解递归链条是“根节点枚举走法、子节点继续递归、叶子节点评分返回”,整棵树的工作方式就清晰了。

5. 常见错误与排查实录

5.1 剪枝条件怎么写才算对

我在帮人看代码时,最常见的错误是把剪枝条件写成beta < alpha而不是alpha >= beta。这个区别看似微小,实际影响很大:如果写成绝对小于,当alpha和beta恰好相等时不会剪枝,剪枝率变差;如果反过来写成beta <= alpha,其实也是一样的,只是条件命名角度不同。

更严重的错误是有人把剪枝条件放在了错误的位置。剪枝必须在更新完alpha或beta之后立刻检查,而不是搜完所有子节点后再统一检查。因为剪枝的意义就在于“某个子节点搜索完就已经知道后面的兄弟节点不用看了”,如果放到循环结束再检查,就等于是全搜完了才想起来剪枝,效率完全浪费掉。

如果你怀疑自己的剪枝写错了,最快的排查方式就是:去掉剪枝跑一遍,加上剪枝跑一遍,对比两者在大量局面上的最佳走法。完全一致说明剪枝逻辑正确;有不一致就说明某个节点该更新alpha的地方没更新,或者该更新beta的地方写成了alpha。

5.2 返回的是分值还是走法

另一个高频问题是不清楚递归函数和入口函数各自返回什么。alphabeta函数内部返回的是“当前节点的评估值”,它永远是一个数字。只有get_best_move才是返回“走法”的入口函数。

我见过不少同学试图把单个走法对象塞进alphabeta的返回值里,结果要么返回类型混乱,要么在深层递归里存走法导致上层拿到的根本不是根节点该选的走法。如果你确实想在alphabeta内部记录最佳走法,也要做到两点:只在根节点的那一层记录,并且每一轮搜索结束都清空旧记录。不过我的建议很简单:只让alphabeta返回数字,走法选择全部交给get_best_move做,分清职责省一堆bug。

5.3 搜索太慢,先别急着加复杂优化

当搜索速度慢,新手第一反应往往是优化评估函数或者上各种高级技巧。但最值得先检查的其实是移动排序。Alpha-Beta剪枝的效率上限取决于子节点的排列顺序,一个简单的启发式排序往往能带来数量级的提升。

一个直观的对比:假设搜索树每层有30个分支,深度为6。如果每次都是运气极好,第一个搜到的就是最优分支,剪枝效率极高;如果每次最差分支排最前,几乎整个树都要遍历。这就是为什么专业的AI都要做走法排序,哪怕排序本身也要花费一定计算量,但通常物超所值。

另一个慢的原因是搜索深度设得过大。当评估函数还很粗糙时,盲目增加深度不一定会带来棋力提升,反而会让人等待很久。我的习惯是先把深度设为2到3,确认整体流程跑通且结果合理,再逐步加大。每加一层深度,时间消耗可能翻好几倍,心里要有预期。

5.4 调试递归时的实用小技巧

调试这种递归算法,不要靠盯着代码猜。我常用的方法是在alphabeta函数入口打印当前节点信息、alpha、beta和当前深度。这样能顺着递归轨迹看到每一层的alpha和beta如何变化,剪枝发生在哪个节点,和手工推演对上就说明逻辑正确。

另一个技巧是把评估函数先做成最简单版本,比如只返回胜负分。等整体搜索验证正确后,再慢慢把评估函数做得精细。这能避免“搜索有问题但被评估函数影响”,让你把变量控制在最小范围内。

还有一个坑是Python的递归层数限制。默认的递归深度上限大概在1000层,复杂棋类一旦深度超过这个值会直接报错。要么用sys.setrecursionlimit调高上限,要么用迭代加深等方式控制层数。不过很多棋类游戏的搜索深度不会到1000层,这个坑更多出现在迷宫类问题里。

6. 从刷题到工程:算法边界的思考

6.1 看一眼博弈树的规模就知道剪枝多重要

不做剪枝的全量搜索,复杂度是O(b^d),b是平均分支数,d是搜索深度。井字棋b约等于4到8,国际象棋b约等于35。当b达到35、深度达到10的时候,要搜索的节点数是一个天文数字,普通机器完全扛不住。

加入Alpha-Beta剪枝后,理论上最佳情况可以把复杂度降到约O(b^(d/2)),也就是在同样的时间内,原来能搜6层的现在可以搜到接近12层。这个提升是数量级的,不是靠处理器算得快能轻易补回来的。

但对博弈搜索来说,剪枝并不是终点。后面还有置换表、迭代加深、Zobrist哈希、启发式评估调参等一系列工程优化手段。理解Alpha-Beta只是进入更大型棋类AI的第一步,但它是最重要的一步。

6.2 走法排序为什么决定剪枝效率

Alpha-Beta剪枝的效率严重依赖“先在好的分支里找到足够好的结果”。在MAX节点,如果我们能先搜索到一个大的子节点值,alpha会快速抬高,后续大量分支都会被beta剪掉;在MIN节点,如果我们能先搜索到一个小的子节点值,beta会快速压低,后续大量分支都会被alpha剪掉。

所以通用的优化方向是:对子节点按评估函数预排序。比如五子棋可以优先搜索靠近已有棋子的落点,国际象棋可以优先搜索吃子、将军等高价值走法。这个排序可以简单也可以复杂,甚至用上一层的评估分数做粗排就够了。

我实测下来,一个朴素的“按风险评估预排序”往往就能把剪枝速度提升好几倍。别一上来就想着用机器学习的评估函数来做排序,先让简单的评估函数帮你把明显不合理的着法排到后面去。

6.3 经典扩展:迭代加深与置换表

迭代加深是最值得了解的扩展。它的做法是:先搜索深度1,再搜索深度2,再搜深度3,直到时间耗尽。表面上看重复搜索浪费了时间,但每一次浅层搜索都会给深层搜索提供一个靠谱的走法顺序,这套顺序能显著提升深层搜索的剪枝效率。另外,迭代加深天然适合“限时决策”的需求,因为任何一秒你都能说出当前最优解。

置换表则是利用搜索过程中可能反复到达同一个局面。棋类中经常出现通过不同走法顺序到达相同盘面的情况,如果缓存这个盘面的搜索值和最佳走法,后续再遇到可以直接读取,省掉一整棵子树的搜索。引擎通常用Zobrist哈希把局面压缩成一个64位整数,作为缓存键。

这两者听起来复杂,但它们的核心目的和Alpha-Beta剪枝一样:不搜那么多,不搜那么废,把已有的信息尽量榨出价值。理解了Alpha-Beta剪枝的动机之后,再看这些扩展,会发现它们完全是同一路思维。

最后说点个人体会。这个算法我前后教过几轮同学,发现上手最快的方法永远是:先用最简单的小棋类游戏,把不带剪枝的极大极小值跑通;再对照手工推演,把alpha和beta的传递过程走一遍;最后才加剪枝。不要一上来就抱着复杂引擎的源码啃,那只会让你陷入细节出不来。

如果你在做某个具体项目时调不通搜索,别急着查“更高级”的资料,先问自己三件事:评估函数符号对不对?递归之后棋盘有没有还原?alphabeta返回的到底是个数字还是个走法?这三个问题解决了,九成以上的bug都能消失。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 15:30:21

归并排序求逆序对:从分治思想到高效代码实现

1. 题面与第一直觉&#xff1a;暴力解法为什么撑不过大数据很多人第一次接触归并排序时&#xff0c;都把它当成一个普通排序算法来学&#xff1a;分两半&#xff0c;分别排好&#xff0c;再合并。这个理解当然没错&#xff0c;但总觉得少了点味道。直到遇到逆序对个数这道题&am…

作者头像 李华
网站建设 2026/10/14 15:30:01

深入解析Fetch API:从设计原理到生产级封装实践

1. 为什么 Fetch API 值得你花时间彻底搞懂前端圈子里有个挺有意思的现象&#xff1a;很多人天天用fetch发请求&#xff0c;但真被问到“它和 XMLHttpRequest 到底差在哪”“为什么我拿不到 404 的错误”“怎么取消一个请求”的时候&#xff0c;能答利索的没几个。我自己带过几…

作者头像 李华
网站建设 2026/10/14 15:28:18

UE4平面反射完全解析:原理、性能开销与水面反射实现

做水面反射的时候&#xff0c;我第一次栽在UE4的反射系统上。屏幕空间反射&#xff08;SSR&#xff09;在起伏的水面上闪烁得厉害&#xff0c;反射探针又只能反映一个静态的近似环境&#xff0c;怎么调都透着一股“塑料感”。后来把目光转到UE4的平面反射&#xff08;Planar Re…

作者头像 李华
网站建设 2026/10/14 15:25:35

机器学习量化策略实战:从数据清洗到诚实回测的完整指南

简介&#xff1a;面向具备一定Python基础、但对炒股与量化投资零概念的人群&#xff0c;这份demo源码演示了基于机器学习构建A股量化策略的完整路径&#xff1a;从行情与交易数据获取&#xff0c;到特征筛选、模型训练&#xff0c;再到策略回测和风险控制&#xff0c;帮助读者快…

作者头像 李华