Статья Автор: Лебедев Дмитрий

Создаем программу для игровой стратегии. Часть 2

Эта статья является продолжением  статьи 49906, в котором было показано как создать программу построения выигрышной стратегии (ВС) по аналогии с "ручными" действиями.
Получившаяся программа имеет ряд недостаткой:
  • имеет сложность порядка O(n3) и хранит все возможные состояния (неэффективное использование памяти)
  • хорошо "решает" задания с одной кучей и "числовыми" вершинами
В этой статье будет произведена ""модификация" программы. Логика работы программы будет сохранена.
Напомним, что описать ВС, значит создать "двудольный граф игры", в котором вершинами являются все возможные состояния игры,
а ребра указывают на возможные ходы игроков. Вершины будут "раскрашены" в "квадратики" (чётные цвета) и "кружки" (нечётные цвета).
Ориентированные ребра графа будут обозначать ходы и могут соединять  "квадратик" с "кружком" или "кружок" с "квадратиком".
Игрок "1-го хода" будет иметь ВС, если "игра" начинается с "кружка", иначе ВС есть у игрока "2-го хода". 


 

Состояниями игры могут быть любые объекты: числа, вектора/наборы значений, строки.
Для из храниния будут использоваться кортежи/списки/множества/словари

Создавать раскраску вершин графа будем на базе следующего алгоритма

# Создадим  Dict - словарь вершин с раскраской и Q - список/множество "стартовых" вершин 
# Dict может содержать все финальные вершины или только две:

 # Dict['Win']=0; Dict['Lose']=-1
Dict, Q =  start(…) # содержимое из условия задачи и выбранного "способа хранения" вершин
t, go = 0, True #номер шага, флаг
while go : # или while Q: или while t<6 
    t+=1 # делаем такт нечетным (это кружки) 
    A=first(Q,Dict, t) #ищем «кружки», A – новые кружки или их кол-во или "флаг"
    
