可变棋盘尺寸与获胜条件下井字棋Minimax算法的性能优化
嘿,我太懂你在大棋盘(比如8x12)和高k值连子规则下遇到的Minimax性能瓶颈了——随着棋盘规模扩大,搜索空间呈指数级爆炸,哪怕有Alpha-Beta剪枝也顶不住。咱们从底层逻辑到算法改进一步步来优化,帮你把AI的速度提上去:
一、先优化最耗时的胜负检测与评估函数
你现在的check_win每次都遍历整个棋盘,这在大棋盘上是巨大的性能浪费——胜负只可能由最新落子的位置产生,完全没必要扫遍所有格子。同时,当前的评估函数只有“胜/负/平”三种状态,导致必须搜很深才能做出决策,咱们一起改:
1. 仅检查最新落子的周边区域
修改check_win,让它只验证最新落子所在的行、列、两条对角线是否满足k连条件:
def check_win(board, player, k, last_move): r, c = last_move rows = len(board) cols = len(board[0]) # 检查水平方向 count = 1 # 向左数 i = c - 1 while i >= 0 and board[r][i] == player: count += 1 i -= 1 # 向右数 i = c + 1 while i < cols and board[r][i] == player: count += 1 i += 1 if count >= k: return True # 检查垂直方向 count = 1 i = r - 1 while i >= 0 and board[r][i] == player: count += 1 i -= 1 i = r + 1 while i < rows and board[r][i] == player: count += 1 i += 1 if count >= k: return True # 检查正对角线(左上到右下) count = 1 i, j = r - 1, c - 1 while i >= 0 and j >= 0 and board[i][j] == player: count += 1 i -= 1 j -= 1 i, j = r + 1, c + 1 while i < rows and j < cols and board[i][j] == player: count += 1 i += 1 j += 1 if count >= k: return True # 检查反对角线(右上到左下) count = 1 i, j = r - 1, c + 1 while i >= 0 and j < cols and board[i][j] == player: count += 1 i -= 1 j += 1 i, j = r + 1, c - 1 while i < rows and j >= 0 and board[i][j] == player: count += 1 i += 1 j -= 1 if count >= k: return True return False
这样每次检测的时间复杂度从O(rows*cols)降到O(k),速度提升非常明显。
2. 增强评估函数的中间状态得分
当前评估函数只有极端值(胜/负),导致必须搜很深才能判断状态优劣。咱们可以计算玩家的潜在连子潜力,比如:
def evaluate_board(board, player, k): opponent = 3 - player player_score = 0 opponent_score = 0 # 遍历所有可能的连子窗口,计算得分 rows = len(board) cols = len(board[0]) # 水平窗口 for r in range(rows): for c in range(cols - k + 1): window = board[r][c:c+k] p_count = window.count(player) o_count = window.count(opponent) if o_count == 0: player_score += p_count ** 2 # 比如3个连子得9分,2个得4分 if p_count == 0: opponent_score += o_count ** 2 # 垂直、对角线窗口同理,这里省略重复代码 # 优先判断胜负 if check_win(board, player, k, (-1,-1)): # 这里可以结合最新落子优化,暂时简化 return 1000 if check_win(board, opponent, k, (-1,-1)): return -1000 return player_score - opponent_score
这样即使搜索深度不够,AI也能优先选择更有潜力的位置,同时减少不必要的深度搜索。
二、优化Minimax的搜索效率
1. 迭代加深搜索(Iterative Deepening)
代替固定深度搜索,先从深度1开始搜索,记录最佳候选位置;然后深度2只搜索这些候选位置,以此类推,直到达到时间限制或最大深度。这样既能在有限时间内搜到尽可能深的结果,又能让Alpha-Beta剪枝更早触发:
def best_move(board, player, k, max_depth=4): best_move = (-1, -1) # 初始候选是所有有意义的位置 candidates = get_relevant_moves(board, k) for depth in range(1, max_depth+1): best_val = -float('inf') new_candidates = [] for (r,c) in candidates: board[r][c] = player move_val = minimax(board, depth-1, -float('inf'), float('inf'), False, player, k) board[r][c] = 0 if move_val > best_val: best_val = move_val best_move = (r,c) new_candidates = [(r,c)] elif move_val == best_val: new_candidates.append((r,c)) candidates = new_candidates if not candidates: break return best_move
2. 排序搜索节点(Move Ordering)
把最有希望的走法(比如能形成潜在连子的位置)放在搜索队列前面,这样Alpha-Beta剪枝能更早剪掉无用分支。比如先评估所有空位置的得分,按得分从高到低排序后再搜索:
def get_sorted_moves(board, player, k): moves = [] for r in range(len(board)): for c in range(len(board[0])): if board[r][c] == 0: board[r][c] = player score = evaluate_board(board, player, k) board[r][c] = 0 moves.append((-score, r, c)) # 负号是为了升序排序相当于降序 moves.sort() return [(r,c) for (s,r,c) in moves] # 在minimax和best_move中使用这个排序后的列表,代替原有的双重循环
3. 置换表(Transposition Table)
用哈希表缓存已经计算过的棋盘状态的得分,避免重复计算相同状态。因为不同的走法序列可能到达同一个棋盘状态,缓存后直接复用结果:
transposition_table = {} def minimax(board, depth, alpha, beta, is_maximizing_player, player, k): # 把棋盘转换成可哈希的键 board_key = tuple(tuple(row) for row in board) if (board_key, depth, is_maximizing_player) in transposition_table: return transposition_table[(board_key, depth, is_maximizing_player)] score = evaluate_board(board, player, k) if score == 1000 or score == -1000 or depth == 0: transposition_table[(board_key, depth, is_maximizing_player)] = score return score # 后续的minimax逻辑不变,最后把结果存入缓存 if is_maximizing_player: best = -float('inf') for r,c in get_sorted_moves(board, player, k): board[r][c] = player best = max(best, minimax(board, depth-1, alpha, beta, False, player, k)) board[r][c] = 0 alpha = max(alpha, best) if beta <= alpha: break else: best = float('inf') opponent = 3 - player for r,c in get_sorted_moves(board, opponent, k): board[r][c] = opponent best = min(best, minimax(board, depth-1, alpha, beta, True, player, k)) board[r][c] = 0 beta = min(beta, best) if beta <= alpha: break transposition_table[(board_key, depth, is_maximizing_player)] = best return best
三、缩小搜索范围,只关注有意义的位置
在大棋盘上,大部分空位置离已有棋子很远,完全不可能形成k连,直接忽略这些位置:
def get_relevant_moves(board, k): relevant = set() rows = len(board) cols = len(board[0]) # 遍历所有已有棋子,把周围k范围内的空位置加入候选 for r in range(rows): for c in range(cols): if board[r][c] != 0: # 上下左右、对角线的k范围内 for dr in range(-k+1, k): for dc in range(-k+1, k): nr = r + dr nc = c + dc if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] == 0: relevant.add((nr, nc)) # 如果没有已有棋子(开局),返回所有位置 return list(relevant) if relevant else [(r,c) for r in range(rows) for c in range(cols) if board[r][c] == 0]
用这个函数代替原有的所有空位置搜索,能大幅减少搜索节点数。
四、改进并行化方案
你之前的多线程方案有线程安全问题(多个线程修改同一个棋盘),而且Python的GIL限制了CPU密集型任务的多线程性能,改用多进程+棋盘复制:
from concurrent.futures import ProcessPoolExecutor def evaluate_move(args): board_copy, depth, alpha, beta, is_maximizing, player, k, r, c = args board_copy[r][c] = player if is_maximizing else 3 - player return minimax(board_copy, depth-1, alpha, beta, not is_maximizing, player, k) def parallel_best_move(board, player, k, depth=4): best_val = -float('inf') best_move = (-1,-1) moves = get_relevant_moves(board, k) # 为每个移动复制一份棋盘 args_list = [] for r,c in moves: board_copy = [row.copy() for row in board] args_list.append((board_copy, depth, -float('inf'), float('inf'), False, player, k, r, c)) with ProcessPoolExecutor() as executor: results = executor.map(evaluate_move, args_list) for idx, val in enumerate(results): r,c = moves[idx] if val > best_val: best_val = val best_move = (r,c) return best_move
总结优化优先级
- 先改胜负检测为仅检查最新落子周边,这是见效最快的优化;
- 然后用缩小搜索范围减少节点数;
- 接着加入节点排序和置换表,提升Alpha-Beta剪枝效率;
- 最后考虑迭代加深和多进程并行,进一步压榨性能。
按照这个顺序优化,你会发现大棋盘下的AI速度会有质的提升,同时决策质量也不会下降太多。
备注:内容来源于stack exchange,提问作者Lumo

