每日算法 — 使用java实现2048:蚁群优化ACO与信息素引导合并策略

引言

2048是一款风靡全球的数字滑动合并游戏,玩家通过上下左右四个方向的操作,使相同数字的方块碰撞合并,最终目标是合成2048甚至更高的数字。游戏的难点在于每一步操作不仅影响当前局面,还会随机在空白格生成新方块(通常是2或4),使得未来的状态具有高度的不确定性。传统解法多依赖贪心启发式评估或Expectimax搜索,本文将探索一种生物启发式算法——蚁群优化(Ant Colony Optimization, ACO),通过模拟蚂蚁在棋盘上”释放信息素”的行为,引导AI在庞大的状态空间中找到高价值的合并路径。

算法核心思想

信息素建模

将2048棋盘视为一张”图”,每个棋盘状态是一个节点,每次合法操作(上/下/左/右)构成一条边。蚂蚁在图上移动时,会在走过的边上沉积信息素,信息素浓度反映该操作路径的历史收益。

信息素更新规则包含两个部分:
1. 正反馈:高得分的操作路径会积累更多信息素
2. 挥发机制:旧信息素以固定速率衰减,避免算法过早收敛到局部最优

启发式能见度

除信息素外,每只蚂蚁在选择下一步时还会参考启发式能见度,由以下因素加权构成:
– 合并奖励:本次操作产生的合并总得分
– 单调性惩罚:棋盘非单调程度的惩罚项(鼓励大数字集中在角落)
– 空格奖励:操作后剩余空白格的数量
– 平滑度:相邻格子数值差异的倒数(鼓励平滑过渡)

蚂蚁选择方向的概率由信息素浓度与启发式能见度的乘积决定:

$$P(direction) = \frac{\tau_{dir}^\alpha \cdot \eta_{dir}^\beta}{\sum_{d \in D} \tau_d^\alpha \cdot \eta_d^\beta}$$

其中 $\tau$ 为信息素,$\eta$ 为能见度,$\alpha$ 和 $\beta$ 为调节参数。

Java实现

项目结构

src/
├── game/
│   ├── Board.java       // 2048棋盘核心逻辑
│   ├── Direction.java   // 方向枚举
│   └── MoveResult.java  // 移动结果封装
├── aco/
│   ├── Ant.java         // 单只蚂蚁
│   ├── ACOEngine.java   // 蚁群优化引擎
│   └── PheromoneMap.java // 信息素映射表
├── heuristic/
│   └── Evaluator.java   // 启发式评估函数
└── Main.java

Direction.java & MoveResult.java

package game;

/**
 * 2048游戏四个操作方向
 */
public enum Direction {
    UP(0, -1), DOWN(0, 1), LEFT(-1, 0), RIGHT(1, 0);

    public final int dx, dy;

    Direction(int dx, int dy) {
        this.dx = dx;
        this.dy = dy;
    }
}

/**
 * 封装一次移动操作的结果
 */
public class MoveResult {
    public final int[][] board;      // 移动后的棋盘状态
    public final int scoreGain;      // 本次合并获得的总分数
    public final boolean moved;      // 是否发生了实际移动
    public final int emptyCells;     // 移动后的空格数量
    public final Direction direction; // 操作方向

    public MoveResult(int[][] board, int scoreGain, boolean moved,
                      int emptyCells, Direction direction) {
        this.board = board;
        this.scoreGain = scoreGain;
        this.moved = moved;
        this.emptyCells = emptyCells;
        this.direction = direction;
    }
}

Board.java — 2048棋盘核心逻辑

package game;

import java.util.*;

/**
 * 2048游戏棋盘,支持滑动合并、随机生成和局面评估
 */
public class Board {
    public static final int SIZE = 4;
    private int[][] grid;
    private Random rand;

    public Board(Random rand) {
        this.rand = rand;
        this.grid = new int[SIZE][SIZE];
    }

    public Board(int[][] grid, Random rand) {
        this.rand = rand;
        this.grid = deepCopy(grid);
    }

