每日算法 — 使用java实现贪吃蛇:蚁群优化路径探索与信息素引导策略

引言

贪吃蛇是经典的编程练习题目,传统的实现通常依赖人工规则或A搜索等启发式算法。本文将换一个视角,使用蚁群优化(Ant Colony Optimization, ACO)*这一群体智能算法来驱动贪吃蛇的决策过程。通过模拟蚂蚁在路径上释放信息素的行为,让蛇在多步决策中自动学会选择最优路径,兼顾吃食物效率与自身安全。

蚁群优化算法原理

蚁群优化算法源于对真实蚂蚁觅食行为的观察。蚂蚁在移动过程中会释放信息素,后续蚂蚁更倾向于选择信息素浓度较高的路径,从而形成正反馈。在贪吃蛇场景中,我们可以将蛇的每一步移动视为蚂蚁的一次路径选择:

  • 信息素初始化:地图上每个格子拥有初始信息素值
  • 启发函数:距离食物越近的格子启发值越高,距离蛇尾越近的格子也给予正启发(预留逃生空间)
  • 转移概率:蛇根据信息素浓度和启发函数计算每个可行方向的转移概率
  • 信息素更新:每轮迭代后,成功到达食物的路径上的信息素增强,无效路径的信息素挥发

核心算法设计

状态表示

将蛇身、食物和空地编码为二维网格,每个格子维护一个信息素浓度值。蛇头当前位置作为蚁群搜索的起点,食物位置作为终点。

启发函数设计

启发函数需要同时考虑两个目标:尽快吃到食物,同时避免将自己困住。定义如下:

η(i,j) = α / distance_to_food(i,j) + β / distance_to_tail(i,j)

其中distance_to_tail鼓励蛇向尾部区域移动,从而在吃食物后仍能留出逃生通道。

转移概率公式

从当前格子i移动到相邻格子j的概率:

P(i→j) = [τ(j)]^λ · [η(j)]^μ / Σ([τ(k)]^λ · [η(k)]^μ)

其中τ为信息素浓度,λ和μ分别控制信息素和启发函数的权重。

Java完整实现

import java.util.*;

/**
 * 基于蚁群优化的贪吃蛇AI
 * 使用信息素引导策略实现路径探索与决策
 */
public class AntColonySnake {

    // 地图尺寸
    private final int width;
    private final int height;

    // 方向向量:上、下、左、右
    private final int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    private final String[] dirNames = {"UP", "DOWN", "LEFT", "RIGHT"};

    // 蚁群参数
    private final int antCount = 30;           // 蚂蚁数量
    private final double alpha = 1.5;          // 信息素权重
    private final double beta = 2.0;           // 启发函数权重
    private final double rho = 0.3;            // 信息素挥发率
    private final double Q = 100.0;            // 信息素增强常量
    private final int maxIterations = 50;      // 每步最大迭代次数

    // 信息素地图:每个格子的信息素浓度
    private double[][] pheromone;

    // 蛇身队列(头部在队尾)
    private Deque<int[]> snake;
    private int[] food;
    private boolean[][] occupied;

    private final Random random = new Random();

    public AntColonySnake(int width, int height) {
        this.width = width;
        this.height = height;
        this.pheromone = new double[width][height];
        this.occupied = new boolean[width][height];

        // 初始化信息素
        for (int i = 0; i < width; i++) {
            Arrays.fill(pheromone[i], 1.0);
        }

        // 初始化蛇:从中心开始,长度3
        this.snake = new ArrayDeque<>();
        int cx = width / 2, cy = height / 2;
        snake.addLast(new int[]{cx, cy});
        snake.addLast(new int[]{cx - 1, cy});
        snake.addLast(new int[]{cx - 2, cy});
        updateOccupied();

        // 随机生成食物
        spawnFood();
    }

    private void updateOccupied() {
        for (int i = 0; i < width; i++) {
            Arrays.fill(occupied[i], false);
        }
        for (int[] body : snake) {
            occupied[body[0]][body[1]] = true;
        }
    }

    private void spawnFood() {
        List<int[]> empty = new ArrayList<>();
        for (int i = 0; i < width; i++) {
            for (int j = 0; j < height; j++) {
                if (!occupied[i][j]) {
                    empty.add(new int[]{i, j});
                }
            }
        }
        if (empty.isEmpty()) {
            food = null; // 胜利
        } else {
            food = empty.get(random.nextInt(empty.size()));
        }
    }

