每日算法 — 使用java实现康威生命游戏:细胞自动机演化与邻域状态转移规则

引言

康威生命游戏(Conway’s Game of Life)是英国数学家约翰·康威于1970年设计的细胞自动机。它由无限二维方格组成,每个格子有”存活”或”死亡”两种状态,依据简单的邻域规则逐代演化。尽管规则极简,生命游戏却能涌现出极其复杂的模式——从静止的稳定结构到移动的”滑翔机”,甚至能够模拟图灵机。本文将用Java实现一个带图形界面的生命游戏模拟器,重点讲解Moore邻域状态转移规则、周期性检测算法与边界条件处理。

核心算法原理

1. Moore邻域与状态转移规则

每个细胞考察其周围8个邻居(Moore邻域),遵循以下规则:

  • 诞生:死亡细胞恰有3个存活邻居时,下一代复活
  • 存活:存活细胞有2或3个存活邻居时,继续存活
  • 死亡:其他情况(过 lonely 或过 crowded)死亡

用数学语言描述,设 $N$ 为存活邻居数,$S$ 为当前状态(1=存活,0=死亡):

$$
S_{next} = \begin{cases} 1 & (S=0 \land N=3) \lor (S=1 \land N \in {2,3}) \ 0 & \text{otherwise} \end{cases}
$$

2. 边界条件处理

实现时常用三种边界策略:

边界类型 处理方式 适用场景
固定边界 边界外视为死亡 简单演示
循环边界(环面) 上下左右相连 消除边缘效应
无限扩展 动态HashMap存储 研究大型模式

本文采用循环边界,使有限网格行为更接近无限平面。

3. 周期性检测

生命游戏可能进入周期振荡(如”闪光灯”周期为2)。检测方法:

  • 维护最近 $K$ 代状态队列
  • 每代用Zobrist哈希或字符串哈希生成状态指纹
  • 若当前指纹与历史指纹匹配,则检测到周期

Java完整实现

package conway;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;

/**
 * 康威生命游戏模拟器
 * 核心算法:Moore邻域状态转移 + 循环边界 + 周期性检测
 */
public class ConwayLifeGame extends JPanel implements Runnable {
    private static final int CELL_SIZE = 10;
    private static final int GRID_WIDTH = 80;
    private static final int GRID_HEIGHT = 60;
    private static final int DELAY_MS = 100;

    // 当前代与下一代的网格状态
    private boolean[][] currentGrid;
    private boolean[][] nextGrid;

    // 周期性检测:存储最近20代的状态哈希
    private LinkedList<Long> stateHistory = new LinkedList<>();
    private static final int HISTORY_SIZE = 20;
    private boolean isPaused = false;

