每日算法 — 使用java实现模拟城市:元胞自动机与贪心区域规划算法

一、游戏背景与核心机制

“模拟城市”(SimCity)是城市规划类游戏的鼻祖。玩家作为市长,需要在有限的土地上合理规划住宅区、商业区、工业区,并提供道路、水电、警消等基础设施,使城市繁荣发展。游戏的核心乐趣在于观察简单规则如何涌现出复杂的城市演化行为。

本文用Java实现一个简化版的模拟城市算法原型,聚焦两个核心问题:
1. 城市演化:用元胞自动机(Cellular Automata)模拟区域随时间的自然发展
2. 区域规划:用贪心算法为新设施寻找最优建设位置

二、核心算法原理

2.1 元胞自动机模型

将城市地图划分为网格,每个格子具有一个状态(空地、住宅、商业、工业、道路等)。每个时间步,格子的新状态仅由其当前状态和邻居状态决定。经典规则包括:
– 住宅需要临近道路和就业(商业/工业)
– 商业需要消费者(住宅)和货运(工业)
– 工业需要劳动力(住宅)和道路连接
– 基础设施(警消、医院)提升周边区域价值

2.2 区域价值评估

每个格子的”土地价值”由多因素加权决定:
– 到道路的距离(越近越好)
– 到水源/电厂的距离
– 到警消/医院的距离
– 周边同类/互补区域密度
– 污染与噪音(工业区负面影响)

2.3 贪心规划策略

当需要建设新设施时,贪心算法评估每个空地的综合价值,选择价值最高的位置。综合价值 = 设施效用(覆盖更多需求区域) – 建设成本。

三、Java实现

3.1 城市网格与基础设施建模

import java.util.*;

/**
 * 模拟城市简化版:元胞自动机演化 + 贪心区域规划
 */
public class SimCitySimulator {

    // 地块类型
    public static final int EMPTY = 0;
    public static final int RESIDENTIAL = 1;  // 住宅
    public static final int COMMERCIAL = 2;   // 商业
    public static final int INDUSTRIAL = 3;   // 工业
    public static final int ROAD = 4;         // 道路
    public static final int POWER_PLANT = 5;  // 电厂
    public static final int WATER_TOWER = 6;  // 水塔
    public static final int POLICE = 7;       // 警察局
    public static final int FIRE_STATION = 8; // 消防局
    public static final int HOSPITAL = 9;     // 医院
    public static final int PARK = 10;        // 公园

    public static final String[] TYPE_NAMES = {
        "空地", "住宅", "商业", "工业", "道路", "电厂", "水塔", "警局", "消防", "医院", "公园"
    };

    private int width, height;
    private int[][] grid;          // 地块类型
    private int[][] density;       // 建筑密度 0-5
    private double[][] landValue;  // 土地价值 0.0-1.0
    private double[][] pollution;  // 污染指数 0.0-1.0
    private int[][] powerMap;      // 电力覆盖 0/1
    private int[][] waterMap;      // 供水覆盖 0/1

    // 城市统计
    public int population = 0;
    public int funds = 20000; // 初始资金

    public SimCitySimulator(int width, int height) {
        this.width = width;
        this.height = height;
        this.grid = new int[height][width];
        this.density = new int[height][width];
        this.landValue = new double[height][width];
        this.pollution = new double[height][width];
        this.powerMap = new int[height][width];
        this.waterMap = new int[height][width];
    }

    /**
     * 初始化城市:中心道路 + 少量基础设施
     */
    public void initCity() {
        // 十字主干道
        int cx = width / 2, cy = height / 2;
        for (int i = 0; i < width; i++) grid[cy][i] = ROAD;
        for (int i = 0; i < height; i++) grid[i][cx] = ROAD;

        // 基础公共设施
        grid[cy - 2][cx - 2] = POWER_PLANT;
        grid[cy - 2][cx + 2] = WATER_TOWER;
        grid[cy + 2][cx - 2] = POLICE;
        grid[cy + 2][cx + 2] = FIRE_STATION;

        updateInfrastructure();
    }

    /**
     * 更新基础设施覆盖图(BFS从设施向外扩散)
     */
    public void updateInfrastructure() {
        // 电力覆盖:从电厂BFS,沿道路传播
        powerMap = computeCoverage(POWER_PLANT, 8);
        // 供水覆盖:从水塔BFS,沿道路传播
        waterMap = computeCoverage(WATER_TOWER, 8);
    }