    /**
     * 深拷贝棋盘
     */
    public int[][] deepCopy(int[][] src) {
        int[][] dst = new int[src.length][src[0].length];
        for (int i = 0; i < src.length; i++) {
            System.arraycopy(src[i], 0, dst[i], 0, src[i].length);
        }
        return dst;
    }

    public int[][] getGrid() { return grid; }
    public void setGrid(int[][] g) { this.grid = deepCopy(g); }

    /**
     * 执行指定方向的滑动合并操作
     * 核心算法:按方向压缩非零元素,相邻相等则合并
     */
    public MoveResult move(Direction dir) {
        int[][] newGrid = deepCopy(grid);
        int scoreGain = 0;
        boolean moved = false;

        if (dir == Direction.LEFT || dir == Direction.RIGHT) {
            for (int r = 0; r < SIZE; r++) {
                int[] row = new int[SIZE];
                for (int c = 0; c < SIZE; c++) row[c] = newGrid[r][c];
                int[] merged = dir == Direction.LEFT ? mergeLine(row) : mergeLineReverse(row);
                for (int c = 0; c < SIZE; c++) {
                    if (newGrid[r][c] != merged[c]) moved = true;
                    newGrid[r][c] = merged[c];
                }
                scoreGain += extractScore(row, merged);
            }
        } else {
            for (int c = 0; c < SIZE; c++) {
                int[] col = new int[SIZE];
                for (int r = 0; r < SIZE; r++) col[r] = newGrid[r][c];
                int[] merged = dir == Direction.UP ? mergeLine(col) : mergeLineReverse(col);
                for (int r = 0; r < SIZE; r++) {
                    if (newGrid[r][c] != merged[r]) moved = true;
                    newGrid[r][c] = merged[r];
                }
                scoreGain += extractScore(col, merged);
            }
        }

        int empty = countEmpty(newGrid);
        return new MoveResult(newGrid, scoreGain, moved, empty, dir);
    }

    /**
     * 单行/单列向左合并的核心算法
     * 步骤:1)压缩非零元素到左侧 2)从左向右相邻相等则合并
     */
    private int[] mergeLine(int[] line) {
        int[] result = new int[line.length];
        int pos = 0;
        for (int i = 0; i < line.length; i++) {
            if (line[i] != 0) {
                result[pos++] = line[i];
            }
        }
        for (int i = 0; i < pos - 1; i++) {
            if (result[i] == result[i + 1] && result[i] != 0) {
                result[i] *= 2;
                result[i + 1] = 0;
            }
        }
        // 再次压缩
        int[] finalResult = new int[line.length];
        int fp = 0;
        for (int v : result) {
            if (v != 0) finalResult[fp++] = v;
        }
        return finalResult;
    }

    private int[] mergeLineReverse(int[] line) {
        int[] reversed = new int[line.length];
        for (int i = 0; i < line.length; i++) reversed[i] = line[line.length - 1 - i];
        int[] merged = mergeLine(reversed);
        int[] result = new int[line.length];
        for (int i = 0; i < line.length; i++) result[i] = merged[line.length - 1 - i];
        return result;
    }

    private int extractScore(int[] before, int[] after) {
        int score = 0;
        for (int i = 0; i < after.length; i++) {
            if (after[i] != 0 && after[i] != before[i]) {
                // 检测是否是合并产生的新值
                for (int j = 0; j < before.length; j++) {
                    if (before[j] == after[i] / 2) {
                        score += after[i];
                        break;
                    }
                }
            }
        }
        return score;
    }

    public int countEmpty(int[][] g) {
        int count = 0;
        for (int[] row : g) {
            for (int v : row) {
                if (v == 0) count++;
            }
        }
        return count;
    }

    /**
     * 在随机空白格生成新方块(90%概率为2,10%概率为4)
     */
    public void spawnRandom() {
        List<int[]> empty = new ArrayList<>();
        for (int r = 0; r < SIZE; r++) {
            for (int c = 0; c < SIZE; c++) {
                if (grid[r][c] == 0) empty.add(new int[]{r, c});
            }
        }
        if (!empty.isEmpty()) {
            int[] pos = empty.get(rand.nextInt(empty.size()));
            grid[pos[0]][pos[1]] = rand.nextDouble() < 0.9 ? 2 : 4;
        }
    }