t+=1
# делаем такт чётным (это квадратики)
    B=second(Q,Dict, t) # определяем «квадратики», B – новые квадратики или их кол-во или "флаг"

     go=A or B # надо ли продолжать или отсутствует или if not (A or B) : break
    print(t//2,A,B) # результаты хода, если это удобно 
# Обработка словара Dict в зависимости от условий


Напишите основной блок программы (остальные подпрограммы уже созданы) и сравните свой ответ с эталоном




ПИШЕМ ПОДПРОГРАММЫ start (n) и next(p,n)
Разберем на примере задания 49932 
Два игрока, Петя и Ваня, играют в следующую игру.
Перед игроками лежат две кучи камней. Игроки ходят по очереди, первый ход делает Петя.
За один ход игрок может добавить в большую из  куч пять камней или увеличить количество камней
в меньшей куче в два раза. Если количество камней в кучах одниковое, то в любую из куч можно добавить три камня.
Для того чтобы делать ходы, у каждого игрока есть неограниченное количество камней.
Игра завершается в тот момент, когда произведение камней в кучах становится не менее N.
Победителем считается игрок, сделавший последний ход, т.е. первым получивший такую позицию,
при которой произведение камней в кучах будет N или более.
В начальный момент в первой куче было x камней, во второй куче – y камней;
1 ≤ x*y <N.


Возможно несколько подходов к хранению вершин графа с помощью словаря и списка/множества стартовых вершин.

Вариант 1: В словарь D записываются все возможные "финальные" вершины с раскраской, 
На каждом шаге обработки в словарь добавляются новые вершины с раскраской  (цвет определяется чётностью номера такта/
"Стартовые" вершины помещаются в список/множество Q.  
На каждом шаге обработки проверяются все вершины из Q. Вершины, получившие раскраску, помещаются в словарь,
остальные остаются в Q для обработки на следующем шаге.
Вариант 1
def start(N): # подпрограмма начальной раскраски с параметром
  D=dict() # создание словаря
  Q=list() # создание списка 
  for x in range(1,2*N): # вариант без оптимизации
    for y in range(1,2*N): # при оптимизации можно считать, что x ≤ y
      if x*y<N : # проверка условия "финальная/стартовая"
        Q.append((x,y)) # "стартовая" - в список
      else: # "финальная"  - в словарь
        D[(x,y)]=0
  return Q , D # возврат список, словарь

При таком варианте решения расходуется больше памяти,
но упрощается структура подпрограммы next(p,N) - не надо учитывать правила "завершения игры"


def next(p,N): # подпрограмма получения смежных вершин 
# N передаем (для стандартизации, но не используем)
  x,y=p # распаковка вершины
  if x==y: # 1 случай - кол-во камней в кучах одинаковое
      return [(x,y+3), (x+3,y)] 
  if x<y : 
# 2 случай - в первой куче камней меньше
      return [(2*x,y),(x,y+5)]
  return [(x+5,y),(x,2*y)]  
# 3 случай - в первой куче камней больше

Вариант 2: "Финальные" вершины в словарь D не записываются.
Создается словарь вида {'W' : 0, 'L' : -1}.
Подпрограмма next(p,N) должна возвращать:
-  'W' для "выигрышных финальных" вершин,
-  'L' для "проигрышных финальных" вершин,
- номер вершины для "стартовых" вершин
"Стартовые" вершины помещаются в список/множество Q.  
На каждом шаге обработки проверяются все вершины из Q. Вершины, получившие раскраску, помещаются в словарь,
остальные остаются в Q для обработки на следующем шаге.
Вариант 2.
def start(N): # подпрограмма начальной раскраски с параметром
    D={'W' : 0, 'L': -1} # создание словаря
    Q=list() # создание списка
    for x in range(1,2*N): # заполнение списка с "оптимизацией"
        for y in range(x,2*N): # считаем, что y не меньше x
            if x*y<N : # "стартовая" - в список
                 Q.append((x,y))
    return Q,D  # возврат список, словарь

При таком варианте решения расходуется меньше памяти,
но усложняется структура подпрограммы next(p,N) - 
надо учитывать правила "завершения игры"

def next(p,N): # подпрограмма получения смежных вершин 
  x,y=min(p),max(p) # распаковка и сортировка
  A=set() # множество смежных вершин (множество - могут совпадать)
  if x==y : # 1 случай - кол-во камней в кучах одинаковое
      if x*(y+3) <N : # проверка на завершение игры
         A.add((x,y+3)) # переход в "игровую" вершину
      else : # переход в "финальную" вершину 
         A.add('W') 
  else: # 2 случай - в кучах разное кол-во камней
      if  2*x*y < N  : # проверка на завершение игры (для 1-команды) 
         A.add((min(2*x,y),max(2*x,y))) # переход в "игровую" вершину 
      else : # переход в "финальную" вершину 
         A.add('W')
      if x*(y+5) < N : # проверка на завершение игры (для 2-команды)
         A.add((x,y+5)) # переход в "игровую" вершину
      else : # переход в "финальную" вершину
         A.add('W') 
  return A # возврат множества переходов
Возможны и другие варианты хранения вершин графа. 
Например, в Q можно записывать только "начальные стартовые" вершины,
а новые (полученные в next(...)) добавлять в процессе решения. 
Такой вариант потребует модификации подпрограмм first и second
 
 

Напишите подпрограммы start(N), next(N) для любого из способов храниения вершин графа (варианты 1 или 2)
и основной блок. Учтите, что подпрограммы first и second возврают количество найденных вершин.
Для поиска ответов на вопросы задания надо провести обработку словаря.
Обработку словаря и получения ответов можно реализовать следующим способом:
# Обработка Q 
B19 =[ p[0] for p in Dict.items() if p[1]==2]
B19.sort(key=lambda x: x[0]+x[1])
print('z19:',B19[:3],B19[-3:])
A20 =[ p[0] for p in Dict.items() if p[1]==3]
A20.sort(key=lambda x: x[0]+x[1])
print('z20:',A20[:3],A20[-3:])
B21 =[ p[0] for p in Dict.items() if p[1]==4]
B21.sort(key=lambda x: x[0]+x[1])
print('z21:'B21[:3],B21[-3:])

Правильность работы проверьте на задании 49932 
 


ПИШЕМ ПОДПРОГРАММЫ first(Q,D,t,N=1)  и second (Q,D,t,N=1)
Мы уже имеем версии этих программ без использования словаря и одномерного списка
first(Q,t)      и     second(Q,t)
Приведем возможный текст подпрограммы first(Q,D,t,N=1) с комментариями.
Подпрограмма second(Q,D,t,N=1) получается путем незначительных изменений 
Пояснения:
  • в подпрограммах проверяются все элемнты списка Q (можно считать, что это стек и забираем/удаляем через pop())
  • для каждой вершины осуществляется проверка на "возможность раскраски" 
  • Если раскраска возможна, то помещаем в словарь с раскраской, если нет откладываем в список New
  • После проверки всех вершин (Q - пустой список) добавляем в Q "отложенные" (через Q+=New или Q.extend(New)
Проверьте правильность написания программы (надо написать только подпрограмму first)
При вводе 384 программа должна вывести:
1 649 64
2 87 69
3 68 52
z19: [(11, 13), (10, 15), (11, 14)] [(1, 189), (1, 190), (1, 191)]
z20: [(8, 10), (7, 11), (9, 10)] [(1, 184), (1, 185), (1, 186)]
z21: [(6, 9), (7, 8), (5, 11)] [(1, 179), (1, 180), (1, 181)]



 


ИТОГИ.
  1. Возможно решение, при котором подпраграмм first и second можно объединить и тогда текст программы значительно "усохнет".
  2. Для заданий ЕГЭ достаточно использовать решение, при котором все возможные вершины сразу записываются в словарь.
    (глубина вопроса в ЕГЭ не более 6 тактов/3 ходов
  3. Реальное время написания программы со словарем не более 15 минут (по кодификатору 25 минут
  4.  В целом решение стандартно и настройка на "правила игры" очень простая
Пропустить Навигационные Ссылки.
Чтобы оставить комментарий нужна авторизация
Печать