Журнал о программированнии на языках Blitz3D, BlitzPlus, BlitzMax

Бесконечная «Жизнь»

Материал из Blitz Et Cetera

Перейти к: навигация, поиск

Введение

В этой заключительной статье из цикла, будем избавляться от основного ограничения предыдущего алгоритма - размера поля. Это, конечно, приведет к заметному снижению скорости, но в некоторых задачах без гигантского поля просто не обойтись. Если взять поле со стороной, скажем, миллион ячеек (не заполняя его клетками случайным образом и не обрабатывая при этом около 1012 / 2 ячеек) и поместить туда организм, даже распространяющийся на все поле, то он будет состоять из относительно небольшого количества клеток, поэтому скороть будет вполне приемлемой. Естественно, бесконечность в буквальном смысле реализовать не удастся, но поля со стороной в 4 миллиарда ячеек (232) будет вполне достаточно.

Основная проблема алгоритмов "Жизни" состоит в определении, заполнена ли ячейка, соседняя с данной. До сих пор она легко решалась созданием массива для ячеек всего доступного поля. Теперь доступны два варианта решения (может быть есть и другие):

  • Разбить поле на квадратные блоки с фиксированным количеством ячеек и обрабатывать каждый блок отдельно. В таком случае, придется создать буфер по размеру блока, очищать и заполнять его информацией несколько раз за цикл (что существенно замедляет работу), создать массив для хранения координат активных блоков и, самое главное, обеспечить стыковку блоков. Попробуйте реализовать этот алгоритм самостоятельно.
  • Организовать древовидную структуру доступа к ячейкам. При этом, создаются цепочки, позволяющие добраться до соседних пикселов. Мало того, что такой алгоритм сам по себе сложен, создание и удаление цепочек замедляет процесс. Но со всем этим вполне можно примириться. Рассмотрим этот алгоритм подробнее.

Одномерная древовидная структура

Чтобы понять суть и преимущества древовидной структуры, еще раз коснемся принципов действия десятичной системы счисления. В древности люди для обозначения количества объектов делали зарубки, рисовали черточки или завязывали узелки (это не что иное, как 1-ричная система счисления). Но чтобы обозначить, скажем, 123 объекта, им пришлось бы делать 123 зарубки, в то время как нам достаточно написать 3 символа. То есть, взять один раз по сто, два по десять и еще три объекта. В итоге, чтобы "добраться" до искомого числа нам нужно сделать 3 а не 123 шага. Причем, при увеличении шагов, количество возможных чисел возрастает в геометрической прогрессии. Теперь взгляните, как можно построить древовидную структуру для двоичной системы:

Здесь три уровня, на каждом из которых можно продолжить движение вниз (0, темно-серый блок) или сделать шаг вправо (1, светло-серый блок). Чем выше уровень, тем больше шаг (степень двойки).Если все числа (блоки первого уровня - снизу) не нужны, то структуру можно упростить. Например, необходимы числа 1, 4, 6, 7:

Выберем блок №6 в качестве исходной точки и определим последовательность шагов для достижения каждого из оставшихся блоков: №7 - вверх, шаг вправо, вниз; №4 - вверх, вверх, шаг влево, вниз, вниз; №1 - вверх, вверх, шаг влево, вверх, шаг влево, вниз, вниз, шаг вправо, вниз. Нетрудно заметить, что путь разбивается на два этапа: 1 - движение вверх, к корню дерева (иногда - шаги влево), 2 - вниз к искомому блоку (иногда - шаги вправо). Т. к. при подъеме мы переходим на указатель более высокого уровня, то шаг влево совершается автоматически.

В нашем алгоритме основное значение имеет указатель на исходный блок и положение конечного блока относительно исходного. Итак, дан исходный блок и приращение до конечного (N, dx). Теперь при поиске алгоритма создания пути возникают две задачи: на сколько уровней подниматься, когда шагать вправо при спуске.

Если приращение не равно 0 (в таком случае результат - исходный блок), то подъем вверх на один уровень обязателен. Поднимаясь от блоков №6 или №7, мы переходим на блок, с которого доступен переход на один из этих блоков. То есть, для данных (№6, +1) и (№7, -1) достаточно подняться только на один уровень. Будем отслеживать положение текущего блока изменением приращения. Рассмотрим ситуацию с поиском пути для данных (№7, -1): поднимаясь на блок более высокого уровня - "11", сделан шаг влево, увеличиваем приращение на 1 (так как мы приблизились к искомому блоку №6). Данные изменились - ("11",0). Для данных (№6, +1) результат подъема будет ("11", +1), т. к. шаг не был сделан. Так как от блока "11" можно сделать шаг (+1) или спуститься (+0), определим диапазон этого блока (он будет равен диапазону всех блоков этого уровня) - (0 - 1). Значит, для данных ("11", 0) и ("11", 1) подъем можно прекратить - приращения входят в диапазон блока.