    private int[][] computeCoverage(int facilityType, int maxDist) {
        int[][] coverage = new int[height][width];
        Queue<int[]> queue = new LinkedList<>();

        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                if (grid[y][x] == facilityType) {
                    queue.offer(new int[]{x, y, 0});
                    coverage[y][x] = 1;
                }
            }
        }

        int[][] dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
        while (!queue.isEmpty()) {
            int[] cur = queue.poll();
            int x = cur[0], y = cur[1], dist = cur[2];
            if (dist >= maxDist) continue;

            for (int[] d : dirs) {
                int nx = x + d[0], ny = y + d[1];
                if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
                if (coverage[ny][nx] == 1) continue;
                // 基础设施沿道路和空地传播
                if (grid[ny][nx] == ROAD || grid[ny][nx] == EMPTY || 
                    grid[ny][nx] == RESIDENTIAL || grid[ny][nx] == COMMERCIAL ||
                    grid[ny][nx] == INDUSTRIAL) {
                    coverage[ny][nx] = 1;
                    queue.offer(new int[]{nx, ny, dist + 1});
                }
            }
        }
        return coverage;
    }

3.2 元胞自动机演化引擎

    /**
     * 元胞自动机演化一步:根据邻居状态更新每个格子
     */
    public void evolveStep() {
        int[][] newGrid = new int[height][width];
        int[][] newDensity = new int[height][width];

        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                newGrid[y][x] = grid[y][x];
                newDensity[y][x] = density[y][x];

                if (grid[y][x] == EMPTY) {
                    // 空地可能自发发展为新区
                    double developChance = computeDevelopmentChance(x, y);
                    if (Math.random() < developChance) {
                        int newType = chooseZoneType(x, y);
                        newGrid[y][x] = newType;
                        newDensity[y][x] = 1;
                    }
                } else if (grid[y][x] == RESIDENTIAL || grid[y][x] == COMMERCIAL || grid[y][x] == INDUSTRIAL) {
                    // 已有区域可能升级或降级
                    double growChance = computeGrowthChance(x, y);
                    double decayChance = computeDecayChance(x, y);

                    if (Math.random() < growChance && density[y][x] < 5) {
                        newDensity[y][x] = density[y][x] + 1;
                    } else if (Math.random() < decayChance && density[y][x] > 0) {
                        newDensity[y][x] = density[y][x] - 1;
                        if (newDensity[y][x] == 0) newGrid[y][x] = EMPTY; // 废弃
                    }
                }
            }
        }

        grid = newGrid;
        density = newDensity;
        updateLandValue();
        updatePollution();
        updatePopulation();
    }

    /**
     * 计算空地发展为建筑的概率
     */
    private double computeDevelopmentChance(int x, int y) {
        if (powerMap[y][x] == 0 || waterMap[y][x] == 0) return 0.0; // 无水无电不发展
        if (!hasRoadNeighbor(x, y)) return 0.0; // 无路不通达

        double baseChance = 0.05;
        double valueBonus = landValue[y][x] * 0.1;
        double pollutePenalty = pollution[y][x] * 0.15;

        return Math.min(baseChance + valueBonus - pollutePenalty, 0.3);
    }

    /**
     * 根据周边需求选择新区类型(住宅/商业/工业)
     */
    private int chooseZoneType(int x, int y) {
        int[] counts = countNeighborTypes(x, y, 3);
        int res = counts[RESIDENTIAL], com = counts[COMMERCIAL], ind = counts[INDUSTRIAL];

        // 需求平衡逻辑:住宅需要就业岗位,商业需要消费者,工业需要劳动力
        double needRes = Math.max(0, ind + com - res); // 就业岗位多于住房则缺住宅
        double needCom = Math.max(0, res - com);       // 消费者多于商店则缺商业
        double needInd = Math.max(0, res - ind);       // 劳动力多于工厂则缺工业

        double total = needRes + needCom + needInd + 1;
        double r = Math.random() * total;

        if (r < needRes) return RESIDENTIAL;
        if (r < needRes + needCom) return COMMERCIAL;
        return INDUSTRIAL;
    }

    /**
     * 计算区域升级概率
     */
    private double computeGrowthChance(int x, int y) {
        double base = 0.1;
        double valueFactor = landValue[y][x] * 0.2;
        double infraBonus = (hasServiceNeighbor(x, y) ? 0.1 : 0); // 警消医院公园加成
        return Math.min(base + valueFactor + infraBonus, 0.5);
    }

    /**
     * 计算区域衰败概率
     */
    private double computeDecayChance(int x, int y) {
        double base = 0.02;
        double polluteFactor = pollution[y][x] * 0.3;
        double crimeFactor = (landValue[y][x] < 0.2) ? 0.1 : 0; // 低地价高犯罪
        double noPower = (powerMap[y][x] == 0) ? 0.2 : 0;
        double noWater = (waterMap[y][x] == 0) ? 0.2 : 0;
        return Math.min(base + polluteFactor + crimeFactor + noPower + noWater, 0.8);
    }

    /**
     * 统计某类型在半径r内的邻居数量
     */
    private int[] countNeighborTypes(int x, int y, int radius) {
        int[] counts = new int[11];
        for (int dy = -radius; dy <= radius; dy++) {
            for (int dx = -radius; dx <= radius; dx++) {
                int nx = x + dx, ny = y + dy;
                if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
                if (grid[ny][nx] >= 0 && grid[ny][nx] < counts.length) {
                    counts[grid[ny][nx]]++;
                }
            }
        }
        return counts;
    }

    private boolean hasRoadNeighbor(int x, int y) {
        int[][] dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
        for (int[] d : dirs) {
            int nx = x + d[0], ny = y + d[1];
            if (nx >= 0 && nx < width && ny >= 0 && ny < height && grid[ny][nx] == ROAD) return true;
        }
        return false;
    }

    private boolean hasServiceNeighbor(int x, int y) {
        for (int dy = -2; dy <= 2; dy++) {
            for (int dx = -2; dx <= 2; dx++) {
                int nx = x + dx, ny = y + dy;
                if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
                int t = grid[ny][nx];
                if (t == POLICE || t == FIRE_STATION || t == HOSPITAL || t == PARK) return true;
            }
        }
        return false;
    }

