Лесной пожар
Подожгите одно дерево на краю леса — и через несколько шагов огонь либо потухнет, либо прокатится фронтом до другого края. Поведёт ли он себя так или иначе, решает всего один параметр: насколько густо стоят деревья.
Модель лесного пожара — это клеточный автомат с тремя состояниями (пусто, дерево, горит), в котором горящее дерево за шаг сгорает дотла, а живое дерево загорается, если рядом есть хотя бы одно горящее, благодаря чему огонь распространяется фронтом по лесу.
Эта модель — мостик между чистой математикой и реальной физикой. На ней изучают не только лесные пожары, но и распространение эпидемий, слухов, трещин в материале. И, что важнее всего, она демонстрирует перколяцию — резкий переход системы из состояния «огонь гаснет сразу» в состояние «огонь проходит насквозь» при изменении плотности всего на пару процентов.
Три состояния и правило
Каждая клетка может быть в одном из трёх состояний, которые мы закодируем числами:
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 соседа), поэтому огонь движется связным фронтом.
- Синхронность критична: нужна полноценная копия сетки, иначе фронт «схлопывается».
- Перколяция: при росте плотности леса есть резкий порог, за которым пожар начинает проходить насквозь — критическая плотность.