Лесной пожар

Подожгите одно дерево на краю леса — и через несколько шагов огонь либо потухнет, либо прокатится фронтом до другого края. Поведёт ли он себя так или иначе, решает всего один параметр: насколько густо стоят деревья.

Модель лесного пожара — это клеточный автомат с тремя состояниями (пусто, дерево, горит), в котором горящее дерево за шаг сгорает дотла, а живое дерево загорается, если рядом есть хотя бы одно горящее, благодаря чему огонь распространяется фронтом по лесу.

Эта модель — мостик между чистой математикой и реальной физикой. На ней изучают не только лесные пожары, но и распространение эпидемий, слухов, трещин в материале. И, что важнее всего, она демонстрирует перколяцию — резкий переход системы из состояния «огонь гаснет сразу» в состояние «огонь проходит насквозь» при изменении плотности всего на пару процентов.

Три состояния и правило

Каждая клетка может быть в одном из трёх состояний, которые мы закодируем числами:

  • 0 — пусто (выгоревшая земля или прогалина), на экране .
  • 1 — дерево (целое, может загореться), на экране #
  • 2 — горит, на экране *

Правило перехода использует окрестность фон Неймана (4 соседа по сторонам) и формулируется так:

  • Горящее дерево (2) за один шаг превращается в пустоту (0) — оно сгорело.
  • Целое дерево (1) загорается (становится 2), если хотя бы один из его 4 соседей горит.
  • Пустая клетка (0) остаётся пустой.

Соберём лес 10x20 со случайной плотностью 0.8, подожжём первое попавшееся дерево и сделаем несколько шагов.

import random
random.seed(5)
R, C = 10, 20
# 0 пусто, 1 дерево, 2 горит
grid = [[1 if random.random() < 0.8 else 0 for _ in range(C)] for _ in range(R)]
# поджигаем первое дерево
for r in range(R):
    done = False
    for c in range(C):
        if grid[r][c] == 1:
            grid[r][c] = 2
            done = True
            break
    if done:
        break

def show(g):
    sym = {0: '.', 1: '#', 2: '*'}
    for row in g:
        print(''.join(sym[x] for x in row))
    print()

def step(g):
    new = [row[:] for row in g]
    for r in range(R):
        for c in range(C):
            if g[r][c] == 2:
                new[r][c] = 0
            elif g[r][c] == 1:
                burning = False
                for dr, dc in ((-1, 0), (1, 0), (0, -1), (0, 1)):
                    rr, cc = r + dr, c + dc
                    if 0 <= rr < R and 0 <= cc < C and g[rr][cc] == 2:
                        burning = True
                if burning:
                    new[r][c] = 2
    return new

print("Шаг 0 (* горит, # дерево, . пусто):")
show(grid)
for s in range(1, 4):
    grid = step(grid)
    print(f"Шаг {s}:")
    show(grid)

Вывод:

Шаг 0 (* горит, # дерево, . пусто):
*##.#.##.#.########.
#######.##..#.###.#.
.###########.#####.#
#.#######.##..######
#.#.###.#.#######.##
.####.###.##.##.####
#.##...########..###
###.#############.##
#####.#####.#..#..##
#..#################

Шаг 1:
.*#.#.##.#.########.
*######.##..#.###.#.
.###########.#####.#
#.#######.##..######
#.#.###.#.#######.##
.####.###.##.##.####
#.##...########..###
###.#############.##
#####.#####.#..#..##
#..#################

Шаг 2:
..*.#.##.#.########.
.*#####.##..#.###.#.
.###########.#####.#
#.#######.##..######
#.#.###.#.#######.##
.####.###.##.##.####
#.##...########..###
###.#############.##
#####.#####.#..#..##
#..#################

Шаг 3:
....#.##.#.########.
..*####.##..#.###.#.
.*##########.#####.#
#.#######.##..######
#.#.###.#.#######.##
.####.###.##.##.####
#.##...########..###
###.#############.##
#####.#####.#..#..##
#..#################

Проследите за звёздочкой * в левом верхнем углу. На шаге 0 горит одно дерево. На шаге 1 оно превратилось в пустоту ., а огонь перекинулся на соседей. Так шаг за шагом по лесу катится фронт пламени: за ним остаётся выжженная земля, перед ним — целый лес. Это и есть суть модели: огонь не прыгает куда попало, он движется непрерывной волной по связанным деревьям.

Как работает под капотом

Обратите внимание на строку new = [row[:] for row in g]. Здесь мы делаем копию сетки, причём копию каждой строки (через срез row[:]), а не просто список ссылок на старые строки. Это снова про синхронность: загорание соседей считаем по старому состоянию g, а записываем в новый new. Без копии огонь распространялся бы внутри одного шага лавиной, мгновенно охватывая весь лес.

Цикл по (dr, dc) с четырьмя парами (-1, 0), (1, 0), (0, -1), (0, 1) — это и есть окрестность фон Неймана: вверх, вниз, влево, вправо. Диагонали мы намеренно не считаем, поэтому огонь не «перепрыгивает» через угол. Проверка 0 <= rr < R and 0 <= cc < C отсекает соседей за краем леса.

Перколяция и критическая плотность

Самое интересное — что будет, если менять плотность леса (вероятность 0.8 в коде). При низкой плотности деревья стоят редкими островками, огонь быстро упирается в пустоту и гаснет, не дойдя до дальнего края. При высокой плотности деревья образуют сплошную связную массу, и пожар почти наверняка прокатывается насквозь. Между этими режимами есть критическая плотность (для квадратной решётки с 4 соседями — около 0.59): чуть ниже неё огонь почти всегда гаснет, чуть выше — почти всегда проходит. Этот резкий скачок и называется перколяционным переходом, и он универсален: те же законы управляют просачиванием воды сквозь пористый камень и протеканием тока через смесь проводника и изолятора.

Частые ошибки

  • Поверхностная копия сетки. Запись new = g[:] копирует только внешний список, а строки остаются общими. Менять new начнёт портить g. Нужен [row[:] for row in g].
  • Распространение внутри шага. Если читать и писать в одну сетку, огонь за один шаг охватит весь связный лес сразу — фронт исчезнет. Только синхронное обновление даёт правильную волну.
  • Не та окрестность. Добавив диагонали (8 соседей), вы измените критическую плотность и скорость фронта — это уже другая модель.
  • Горящее не гаснет. Если забыть правило «2 переходит в 0», деревья будут гореть вечно, и понятие «фронта» потеряет смысл.

Итоги

  • Модель лесного пожара — автомат с тремя состояниями: пусто, дерево, горит.
  • Правило: горящее дерево за шаг сгорает в пустоту, целое дерево загорается рядом с любым горящим соседом.
  • Используется окрестность фон Неймана (4 соседа), поэтому огонь движется связным фронтом.
  • Синхронность критична: нужна полноценная копия сетки, иначе фронт «схлопывается».
  • Перколяция: при росте плотности леса есть резкий порог, за которым пожар начинает проходить насквозь — критическая плотность.
Проверьте себя
1. Почему в коде модели пожара важно делать new = [row[:] for row in g], а не new = g[:]?
AЭто ускоряет программу в несколько раз
Bg[:] копирует только внешний список, а строки остаются общими, и изменения new испортят g
Cg[:] вообще не работает в Python и вызовет ошибку
DТак делать не обязательно, оба варианта дают одинаковый результат
2. Какую окрестность использует базовая модель лесного пожара и почему?
AМура (8 соседей), чтобы огонь перепрыгивал через углы
BФон Неймана (4 соседа), поэтому огонь движется связным фронтом без диагональных прыжков
CТолько одного соседа справа
DВсю сетку целиком
3. Что такое критическая плотность (перколяционный порог) в модели пожара?
AПлотность, при которой деревья перестают расти
BРезкий порог плотности леса: чуть ниже — огонь гаснет, чуть выше — проходит насквозь
CМаксимальное число горящих клеток за шаг
DСкорость движения фронта пламени