Данные (№6, -2), где искомый блок - №4: подъем вверх - ("11",-2), диапазон (0 - 1). Приращение в диапазон не попадает, поднимаемся еще. Заметьте, шаг увеличивается в 2 раза. Поэтому, т. к. сделан шаг влево - увеличиваем приращение не на 1, а на 2 - данные ("1", 0), диапазон тоже увеличивается в два раза - (0 - 3). Теперь можно спускаться.

Еще более усложненный пример - данные (№6, -5), где искомый блок - №1. Подъем: (№6, -5), (0); ("11",-5), (0 - 1); ("1", -3), (0 - 3); ("корень", 1), (0 - 7). Подъем завершен, но как теперь правильно спуститься? Выручит двоичное представление приращения (оно теперь - 001). Так как в третьем разряде - 0, при спуске на третий уровень шаг вправо не делаем. То же самое и для второго уровня. В первом разряде - единица, поэтому шагаем вправо и приходим к искомому блоку.

А что делать, если искомые блоки выходят за пределы дерева? Взгляните на схему (сверху - путь для (№7, +4), снизу - (№1, -4)):

Cтруктуре придана гибкость за счет отказа от фиксированной нумерации блоков. Добавляются указатели четвертого уровня: в первом случае, бывший "корень" становится "темно-серым", чтобы можно было сделать шаг вправо к искомому блоку, во втором - "светло-серым", чтобы можно было сделать шаг влево. Еще один аспект - если соответствующие указатели отсутствуют, они создаются по мере надобности. Вообще, операция "надстройки" структуры повторяется до тех пор, пока блок не станет доступен (для отдаленных блоков).

Особенности алгоритма + программа

Так как поле двумерно, придется создавать двумерную древовидную структуру. Ее можно синтезировать из двух одномерных - для оси X и для оси Y. При этом возникают два нюанса: каждый указатель теперь указывает не на два, а на четыре указателя уровнем ниже. Указатель, в зависимости от уровня охватывает квадратную площадь, а подъем вверх завершается только тогда, когда искомый блок окажется в пределах этой площади (оба приращения будут лежать в диапазоне блока для каждой оси). Вот организмы: "паровоз" и "вирус".

;Указатель и одновременно ячейка

Type ptr
 Field nxt.ptr[3] ;следующие указатели в иерархии
 Field prev.ptr ;предыдущий указатель в иерархии
 Field prevpos ;расположение предыдущего указателя
 Field neig.ptr[7] ;адреса соседей (для клетки)
 Field x,y,nq; координаты и количество соседей
End Type

;Указатели на ячейки, для которых возможно изменение состояния
Type chang
 Field p.ptr
End Type

Const loadorg$="Life-Locomotive.png"
;Const loadorg$="Life-Virus.png"
Const xres=800,yres=600

Global cellq, scrx, scry, ib

;Разбивка массива указателей на 3 списка
Dim pmark.ptr(3)
For n=0 To 3
 pmark(n)=New ptr
Next

Graphics xres,yres
SetFont LoadFont ("Arial cyr",14)

Dim change(24)
For n=0 To 1
 Read m$
 For nn=0 To 8
  change(n*16+nn)=Sgn(Instr(m$,nn))
 Next
Next
Data "3","0145678"

i=CreateImage(xres,yres)
ib=ImageBuffer(i)

i2=LoadImage(loadorg$)
ib2=ImageBuffer(i2)
xsiz=ImageWidth(i2)
ysiz=ImageHeight(i2)
For x=0 To xsiz-1
 For y=0 To ysiz-1
  If ReadPixel(x,y,ib2) And 255 Then
   ;Первая встреченная ячейка - отправная точка для создания остальных
   If cellq=0 Then
    cell.ptr=New ptr
    ptrq=ptrq+1
    cell\x=400
    cell\y=300
    xx=x
    yy=y
   End If
   newborn findcell(cell,x-xx,y-yy)
  End If
 Next
Next
FreeImage i2