3.3 土地价值与污染系统

    /**
     * 更新全图土地价值(基于基础设施和区域搭配)
     */
    public void updateLandValue() {
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                if (grid[y][x] == ROAD || grid[y][x] == POWER_PLANT || grid[y][x] == WATER_TOWER) {
                    landValue[y][x] = 0;
                    continue;
                }

                double value = 0.3; // 基础价值

                // 道路 proximity
                int roadDist = findNearestTypeDistance(x, y, ROAD);
                value += Math.max(0, (5 - roadDist) * 0.05);

                // 公园提升(高价值)
                int parkDist = findNearestTypeDistance(x, y, PARK);
                if (parkDist <= 3) value += (4 - parkDist) * 0.08;

                // 警消提升安全
                int policeDist = findNearestTypeDistance(x, y, POLICE);
                int fireDist = findNearestTypeDistance(x, y, FIRE_STATION);
                value += Math.max(0, (4 - policeDist) * 0.03);
                value += Math.max(0, (4 - fireDist) * 0.03);

                // 医院
                int hospDist = findNearestTypeDistance(x, y, HOSPITAL);
                value += Math.max(0, (5 - hospDist) * 0.04);

                // 远离工业区
                int indDist = findNearestTypeDistance(x, y, INDUSTRIAL);
                value -= Math.max(0, (6 - indDist) * 0.05);

                // 互补区域搭配加成
                if (grid[y][x] == RESIDENTIAL) {
                    int comDist = findNearestTypeDistance(x, y, COMMERCIAL);
                    value += Math.max(0, (4 - comDist) * 0.03);
                }

                landValue[y][x] = Math.max(0.05, Math.min(1.0, value));
            }
        }
    }

    /**
     * 更新污染分布(从工业区向外扩散,随距离衰减)
     */
    public void updatePollution() {
        // 重置
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) pollution[y][x] = 0;
        }

        // 工业区和电厂产生污染
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                if (grid[y][x] == INDUSTRIAL) {
                    spreadEffect(x, y, pollution, 0.4, 4); // 工业污染半径4
                } else if (grid[y][x] == POWER_PLANT) {
                    spreadEffect(x, y, pollution, 0.3, 5); // 电厂污染半径5
                }
            }
        }

        // 公园降低污染
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                if (grid[y][x] == PARK) {
                    spreadEffect(x, y, pollution, -0.15, 3); // 公园净化半径3
                }
            }
        }

        // 钳制到0-1
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                pollution[y][x] = Math.max(0, Math.min(1, pollution[y][x]));
            }
        }
    }

    /**
     * 从源点向外扩散效果(污染或正面效果)
     */
    private void spreadEffect(int sx, int sy, double[][] field, double peak, int radius) {
        for (int dy = -radius; dy <= radius; dy++) {
            for (int dx = -radius; dx <= radius; dx++) {
                int nx = sx + dx, ny = sy + dy;
                if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
                double dist = Math.sqrt(dx*dx + dy*dy);
                if (dist > radius) continue;
                double effect = peak * (1 - dist / radius);
                field[ny][nx] += effect;
            }
        }
    }

    /**
     * 查找最近某类型地块的曼哈顿距离
     */
    private int findNearestTypeDistance(int x, int y, int type) {
        int best = Integer.MAX_VALUE;
        for (int dy = 0; dy < height; dy++) {
            for (int dx = 0; dx < width; dx++) {
                if (grid[dy][dx] == type) {
                    int dist = Math.abs(dx - x) + Math.abs(dy - y);
                    best = Math.min(best, dist);
                }
            }
        }
        return best == Integer.MAX_VALUE ? 99 : best;
    }

    private void updatePopulation() {
        population = 0;
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                if (grid[y][x] == RESIDENTIAL) {
                    population += density[y][x] * 50; // 每级密度50人
                }
            }
        }
    }