    public ConwayLifeGame() {
        currentGrid = new boolean[GRID_HEIGHT][GRID_WIDTH];
        nextGrid = new boolean[GRID_HEIGHT][GRID_WIDTH];

        // 初始化经典模式:滑翔机 + 闪光灯 + 蜂巢
        initClassicPatterns();

        setPreferredSize(new Dimension(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE));
        setBackground(Color.BLACK);

        // 鼠标点击切换细胞状态
        addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                int col = e.getX() / CELL_SIZE;
                int row = e.getY() / CELL_SIZE;
                if (row >= 0 && row < GRID_HEIGHT && col >= 0 && col < GRID_WIDTH) {
                    currentGrid[row][col] = !currentGrid[row][col];
                    repaint();
                }
            }
        });

        // 空格键暂停/继续
        setFocusable(true);
        addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                if (e.getKeyCode() == KeyEvent.VK_SPACE) {
                    isPaused = !isPaused;
                }
            }
        });
    }

    /**
     * 初始化经典模式:
     * - 滑翔机(Glider):周期性移动的模式
     * - 闪光灯(Blinker):周期为2的振荡器
     * - 蜂巢(Beehive):静止稳定结构
     */
    private void initClassicPatterns() {
        // 滑翔机(左上角)
        int[][] glider = {{1,0}, {2,1}, {0,2}, {1,2}, {2,2}};
        for (int[] p : glider) currentGrid[p[0]+5][p[1]+5] = true;

        // 闪光灯(中央)
        int[][] blinker = {{1,0}, {1,1}, {1,2}};
        for (int[] p : blinker) currentGrid[p[0]+20][p[1]+30] = true;

        // 蜂巢(右下角)
        int[][] beehive = {{1,0}, {2,0}, {0,1}, {3,1}, {1,2}, {2,2}};
        for (int[] p : beehive) currentGrid[p[0]+40][p[1]+50] = true;
    }

    /**
     * 核心算法:计算下一代网格状态
     * 使用循环边界(toroidal)处理边缘
     */
    public void nextGeneration() {
        for (int row = 0; row < GRID_HEIGHT; row++) {
            for (int col = 0; col < GRID_WIDTH; col++) {
                int neighbors = countLiveNeighbors(row, col);
                boolean isAlive = currentGrid[row][col];

                // 康威生命规则
                if (isAlive) {
                    nextGrid[row][col] = (neighbors == 2 || neighbors == 3);
                } else {
                    nextGrid[row][col] = (neighbors == 3);
                }
            }
        }

        // 交换当前代与下一代
        boolean[][] temp = currentGrid;
        currentGrid = nextGrid;
        nextGrid = temp;

        // 周期性检测
        detectPeriodicity();
    }

    /**
     * 计算指定位置的存活邻居数(Moore邻域,8方向)
     * 使用循环边界:(row + r + GRID_HEIGHT) % GRID_HEIGHT
     */
    private int countLiveNeighbors(int row, int col) {
        int count = 0;
        for (int dr = -1; dr <= 1; dr++) {
            for (int dc = -1; dc <= 1; dc++) {
                if (dr == 0 && dc == 0) continue; // 跳过自身

                // 循环边界处理
                int nr = (row + dr + GRID_HEIGHT) % GRID_HEIGHT;
                int nc = (col + dc + GRID_WIDTH) % GRID_WIDTH;

                if (currentGrid[nr][nc]) count++;
            }
        }
        return count;
    }

    /**
     * 周期性检测:使用Zobrist哈希快速比较状态
     * 若检测到周期,在控制台输出警告
     */
    private void detectPeriodicity() {
        long hash = computeZobristHash();

        // 检查是否在历史中出现
        int period = 0;
        for (int i = stateHistory.size() - 1; i >= 0; i--) {
            period++;
            if (stateHistory.get(i) == hash) {
                System.out.println("检测到周期振荡!周期=" + period + " 代");
                break;
            }
        }

        stateHistory.addLast(hash);
        if (stateHistory.size() > HISTORY_SIZE) {
            stateHistory.removeFirst();
        }
    }

    /**
     * Zobrist哈希:为每个(r,c)预生成随机数,快速计算状态指纹
     * 时间复杂度O(存活细胞数),远快于字符串哈希
     */
    private static final long[][] ZOBRIST_TABLE = generateZobristTable();

    private static long[][] generateZobristTable() {
        long[][] table = new long[GRID_HEIGHT][GRID_WIDTH];
        Random rand = new Random(42); // 固定种子保证可复现
        for (int i = 0; i < GRID_HEIGHT; i++) {
            for (int j = 0; j < GRID_WIDTH; j++) {
                table[i][j] = rand.nextLong();
            }
        }
        return table;
    }

    private long computeZobristHash() {
        long hash = 0;
        for (int i = 0; i < GRID_HEIGHT; i++) {
            for (int j = 0; j < GRID_WIDTH; j++) {
                if (currentGrid[i][j]) {
                    hash ^= ZOBRIST_TABLE[i][j];
                }
            }
        }
        return hash;
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.setColor(Color.GREEN);
        for (int row = 0; row < GRID_HEIGHT; row++) {
            for (int col = 0; col < GRID_WIDTH; col++) {
                if (currentGrid[row][col]) {
                    g.fillRect(col * CELL_SIZE, row * CELL_SIZE, CELL_SIZE - 1, CELL_SIZE - 1);
                }
            }
        }
    }

    @Override
    public void run() {
        while (true) {
            if (!isPaused) {
                nextGeneration();
                repaint();
            }
            try {
                Thread.sleep(DELAY_MS);
            } catch (InterruptedException e) {
                break;
            }
        }
    }

    public static void main(String[] args) {
        JFrame frame = new JFrame("康威生命游戏 - Java实现");
        ConwayLifeGame game = new ConwayLifeGame();
        frame.add(game);
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);

        new Thread(game).start();
    }
}

算法复杂度分析

指标 复杂度 说明
时间复杂度(每代) $O(W \times H)$ 遍历每个细胞并统计8邻域
空间复杂度 $O(W \times H)$ 双缓冲存储当前代与下一代
邻居统计优化 $O(1)$ 每细胞 固定8方向循环,常数时间
Zobrist哈希 $O(K)$ $K$ 为存活细胞数,远小于 $W \times H$

扩展思考

  1. 大规模并行:可用Java Fork/Join框架将网格分块并行计算,适合 $10^6 \times 10^6$ 级别模拟
  2. 规则泛化:将规则参数化为 $B3/S23$(诞生/存活),可探索 $B36/S23$ 等变体的”高生命”行为
  3. 图灵完备性:生命游戏已被证明是图灵完备的,理论上可用滑翔机碰撞实现任意计算

总结

康威生命游戏展示了”简单规则产生复杂行为”的涌现现象。通过Java实现,我们掌握了细胞自动机的核心编程范式:离散网格 + 局部规则 + 同步更新。Zobrist哈希的引入也展示了博弈状态编码技巧在实际仿真中的妙用。