Repeat
 
 DrawBlock i,0,0

 gen=gen+1
 ;Курсор мыши перемещается в центр экрана (чтобы не цепляться за края)
 MoveMouse xres Sar 1,yres Sar 1

 If cellq=0 Then Exit

 ;Все ячейки, подверженные изменениям, переещаются в список №2
 For ch.chang=Each chang
  If change(ch\p\nq) Then Insert ch\p After pmark(2)
 Next
 Delete Each chang

 ;Изменяем состояние всех клеток из списка №2
 cell=pmark(2)
 Repeat
  cell=After cell
  If cell=pmark(3) Then Exit
  If cell\nq<16 Then
   newborn cell
  Else
   ;Для всех соседей - уменьшение их счетчика соседей
   For n=0 To 7
    cell2.ptr=cell\neig[n]
    cell2\nq=cell2\nq-1
    ;Занесение соседа в список ячеек, которые, возможно, изменят свое состояние
    If change(cell2\nq) Then ch.chang=New chang: ch\p=cell2
   Next
   ;Выключение бита заполненности
   cell\nq=cell\nq And 15
   WritePixel scrx+cell\x,scry+cell\y,0,ib
   cellq=cellq-1
   ;Занесение обработанной ячейки в список потенциально изменяющихся ячеек
   If change(cell\nq) Then ch.chang=New chang: ch\p=cell
  End If
 Forever

 ;Все ячейки из списка №2 переходят в список №1 (стабилизируются)
 Insert pmark(2) Before pmark(3)

 ;Определяется смещение курсора мыши
 dx=MouseX()-xres Sar 1
 dy=MouseY()-yres Sar 1
 ;Если курсор сместился, то происходит перерисовка клеток (весь 1-й список)
 If dx<>0 Or dy<>0 Then
  scrx=scrx+dx
  scry=scry+dy

  SetBuffer ib
  Cls
  SetBuffer FrontBuffer()

  cell=pmark(1)
  Repeat
   cell=After cell
   If cell=pmark(2) Then Exit
   If cell\nq And 16 Then WritePixel scrx+cell\x,scry+cell\y,-1,ib
  Forever

 End If

 SetBuffer ib
 Color 0,0,0
 Rect 0,0,100,36
 Color 255,255,255
 Text 0,0,"FPS: "+1000.0/(MilliSecs()-fps)
 Text 0,12,"Клеток: "+cellq
 Text 0,24,"Поколение: "+gen
 fps=MilliSecs()
 SetBuffer FrontBuffer()
 
Until KeyHit(1)
End

;Функция поиска ячейки в иерархии по отправной точке и смещению
;Если ячейка не существует, она создается вместе с цепочкой указателей
Function findcell.ptr(cell.ptr,x,y)
;Если смещение нулевое, то результат - отправная точка
If x=0 And y=0 Then Return cell
;Запоминаем координаты искомой ячейки (на случай, если ее придется создать)
xx=x+cell\x
yy=y+cell\y
pmax=1 ;Счетчик уровня в иерархии
;Первый этап - подъем вверх
Repeat
 ;Добавление нового указателя сверху, если достигнута вершина иерархии
 If cell\prev=Null Then
  p.ptr=New ptr
  Insert p After pmark(0)
  ;Позиция определяется положением искомой ячейки
  pos=(x<0)+(y<0) Shl 1
  p\nxt[pos]=cell
  cell\prev=p
  cell\prevpos=pos
 Else
  ;Иначе - переход на более высокий уровень в иерархии
  pos=cell\prevpos
  p.ptr=cell\prev
 End If
 ;Изменение координат в соответствии с перемещением
 If pos And 1 Then x=x+pmax
 If pos And 2 Then y=y+pmax
 ;Повышение уровня
 pmax=pmax Shl 1
 cell=p
 ;Выход, если достигнута точка, откуда можно спуститься до искомой
Until x>=0 And y>=0 And x<pmax And y<pmax

;Второй этап - спуск
Repeat
 ;Понижение уровня
 pmax=pmax Shr 1
 ;Определение направления
 pos=((x And pmax)=pmax)+((y And pmax)=pmax) Shl 1

 ;Создание нового указателя, если ветвь отсутствует
 If cell\nxt[pos]=Null Then
  p.ptr=New ptr
  Insert p After pmark(0)
  cell\nxt[pos]=p
  p\prev=cell
  p\prevpos=pos
  ;Если создаем клетку (указатель 1-го уровня), то перемещаем ее в список №1 и
  ; присваиваем запомненные координаты
  If pmax=1 Then
   Insert p After pmark(1)
   p\x=xx
   p\y=yy
  End If
 End If
 cell=cell\nxt[pos]
 ;Если достигнут низ иерархии (уровень клеток) - выход
Until pmax=1
Return cell
End Function

;Функция рождения новой клетки
Function newborn(cell.ptr)
;Поиск, запоминание соседей и увеличение их счетчика количества соседей
For xx=-1 To 1
 For yy=-1 To 1
  If xx Or yy Then
   cell2.ptr=findcell(cell,xx,yy)
   cell2\nq=cell2\nq+1
   ;Занесение соседа в список ячеек, которые, возможно, изменят свое состояние
   If change(cell2\nq) Then ch.chang=New chang: ch\p=cell2
   cell\neig[n]=cell2
   n=n+1
  End If
 Next
Next
;Включение бита заполненности
cell\nq=cell\nq Or 16
;Занесение обработанной ячейки в список потенциально изменяющихся ячеек
If change(cell\nq) Then ch.chang=New chang: ch\p=cell
WritePixel cell\x+scrx,cell\y+scry,-1,ib
cellq=cellq+1

End Function

Автор: Матвей Меркулов (E-mail: MattMerkulov_sobaka_gmail.com, ICQ: 392-274-050)

Другие

Друзья