3.4 贪心区域规划算法

    /**
     * 贪心算法:为指定设施寻找最优建设位置
     * @param facilityType 设施类型
     * @return 推荐坐标 [x, y],null表示无合适位置
     */
    public int[] findOptimalLocation(int facilityType) {
        double bestScore = Double.NEGATIVE_INFINITY;
        int[] bestPos = null;

        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                if (grid[y][x] != EMPTY) continue; // 只能建在空地上

                double score = evaluateLocation(x, y, facilityType);
                if (score > bestScore) {
                    bestScore = score;
                    bestPos = new int[]{x, y};
                }
            }
        }

        return bestPos;
    }

    /**
     * 评估某位置建设特定设施的得分
     */
    private double evaluateLocation(int x, int y, int facilityType) {
        double score = 0;

        switch (facilityType) {
            case ROAD:
                // 道路:连接尽量多的非道路区域,填补交通盲区
                score = countNonRoadNeighbors(x, y) * 10;
                score -= findNearestTypeDistance(x, y, ROAD) * 2; // 不要靠太近
                break;

            case PARK:
                // 公园:服务尽量多住宅,远离工业区
                score = countTypeInRadius(x, y, RESIDENTIAL, 3) * 15;
                score -= countTypeInRadius(x, y, INDUSTRIAL, 4) * 10;
                score += landValue[y][x] * 20;
                break;

            case POLICE:
            case FIRE_STATION:
                // 警消:覆盖高犯罪/高风险区域(低地价、高密度住宅/工业)
                score = countTypeInRadius(x, y, RESIDENTIAL, 4) * 8;
                score += countTypeInRadius(x, y, COMMERCIAL, 4) * 6;
                score += countTypeInRadius(x, y, INDUSTRIAL, 4) * 5;
                // 避免与其他同类设施重叠覆盖
                int nearestSame = findNearestTypeDistance(x, y, facilityType);
                if (nearestSame < 5) score -= (5 - nearestSame) * 20;
                break;

            case HOSPITAL:
                // 医院:服务高人口密度区域
                score = countTypeInRadius(x, y, RESIDENTIAL, 5) * 10;
                score += countTypeInRadius(x, y, COMMERCIAL, 5) * 5;
                score += landValue[y][x] * 15;
                break;

            case POWER_PLANT:
            case WATER_TOWER:
                // 基础设施:中心位置,方便向外辐射
                int cx = width / 2, cy = height / 2;
                score = 100 - (Math.abs(x - cx) + Math.abs(y - cy));
                score -= countTypeInRadius(x, y, RESIDENTIAL, 3) * 5; // 远离住宅区
                break;
        }

        // 通用惩罚
        if (!hasRoadNeighbor(x, y)) score -= 50; // 必须临路

        return score;
    }

    private int countNonRoadNeighbors(int x, int y) {
        int count = 0;
        int[][] dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
        for (int[] d : dirs) {
            int nx = x + d[0], ny = y + d[1];
            if (nx >= 0 && nx < width && ny >= 0 && ny < height && grid[ny][nx] != ROAD) count++;
        }
        return count;
    }

    private int countTypeInRadius(int x, int y, int type, int radius) {
        int count = 0;
        for (int dy = -radius; dy <= radius; dy++) {
            for (int dx = -radius; dx <= radius; dx++) {
                int nx = x + dx, ny = y + dy;
                if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
                if (grid[ny][nx] == type) count++;
            }
        }
        return count;
    }

    /**
     * 执行贪心规划:按优先级建设缺失设施
     */
    public void greedyPlan() {
        int[][] priorities = {
            {ROAD, 3}, {RESIDENTIAL, 5}, {COMMERCIAL, 3}, {INDUSTRIAL, 2},
            {PARK, 2}, {POLICE, 1}, {FIRE_STATION, 1}
        };

        for (int[] plan : priorities) {
            int type = plan[0];
            int count = plan[1];
            for (int i = 0; i < count; i++) {
                int[] pos = findOptimalLocation(type);
                if (pos != null) {
                    grid[pos[1]][pos[0]] = type;
                    if (type == RESIDENTIAL || type == COMMERCIAL || type == INDUSTRIAL) {
                        density[pos[1]][pos[0]] = 1;
                    }
                    System.out.println("建设 " + TYPE_NAMES[type] + " 于 (" + pos[0] + "," + pos[1] + ")");
                }
            }
        }
        updateInfrastructure();
        updateLandValue();
        updatePollution();
    }