    /**
     * 获取所有合法移动方向
     */
    public List<Direction> getValidMoves() {
        List<Direction> valid = new ArrayList<>();
        for (Direction d : Direction.values()) {
            MoveResult mr = move(d);
            if (mr.moved) valid.add(d);
            // 恢复棋盘(这里简化处理,实际应保存恢复)
        }
        return valid;
    }

    /**
     * 检查是否游戏结束(无合法移动)
     */
    public boolean isGameOver() {
        for (Direction d : Direction.values()) {
            MoveResult mr = move(d);
            if (mr.moved) {
                // 恢复棋盘状态
                return false;
            }
        }
        return true;
    }

    public void printBoard() {
        for (int[] row : grid) {
            for (int v : row) {
                System.out.printf("%5d", v);
            }
            System.out.println();
        }
        System.out.println();
    }
}

Evaluator.java — 启发式评估函数

package heuristic;

import game.Direction;
import game.MoveResult;

/**
 * 2048局面启发式评估器
 * 综合评分 = 单调性权重 + 空格权重 + 平滑度权重 + 最大数位置权重
 */
public class Evaluator {
    private static final double W_MONOTONICITY = 1.0;
    private static final double W_EMPTY = 2.7;
    private static final double W_SMOOTHNESS = 1.0;
    private static final double W_MAX_CORNER = 1.5;

    /**
     * 计算启发式能见度 eta(值越大表示该操作越优)
     */
    public double evaluateMove(MoveResult result) {
        if (!result.moved) return 0.0;

        int[][] board = result.board;
        double score = 0.0;

        // 1. 合并得分奖励(直接收益)
        score += result.scoreGain * 0.5;

        // 2. 空格数量奖励(未来操作空间)
        score += result.emptyCells * W_EMPTY;

        // 3. 单调性评估(大数字应集中在角落)
        score += calculateMonotonicity(board) * W_MONOTONICITY;

        // 4. 平滑度评估(相邻格子数值差异小)
        score += calculateSmoothness(board) * W_SMOOTHNESS;

        // 5. 最大数在角落奖励
        score += maxInCornerBonus(board) * W_MAX_CORNER;

        return Math.max(score, 0.01); // 避免除零
    }

    private double calculateMonotonicity(int[][] board) {
        double monoScore = 0.0;
        // 检查每行是否单调递减(从左到右)
        for (int[] row : board) {
            for (int i = 0; i < row.length - 1; i++) {
                if (row[i] >= row[i + 1]) monoScore += 1.0;
                else monoScore -= 1.5;
            }
        }
        // 检查每列是否单调递减(从上到下)
        for (int c = 0; c < board[0].length; c++) {
            for (int r = 0; r < board.length - 1; r++) {
                if (board[r][c] >= board[r + 1][c]) monoScore += 1.0;
                else monoScore -= 1.5;
            }
        }
        return Math.max(monoScore, 0);
    }

    private double calculateSmoothness(int[][] board) {
        double smooth = 0.0;
        for (int r = 0; r < board.length; r++) {
            for (int c = 0; c < board[0].length; c++) {
                if (c < board[0].length - 1 && board[r][c] > 0 && board[r][c + 1] > 0) {
                    smooth += 1.0 / (1.0 + Math.abs(board[r][c] - board[r][c + 1]));
                }
                if (r < board.length - 1 && board[r][c] > 0 && board[r + 1][c] > 0) {
                    smooth += 1.0 / (1.0 + Math.abs(board[r][c] - board[r + 1][c]));
                }
            }
        }
        return smooth;
    }

    private double maxInCornerBonus(int[][] board) {
        int max = 0;
        for (int[] row : board) {
            for (int v : row) max = Math.max(max, v);
        }
        // 检查四个角落是否有最大值
        int corners = board[0][0];
        corners = Math.max(corners, board[0][board[0].length - 1]);
        corners = Math.max(corners, board[board.length - 1][0]);
        corners = Math.max(corners, board[board.length - 1][board[0].length - 1]);
        return corners == max ? 10.0 : 0.0;
    }
}