    /**
     * 计算曼哈顿距离
     */
    private int manhattan(int[] a, int[] b) {
        return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]);
    }

    /**
     * 启发函数:综合考虑到食物的距离和到蛇尾的距离
     */
    private double heuristic(int x, int y) {
        if (occupied[x][y]) return 0.0;

        double distFood = manhattan(new int[]{x, y}, food);
        // 获取蛇尾位置(队列头部)
        int[] tail = snake.peekFirst();
        double distTail = manhattan(new int[]{x, y}, tail);

        // 食物距离越小越好,蛇尾距离适中最好(预留空间)
        double foodBonus = 1.0 / (distFood + 1.0);
        double tailBonus = 1.0 / (distTail + 1.0);

        return foodBonus * 2.0 + tailBonus * 0.5;
    }

    /**
     * 使用BFS判断从起点是否能到达终点(用于路径可行性检验)
     */
    private boolean isReachable(int[] start, int[] end, boolean[][] occ) {
        boolean[][] vis = new boolean[width][height];
        Queue<int[]> q = new LinkedList<>();
        q.add(start);
        vis[start[0]][start[1]] = true;

        while (!q.isEmpty()) {
            int[] cur = q.poll();
            if (cur[0] == end[0] && cur[1] == end[1]) return true;

            for (int[] d : dirs) {
                int nx = cur[0] + d[0], ny = cur[1] + d[1];
                if (nx >= 0 && nx < width && ny >= 0 && ny < height 
                        && !vis[nx][ny] && !occ[nx][ny]) {
                    vis[nx][ny] = true;
                    q.add(new int[]{nx, ny});
                }
            }
        }
        return false;
    }

    /**
     * 蚁群搜索核心:从当前蛇头出发,模拟多只蚂蚁寻找食物
     * 返回最优移动方向索引
     */
    public int decideMove() {
        int[] head = snake.peekLast();

        // 若食物不存在,游戏结束
        if (food == null) return -1;

        // 统计每个方向的收益
        double[] directionScore = new double[4];
        int[] validCount = new int[4];

        // 模拟多轮迭代
        for (int iter = 0; iter < maxIterations; iter++) {
            // 每只蚂蚁从蛇头出发
            for (int ant = 0; ant < antCount; ant++) {
                int ax = head[0], ay = head[1];
                boolean[][] antVisited = new boolean[width][height];
                antVisited[ax][ay] = true;
                List<int[]> path = new ArrayList<>();
                path.add(new int[]{ax, ay});

                boolean found = false;
                int firstDir = -1;

                // 蚂蚁最大步数限制
                for (int step = 0; step < width * height; step++) {
                    if (ax == food[0] && ay == food[1]) {
                        found = true;
                        break;
                    }

                    // 计算可行邻居
                    List<int[]> neighbors = new ArrayList<>();
                    List<Integer> neighborDirs = new ArrayList<>();
                    for (int d = 0; d < 4; d++) {
                        int nx = ax + dirs[d][0], ny = ay + dirs[d][1];
                        if (nx >= 0 && nx < width && ny >= 0 && ny < height 
                                && !occupied[nx][ny] && !antVisited[nx][ny]) {
                            neighbors.add(new int[]{nx, ny});
                            neighborDirs.add(d);
                        }
                    }

                    if (neighbors.isEmpty()) break; // 蚂蚁死亡

                    // 计算转移概率
                    double[] probs = new double[neighbors.size()];
                    double sum = 0.0;
                    for (int i = 0; i < neighbors.size(); i++) {
                        int[] n = neighbors.get(i);
                        double tau = Math.pow(pheromone[n[0]][n[1]], alpha);
                        double eta = Math.pow(heuristic(n[0], n[1]), beta);
                        probs[i] = tau * eta;
                        sum += probs[i];
                    }

                    // 轮盘赌选择
                    double r = random.nextDouble() * sum;
                    double acc = 0.0;
                    int choice = 0;
                    for (int i = 0; i < probs.length; i++) {
                        acc += probs[i];
                        if (r <= acc) {
                            choice = i;
                            break;
                        }
                    }

                    int[] next = neighbors.get(choice);
                    if (firstDir == -1) firstDir = neighborDirs.get(choice);
                    ax = next[0];
                    ay = next[1];
                    antVisited[ax][ay] = true;
                    path.add(new int[]{ax, ay});
                }

                // 更新信息素和方向得分
                if (found && firstDir != -1) {
                    double deposit = Q / path.size(); // 路径越短,沉积越多
                    directionScore[firstDir] += deposit;
                    validCount[firstDir]++;

                    // 在路径上沉积信息素
                    for (int[] p : path) {
                        pheromone[p[0]][p[1]] += deposit * 0.1;
                    }
                }
            }

            // 信息素挥发
            for (int i = 0; i < width; i++) {
                for (int j = 0; j < height; j++) {
                    pheromone[i][j] *= (1.0 - rho);
                    if (pheromone[i][j] < 0.1) pheromone[i][j] = 0.1;
                }
            }
        }

        // 选择最优方向:综合得分最高且不会造成自困的方向
        int bestDir = -1;
        double bestScore = -1;

        for (int d = 0; d < 4; d++) {
            int nx = head[0] + dirs[d][0], ny = head[1] + dirs[d][1];
            if (nx < 0 || nx >= width || ny < 0 || ny >= height || occupied[nx][ny]) {
                continue; // 撞墙或撞自己
            }

            // 安全检验:移动后蛇尾会空出,判断蛇头能否到达蛇尾
            boolean[][] nextOcc = new boolean[width][height];
            for (int i = 0; i < width; i++) {
                System.arraycopy(occupied[i], 0, nextOcc[i], 0, height);
            }
            nextOcc[head[0]][head[1]] = true; // 蛇头占据新位置
            // 若未吃到食物,蛇尾移动
            if (!(nx == food[0] && ny == food[1])) {
                int[] tail = snake.peekFirst();
                nextOcc[tail[0]][tail[1]] = false;
            }

            int[] nextHead = new int[]{nx, ny};
            int[] nextTail = (nx == food[0] && ny == food[1]) 
                    ? snake.peekFirst() 
                    : snake.peekFirst();
            if (!isReachable(nextHead, nextTail, nextOcc)) {
                continue; // 此方向会导致自困
            }

            double score = directionScore[d] + validCount[d] * 0.1 + heuristic(nx, ny);
            if (score > bestScore) {
                bestScore = score;
                bestDir = d;
            }
        }

        // 若无安全方向,选择任意可行方向
        if (bestDir == -1) {
            for (int d = 0; d < 4; d++) {
                int nx = head[0] + dirs[d][0], ny = head[1] + dirs[d][1];
                if (nx >= 0 && nx < width && ny >= 0 && ny < height && !occupied[nx][ny]) {
                    bestDir = d;
                    break;
                }
            }
        }

        return bestDir;
    }

    /**
     * 执行一步移动
     */
    public boolean step() {
        int dir = decideMove();
        if (dir == -1) return false;

        int[] head = snake.peekLast();
        int nx = head[0] + dirs[dir][0];
        int ny = head[1] + dirs[dir][1];

        // 检查碰撞
        if (nx < 0 || nx >= width || ny < 0 || ny >= height || occupied[nx][ny]) {
            return false; // 游戏结束
        }

        snake.addLast(new int[]{nx, ny});
        occupied[nx][ny] = true;

        if (food != null && nx == food[0] && ny == food[1]) {
            // 吃到食物,长度增加,不删除尾部
            spawnFood();
        } else {
            // 未吃到食物,尾部前移
            int[] tail = snake.pollFirst();
            occupied[tail[0]][tail[1]] = false;
        }

        return true;
    }

    /**
     * 打印当前地图状态
     */
    public void printMap() {
        char[][] map = new char[width][height];
        for (int i = 0; i < width; i++) {
            Arrays.fill(map[i], '.');
        }
        for (int[] body : snake) {
            map[body[0]][body[1]] = 'S';
        }
        int[] head = snake.peekLast();
        map[head[0]][head[1]] = 'H';
        if (food != null) {
            map[food[0]][food[1]] = 'F';
        }

        for (int j = 0; j < height; j++) {
            for (int i = 0; i < width; i++) {
                System.out.print(map[i][j] + " ");
            }
            System.out.println();
        }
        System.out.println("Score: " + (snake.size() - 3));
        System.out.println();
    }

    public static void main(String[] args) {
        AntColonySnake game = new AntColonySnake(15, 15);
        System.out.println("Initial state:");
        game.printMap();

        int steps = 0;
        while (game.step()) {
            steps++;
            if (steps % 50 == 0) {
                game.printMap();
            }
            if (game.snake.size() >= game.width * game.height) {
                System.out.println("Victory! Snake fills the board.");
                break;
            }
        }

        System.out.println("Final state:");
        game.printMap();
        System.out.println("Total steps: " + steps);
        System.out.println("Final score: " + (game.snake.size() - 3));
    }
}

算法复杂度分析

模块 时间复杂度 空间复杂度
蚁群迭代搜索 O(iterations × ants × grid_size) O(grid_size)
BFS安全检验 O(width × height) O(width × height)
信息素更新 O(width × height) O(width × height)
单步决策 O(maxIterations × antCount × W × H + W × H) O(W × H)

关键优化点

  1. 安全检验机制:每次决策前通过BFS验证移动后蛇头是否能到达蛇尾,避免自困死局
  2. 启发函数平衡:食物吸引力和尾部空间预留的加权组合,防止蛇为吃食物而堵死自己
  3. 信息素挥发:防止局部最优陷阱,保持全局探索能力
  4. 方向预筛选:将方向得分与实际安全性结合,避免纯概率选择带来的风险

总结

本文将蚁群优化算法引入贪吃蛇AI决策中,通过信息素引导与启发函数的结合,实现了无需人工规则设计的智能蛇。读者可以通过调整alpha、beta、rho等参数,观察蛇的行为变化,深入理解群体智能算法的收敛特性与参数敏感性。