如何递归创建类Nim游戏博弈树并计算获胜概率?
Hey there! Let's break down how to tackle this recursive game tree problem with memoization for Nim-like games—super common for combinatorial game theory problems, so you're on the right track. Below is a structured, abstract approach that translates cleanly to Java, with solutions for your bottleneck concerns.
The foundation of your entire system is a game state representation that's immutable, hashable, and captures all necessary information about the game's current state. For Nim-like games, this includes:
- The current configuration of piles (or game elements)
- Which player's turn it is
This state needs to support three core operations:
interface GameState { // Is this a terminal state (no more moves can be made)? boolean isTerminal(); // Generate all valid next states from the current state (one per possible move) List<GameState> getNextStates(); // For terminal states: does the current player win here? boolean isWinningTerminal(); }
For a concrete Nim example, use a Java 16+ record (automatically immutable with built-in equals()/hashCode()):
record NimState(int[] heaps, Player currentPlayer) implements GameState { // Ensure immutability by copying the heap array public NimState { heaps = Arrays.copyOf(heaps, heaps.length); // Optional: sort heaps to compress equivalent states (e.g., [3,1,2] = [1,2,3]) Arrays.sort(heaps); } @Override public boolean isTerminal() { return Arrays.stream(heaps).allMatch(h -> h == 0); } @Override public List<GameState> getNextStates() { List<GameState> nextStates = new ArrayList<>(); for (int i = 0; i < heaps.length; i++) { int heapSize = heaps[i]; if (heapSize == 0) continue; // Add all valid moves (adjust this logic for your Nim variant) for (int take = 1; take <= heapSize; take++) { int[] newHeaps = Arrays.copyOf(heaps, heaps.length); newHeaps[i] -= take; nextStates.add(new NimState(newHeaps, currentPlayer.opponent())); } } return nextStates; } @Override public boolean isWinningTerminal() { // Terminal state = current player can't move, so they lose return false; } } enum Player { PLAYER1, PLAYER2; public Player opponent() { return this == PLAYER1 ? PLAYER2 : PLAYER1; } }
You don't need to explicitly build and store the entire game tree (it will explode exponentially for large states). Instead, use memoization to cache results of state calculations, so you only compute each state once.
2.1 For Deterministic Nim (Win/Lose States)
In standard Nim, each state is either a winning state (current player can force a win) or losing state (all moves lead to a winning state for the opponent).
private final Map<GameState, Boolean> winStateMemo = new HashMap<>(); public boolean isWinningState(GameState state) { // Check cache first to avoid re-computing if (winStateMemo.containsKey(state)) { return winStateMemo.get(state); } // Terminal state: return pre-defined win/lose result if (state.isTerminal()) { boolean result = state.isWinningTerminal(); winStateMemo.put(state, result); return result; } // Check all possible next moves for (GameState nextState : state.getNextStates()) { // If any move leads to a losing state for the opponent, current state is winning if (!isWinningState(nextState)) { winStateMemo.put(state, true); return true; } } // All moves lead to opponent winning: current state is losing winStateMemo.put(state, false); return false; }
2.2 For Probabilistic Nim (Win Probabilities)
If your variant includes randomness (e.g., random move selection, or chance-based outcomes), calculate the current player's win probability instead of a binary win/lose:
private final Map<GameState, Double> winProbMemo = new HashMap<>(); public double getWinProbability(GameState state) { if (winProbMemo.containsKey(state)) { return winProbMemo.get(state); } if (state.isTerminal()) { double result = state.isWinningTerminal() ? 1.0 : 0.0; winProbMemo.put(state, result); return result; } List<GameState> nextStates = state.getNextStates(); double totalProb = 0.0; // Assume equal probability for all valid moves (adjust if your game has weighted moves) for (GameState nextState : nextStates) { // Opponent's win probability = getWinProbability(nextState), so current player's chance is 1 - that value totalProb += 1 - getWinProbability(nextState); } double result = totalProb / nextStates.size(); winProbMemo.put(state, result); return result; }
If you need to evaluate every possible move's win probability (not just the current state's overall chance), extend the memoization to store move-by-move analysis:
class StateAnalysis { GameState state; boolean isTerminal; double overallWinProb; List<MoveAnalysis> moveAnalyses; } class MoveAnalysis { GameState nextState; double winProbIfTaken; } private final Map<GameState, StateAnalysis> analysisMemo = new HashMap<>(); public StateAnalysis analyzeState(GameState state) { if (analysisMemo.containsKey(state)) { return analysisMemo.get(state); } StateAnalysis analysis = new StateAnalysis(); analysis.state = state; analysis.isTerminal = state.isTerminal(); if (analysis.isTerminal) { analysis.overallWinProb = state.isWinningTerminal() ? 1.0 : 0.0; analysis.moveAnalyses = Collections.emptyList(); analysisMemo.put(state, analysis); return analysis; } List<MoveAnalysis> moveAnalyses = new ArrayList<>(); double totalProb = 0.0; for (GameState nextState : state.getNextStates()) { StateAnalysis nextAnalysis = analyzeState(nextState); double moveWinProb = 1 - nextAnalysis.overallWinProb; moveAnalyses.add(new MoveAnalysis(nextState, moveWinProb)); totalProb += moveWinProb; } analysis.overallWinProb = totalProb / moveAnalyses.size(); analysis.moveAnalyses = moveAnalyses; analysisMemo.put(state, analysis); return analysis; }
This gives you a tree-like structure where each node contains its overall win probability and the probability of winning for every possible move.
If you're hitting performance or memory issues, try these optimizations:
- State Compression: For Nim-like games, equivalent states (e.g., heap order doesn't matter) can be normalized (like sorting heaps) to reduce the number of unique states in your cache.
- Iterative Recursion: If recursive depth exceeds Java's stack limit (common for large piles), replace recursion with an iterative stack-based approach to avoid stack overflow:
public boolean isWinningStateIterative(GameState initialState) { Map<GameState, Boolean> memo = new HashMap<>(); Stack<GameState> stack = new Stack<>(); Set<GameState> visited = new HashSet<>(); stack.push(initialState); visited.add(initialState); while (!stack.isEmpty()) { GameState state = stack.peek(); if (state.isTerminal()) { memo.put(state, state.isWinningTerminal()); stack.pop(); continue; } boolean allChildrenProcessed = true; boolean hasWinningChild = false; for (GameState nextState : state.getNextStates()) { if (!memo.containsKey(nextState)) { if (!visited.contains(nextState)) { stack.push(nextState); visited.add(nextState); } allChildrenProcessed = false; } else if (!memo.get(nextState)) { hasWinningChild = true; } } if (allChildrenProcessed) { memo.put(state, hasWinningChild); stack.pop(); } } return memo.get(initialState); } - Cache Tuning: Use
LinkedHashMapfor LRU caching (automatically evict least-used states if memory is tight) orConcurrentHashMapfor multi-threaded environments.
内容的提问来源于stack exchange,提问作者Alexander Larios