PheromoneMap.java — 信息素映射表

package aco;

import game.Direction;
import java.util.*;

/**
 * 信息素映射表
 * 使用"棋盘哈希 -> 方向 -> 信息素浓度"的三级结构
 * 由于状态空间巨大,采用局部信息素(基于最近几步的历史)
 */
public class PheromoneMap {
    private Map<String, Map<Direction, Double>> pheromones;
    private final double evaporationRate; // 挥发率
    private final double initialPheromone;

    public PheromoneMap(double evaporationRate, double initialPheromone) {
        this.pheromones = new HashMap<>();
        this.evaporationRate = evaporationRate;
        this.initialPheromone = initialPheromone;
    }

    /**
     * 获取指定状态下某方向的信息素浓度
     */
    public double getPheromone(String stateHash, Direction dir) {
        Map<Direction, Double> map = pheromones.getOrDefault(stateHash, new HashMap<>());
        return map.getOrDefault(dir, initialPheromone);
    }

    /**
     * 更新信息素:增加新沉积,并全局挥发
     */
    public void updatePheromone(String stateHash, Direction dir, double deposit) {
        // 全局挥发
        for (Map<Direction, Double> map : pheromones.values()) {
            for (Direction d : map.keySet()) {
                map.put(d, map.get(d) * (1.0 - evaporationRate));
            }
        }

        // 当前路径增加信息素
        Map<Direction, Double> map = pheromones.computeIfAbsent(stateHash, k -> new HashMap<>());
        double current = map.getOrDefault(dir, initialPheromone);
        map.put(dir, current + deposit);
    }

    /**
     * 计算选择概率(信息素^alpha * 能见度^beta)
     */
    public Map<Direction, Double> calculateProbabilities(String stateHash,
                                                          Map<Direction, Double> visibilities,
                                                          double alpha, double beta) {
        Map<Direction, Double> probs = new HashMap<>();
        double sum = 0.0;

        for (Direction dir : visibilities.keySet()) {
            double tau = getPheromone(stateHash, dir);
            double eta = visibilities.get(dir);
            double value = Math.pow(tau, alpha) * Math.pow(eta, beta);
            probs.put(dir, value);
            sum += value;
        }

        // 归一化
        if (sum > 0) {
            for (Direction dir : probs.keySet()) {
                probs.put(dir, probs.get(dir) / sum);
            }
        }
        return probs;
    }

    /**
     * 生成棋盘状态的紧凑哈希表示
     */
    public static String hashBoard(int[][] board) {
        StringBuilder sb = new StringBuilder();
        for (int[] row : board) {
            for (int v : row) {
                sb.append(v).append(',');
            }
        }
        return sb.toString();
    }
}

Ant.java — 单只蚂蚁模拟

package aco;

import game.*;
import heuristic.Evaluator;
import java.util.*;

/**
 * 单只蚂蚁的2048游戏模拟
 * 蚂蚁根据信息素和启发式能见度选择操作方向,直到游戏结束
 */
public class Ant {
    private final Random rand;
    private final Evaluator evaluator;
    private final PheromoneMap pheromoneMap;
    private final double alpha;
    private final double beta;

    private List<MoveRecord> path; // 记录走过的路径
    private int finalScore;
    private int maxTile;

    public Ant(Random rand, Evaluator evaluator, PheromoneMap pheromoneMap,
               double alpha, double beta) {
        this.rand = rand;
        this.evaluator = evaluator;
        this.pheromoneMap = pheromoneMap;
        this.alpha = alpha;
        this.beta = beta;
        this.path = new ArrayList<>();
    }