3.5 可视化与主程序

    /**
     * ASCII可视化当前城市状态
     */
    public void printCity() {
        char[] symbols = {'.', 'R', 'C', 'I', '=', 'P', 'W', '*', 'F', 'H', 'T'};
        System.out.println("\n=== 城市地图 (人口: " + population + ") ===");
        System.out.print("  ");
        for (int x = 0; x < width; x++) System.out.print(x % 10);
        System.out.println();

        for (int y = 0; y < height; y++) {
            System.out.print(y % 10 + " ");
            for (int x = 0; x < width; x++) {
                char c = symbols[grid[y][x]];
                if (density[y][x] > 0 && grid[y][x] >= RESIDENTIAL && grid[y][x] <= INDUSTRIAL) {
                    c = (char)('0' + density[y][x]); // 密度数字覆盖
                }
                System.out.print(c);
            }
            System.out.println();
        }

        // 打印统计
        int[] counts = new int[11];
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) counts[grid[y][x]]++;
        }
        System.out.println("统计: 住宅=" + counts[RESIDENTIAL] + " 商业=" + counts[COMMERCIAL] + 
                          " 工业=" + counts[INDUSTRIAL] + " 道路=" + counts[ROAD]);
    }

    public static void main(String[] args) {
        SimCitySimulator city = new SimCitySimulator(20, 20);
        city.initCity();

        System.out.println("=== 模拟城市演化与规划 ===\n");
        System.out.println("【初始状态】");
        city.printCity();

        // 阶段1:贪心规划基础设施
        System.out.println("\n【阶段1:贪心规划】");
        city.greedyPlan();
        city.printCity();

        // 阶段2:元胞自动机演化
        System.out.println("\n【阶段2:元胞自动机演化】");
        for (int step = 1; step <= 10; step++) {
            city.evolveStep();
            if (step % 3 == 0) {
                System.out.println("\n--- 第 " + step + " 步 ---");
                city.printCity();
            }
        }

        // 阶段3:动态规划响应
        System.out.println("\n【阶段3:响应式规划(新增公园)】");
        int[] parkPos = city.findOptimalLocation(PARK);
        if (parkPos != null) {
            city.grid[parkPos[1]][parkPos[0]] = PARK;
            System.out.println("新增公园于 (" + parkPos[0] + "," + parkPos[1] + ")");
        }
        city.updatePollution();
        city.updateLandValue();
        city.printCity();

        System.out.println("\n最终人口: " + city.population);
    }
}

四、复杂度分析

模块 时间复杂度 空间复杂度 说明
元胞自动机演化 O(W·H) O(W·H) 遍历每个格子,检查邻居
土地价值更新 O(W·H·T) O(W·H) T为设施类型数
污染扩散 O(W·H·R²) O(W·H) R为最大扩散半径
贪心选址 O(W·H·R²) O(1) 评估每个空地
基础设施覆盖 O(W·H) O(W·H) BFS传播

对于20×20的小型城市,单步演化在毫秒级完成;扩展到100×100时,通过空间索引(如R树)可将复杂度降至O(N log N),N为设施数量。

五、总结

本文用Java实现了一个简化版模拟城市的核心算法引擎,涵盖元胞自动机演化与贪心区域规划两大模块:

  1. 元胞自动机:通过局部邻居规则模拟城市自然发展,包含区域升级、衰败和自发建设
  2. 多因素评估:综合考虑道路通达、基础设施覆盖、环境污染和区域互补性计算土地价值
  3. 贪心规划器:针对不同设施类型设计专用评分函数,自动推荐最优建设位置
  4. 系统耦合:基础设施覆盖、污染扩散和土地价值三者相互影响,形成闭环反馈

读者可在此基础上加入交通流模拟(A*寻路评估拥堵)、供需平衡市场模型和玩家预算约束,构建更完整的模拟城市游戏体验。元胞自动机规则也可尝试用遗传算法自动优化,寻找使城市最繁荣的规则参数组合。