Россия, Пошатово |
Анализ игр
11.3. Вычисление цены: полный обход
Как видно из доказательства теоремы Цермело, для нахождения оптимальной стратегии достаточно уметь вычислять цены всех вершин. В этом разделе мы рассмотрим случай, когда позиции игры образуют дерево (ведущие вверх ребра дерева соответствуют возможным в данной позиции ходам) и покажем, как применить программу обхода дерева (лекция 3).
Напомним, что мы рассматривали Робота, который в каждый момент находится в одной из вершин дерева и умеет выполнять команды вверх_налево, вправо и вниз. Робот начинает работу в корне дерева (роль которого теперь играет начальная позиция игры). Раньше Робот умел еще обрабатывать вершины; теперь мы предполагаем, что он может определить тип текущей вершины (один из трех: max, min и final, что соответствует вершинам Макса, Мина и заключительным) и может определить стоимость текущей вершины, если она является заключительной.
11.3.1. Написать программу, которая управляет Роботом и вычисляет цену игры.
Решение. Напишем рекурсивную процедуру, которая, начав с некоторой вершины, обходит поддерево этой вершины, возвращает Робота на место и сообщает цену вершины (где она начала и кончила):
procedure find_cost (var c: integer) | var x: integer; begin | if тип = final then begin | | c:= стоимость; | end else if тип = max then begin | | вверх_налево; | | find_cost (c); | | {c = максимум цен текущей вершины и братьев слева} | | while есть_справа do begin | | | вправо; | | | find_cost (x); | | | c := max (c,x); | | end; | | {c=цена вершины под текущей} | | вниз; | end else begin {тип = мин} | | ...аналогично с заменой max(c,x) на min(c,x) | end; end;
Мы пользуемся тем, что у вершин типа max и min есть хотя бы один сын (вершины без сыновей должны быть заключительными, и мы предполагаем, что они отнесены к типу final ).
11.3.2. Написать нерекурсивную программу для вычисления цены игры (заданной деревом, по которому ходит Робот).
Решение. Как обычно, рекурсию можно устранить, используя стек. В данном случае каждый элемент стека будет хранить информацию об одном из предков текущей вершины (чем дальше, тем глубже - на дне стека будет информация о корне). Когда мы находимся в корне, стек пуст, при движении вверх по дереву он удлиняется, при движении вниз - укорачивается.
Каждый элемент стека представляет собой пару; первый элемент - тип соответствующей вершины ( min/max ), а второй элемент - минимум/максимум значений всех ее сыновей левее текущего. В программе из лекции 3 существенную роль играли два утверждения: ОЛ означало, что обработаны все вершины левее текущей (те, путь в которые отклоняется налево от пути в текущую); ОЛН означало, что обработаны все вершины левее и над текущей (это бывало, когда мы проходили вершину второй раз).
Помимо стека (который всегда будет хранить данные, указанные выше) программа использует еще переменную c. В ситуации ОЛ эта переменная не используется, а в ситуации ОЛН она хранит цену текущей вершины. Покажем, как можно поддерживать это, описав действия с переменной и стеком для каждого варианта движения Робота (ср. "Обход дерева. Перебор с возвратами" свойства команд Робота):
- {ОЛ, не есть_сверху} обработать {ОЛН}: в переменную c записываем цену текущего листа;
- {ОЛ, есть_сверху} вверх_налево {ОЛ}: перед тем, как идти вверх, добавляем в стек тип текущей вершины ( max/min ) и значение / соответственно, имея в виду, что максимум пустого множества равен , а минимум равен ;
- {есть_справа, ОЛН} вправо {ОЛ}: обновляем значение в вершине стека, беря максимум или минимум (в зависимости от типа вершины стека) со значением переменной c ;
- {не есть_справа, есть_сниз, ОЛН} вниз {ОЛН}: в переменную c помещаем максимум/минимум (в зависимости от типа вершины стека) ее прежнего значения и значения на вершине стека (оно забирается из стека, и стек укорачивается).
Легко видеть, что при этом утверждения о содержании стека и значении переменной c не нарушаются, и по окончанию работы программы стек будет пуст, а значение переменной c будет равно цене игры.