    /**
     * 蚂蚁从初始棋盘开始模拟一局游戏
     * @param initialBoard 初始棋盘状态
     * @return 本局最终得分
     */
    public int simulate(Board initialBoard) {
        Board board = new Board(initialBoard.getGrid(), rand);
        path.clear();
        int totalScore = 0;
        maxTile = 0;

        // 如果初始棋盘为空,生成两个初始方块
        if (board.countEmpty(board.getGrid()) == 16) {
            board.spawnRandom();
            board.spawnRandom();
        }

        while (true) {
            // 计算每个合法移动的信息素和能见度
            Map<Direction, Double> visibilities = new HashMap<>();
            Map<Direction, MoveResult> moveResults = new HashMap<>();

            for (Direction dir : Direction.values()) {
                Board testBoard = new Board(board.getGrid(), rand);
                MoveResult mr = testBoard.move(dir);
                if (mr.moved) {
                    double eta = evaluator.evaluateMove(mr);
                    visibilities.put(dir, eta);
                    moveResults.put(dir, mr);
                }
            }

            if (visibilities.isEmpty()) break; // 无合法移动,游戏结束

            // 根据信息素概率选择方向
            String stateHash = PheromoneMap.hashBoard(board.getGrid());
            Map<Direction, Double> probs = pheromoneMap.calculateProbabilities(
                stateHash, visibilities, alpha, beta);
            Direction chosen = selectByProbability(probs);

            // 执行移动
            MoveResult result = board.move(chosen);
            totalScore += result.scoreGain;
            board.setGrid(result.board);
            board.spawnRandom();

            // 记录路径
            path.add(new MoveRecord(stateHash, chosen, result.scoreGain));

            // 更新最大方块
            for (int[] row : board.getGrid()) {
                for (int v : row) maxTile = Math.max(maxTile, v);
            }
        }

        finalScore = totalScore;
        return finalScore;
    }

    /**
     * 根据概率轮盘选择方向
     */
    private Direction selectByProbability(Map<Direction, Double> probs) {
        double r = rand.nextDouble();
        double cumulative = 0.0;
        for (Map.Entry<Direction, Double> entry : probs.entrySet()) {
            cumulative += entry.getValue();
            if (r <= cumulative) return entry.getKey();
        }
        return probs.keySet().iterator().next();
    }

    /**
     * 蚂蚁返回路径信息,用于全局信息素更新
     */
    public List<MoveRecord> getPath() { return path; }
    public int getFinalScore() { return finalScore; }
    public int getMaxTile() { return maxTile; }

    public static class MoveRecord {
        public final String stateHash;
        public final Direction direction;
        public final int scoreGain;

        public MoveRecord(String stateHash, Direction direction, int scoreGain) {
            this.stateHash = stateHash;
            this.direction = direction;
            this.scoreGain = scoreGain;
        }
    }
}

ACOEngine.java — 蚁群优化引擎

package aco;

import game.Board;
import heuristic.Evaluator;
import java.util.*;

/**
 * 蚁群优化引擎
 * 协调多只蚂蚁的模拟、信息素更新和最优解追踪
 */
public class ACOEngine {
    private final int numAnts;          // 蚂蚁数量
    private final int maxIterations;    // 最大迭代次数
    private final double alpha;         // 信息素权重
    private final double beta;          // 启发式权重
    private final double evaporation;   // 信息素挥发率
    private final double depositRate;   // 信息素沉积系数

    private final PheromoneMap pheromoneMap;
    private final Evaluator evaluator;
    private final Random rand;

    private int bestScore;
    private int bestMaxTile;
    private List<Ant.MoveRecord> bestPath;

    public ACOEngine(int numAnts, int maxIterations, double alpha, double beta,
                     double evaporation, double depositRate, Random rand) {
        this.numAnts = numAnts;
        this.maxIterations = maxIterations;
        this.alpha = alpha;
        this.beta = beta;
        this.evaporation = evaporation;
        this.depositRate = depositRate;
        this.rand = rand;
        this.pheromoneMap = new PheromoneMap(evaporation, 1.0);
        this.evaluator = new Evaluator();
        this.bestScore = 0;
        this.bestMaxTile = 0;
    }

