引言
贪吃蛇是经典的编程练习题目,传统的实现通常依赖人工规则或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) |
关键优化点
- 安全检验机制:每次决策前通过BFS验证移动后蛇头是否能到达蛇尾,避免自困死局
- 启发函数平衡:食物吸引力和尾部空间预留的加权组合,防止蛇为吃食物而堵死自己
- 信息素挥发:防止局部最优陷阱,保持全局探索能力
- 方向预筛选:将方向得分与实际安全性结合,避免纯概率选择带来的风险
总结
本文将蚁群优化算法引入贪吃蛇AI决策中,通过信息素引导与启发函数的结合,实现了无需人工规则设计的智能蛇。读者可以通过调整alpha、beta、rho等参数,观察蛇的行为变化,深入理解群体智能算法的收敛特性与参数敏感性。