    /**
     * 运行蚁群优化,返回最优策略的推荐方向
     */
    public Direction optimize(Board currentBoard) {
        System.out.println("=== 启动蚁群优化 ===");

        for (int iter = 0; iter < maxIterations; iter++) {
            List<Ant> ants = new ArrayList<>();
            int iterBestScore = 0;
            Ant iterBestAnt = null;

            // 每只蚂蚁模拟一局游戏
            for (int i = 0; i < numAnts; i++) {
                Ant ant = new Ant(rand, evaluator, pheromoneMap, alpha, beta);
                int score = ant.simulate(currentBoard);
                ants.add(ant);

                if (score > iterBestScore) {
                    iterBestScore = score;
                    iterBestAnt = ant;
                }
            }

            // 更新全局最优
            if (iterBestScore > bestScore) {
                bestScore = iterBestScore;
                bestPath = new ArrayList<>(iterBestAnt.getPath());
                bestMaxTile = iterBestAnt.getMaxTile();
            }

            // 全局信息素更新(仅最优蚂蚁的路径)
            if (iterBestAnt != null) {
                for (Ant.MoveRecord record : iterBestAnt.getPath()) {
                    double deposit = depositRate * record.scoreGain;
                    pheromoneMap.updatePheromone(record.stateHash, record.direction, deposit);
                }
            }

            if (iter % 10 == 0) {
                System.out.printf("  迭代%d: 本轮最优=%d, 全局最优=%d, 最大方块=%d%n",
                    iter, iterBestScore, bestScore, bestMaxTile);
            }
        }

        // 返回当前状态下信息素最高的方向作为推荐
        String currentHash = PheromoneMap.hashBoard(currentBoard.getGrid());
        Direction bestDir = null;
        double maxPheromone = -1;
        for (Direction dir : game.Direction.values()) {
            double p = pheromoneMap.getPheromone(currentHash, dir);
            if (p > maxPheromone) {
                maxPheromone = p;
                bestDir = dir;
            }
        }

        System.out.printf("=== 优化完成,全局最优得分=%d,最大方块=%d ===%n", bestScore, bestMaxTile);
        return bestDir;
    }

    public int getBestScore() { return bestScore; }
    public int getBestMaxTile() { return bestMaxTile; }
}

Main.java — 运行演示

import game.*;
import aco.*;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Random rand = new Random(42);

        // 创建初始棋盘
        Board board = new Board(rand);
        board.spawnRandom();
        board.spawnRandom();

        System.out.println("初始棋盘:");
        board.printBoard();

        // 配置蚁群优化参数
        ACOEngine engine = new ACOEngine(
            20,      // 蚂蚁数量
            50,      // 迭代次数
            1.0,     // alpha: 信息素权重
            2.0,     // beta: 启发式权重
            0.1,     // 挥发率
            0.5,     // 沉积系数
            rand
        );

        // 获取当前最优操作方向
        Direction bestMove = engine.optimize(board);
        System.out.println("推荐操作方向: " + bestMove);

        // 执行推荐操作并展示结果
        MoveResult result = board.move(bestMove);
        board.setGrid(result.board);
        System.out.println("操作后棋盘(得分+" + result.scoreGain + "):");
        board.printBoard();
    }
}

算法复杂度分析

模块 时间复杂度 说明
单次移动合并 O(n²) n为棋盘边长(2048中固定为4,实际为O(1))
启发式评估 O(n²) 遍历棋盘计算单调性、平滑度等
单只蚂蚁模拟 O(steps × n²) steps为一局游戏的平均步数
完整ACO迭代 O(ants × iterations × steps × n²) 蚂蚁数×迭代数×步数
信息素更新 O(paths) 与蚂蚁走过的路径长度成正比

总结

本文将蚁群优化算法应用于2048游戏,通过信息素正反馈机制和启发式能见度引导,使AI能够在有限计算预算内探索高价值的操作序列。ACO的核心优势在于:

  1. 群体智能:多只蚂蚁并行探索不同策略,避免单一路径的局部最优
  2. 自适应学习:信息素沉积机制让算法自动聚焦于高收益操作模式
  3. 可扩展性:通过调整蚂蚁数量和迭代次数,可灵活平衡计算开销与决策质量

该方法展示了生物启发式算法在组合优化问题中的独特价值,特别是在状态空间巨大、存在随机因素的游戏场景中,ACO提供了一种与传统Minimax/Expectimax截然不同的求解思路。