Привет, %usename%!
В этой статье я хочу немного рассказать про комбинаторные игры и разобрать решение одной из них. В жизни мы встречаемся именно с этим типом игр, формальное их определение звучит так:
Сегодня мы попытаемся понять суть подхода к простейшим играм, с этой целью рассмотрим игру «Тримино», которая заключается в следующем: имеется игровое поле размера 2*N, N <= 800 и бесконечное количество плоских фигурок, состоящих из трех клеточек (см.рисунок).
Два игрока ходят по очереди, пусть первый из них Х, а второй Y. На каждом ходе игрок кладет фигурку, поворачивая ее как угодно, на свободное место. Накладывать фигурки друг на друга нельзя. Проигрывает тот кто не может сделать ход. Наша задача состоит в том чтобы опеределить кто победит при оптимальной игре: Х или Y.
Легко видеть что наша игра с нормальным окончанием (проигрывает тот кто не может ходить) и обладает свойствами комбинаторных игр, значит её можно свести к игре «Ним», что мы и попытаемся сделать. Общий подход к подобного рода задачам заключается в том чтобы разбивать большую игру на множество типичных, но меньшего размера, для каждой из них вычислять nim value через сумму игр, потом найти mex(minimal exclusive) всех разбиений и сравнить его с нулем. В случае равенства получаем P позицию, иначе N позицию. Все было бы тривиально, но в нашей игре есть некоторые небольшие трудности, которые, немного подумав, можно обойти. А трудность вот в чем: первоначальное поле имеет форму прямоугольника, но поставив первую триминошку мы разбиваем игру на две другие, одна из которых точно такая же, а другая отличается тем что один из ее краев кривой. Поэтому в нашей задаче ничего другого не остается, как сделать несколько типов игр, их будет 4: I, II, III, IV. Схематически они изображены на картинке.
![](https://habrastorage.org/r/w780q1/getpro/habr/post_images/625/abd/926/625abd92625a28c5cf569eb295e65398.jpg)
Каждый из этих типов мы будем рассматривать отдельно, ставя триминошку в любые позиции и разбивая на суммы других игр. Теперь нужно проследить на какие игры можно разбить каждый из наших типов. Первый тип разбивается на сумму первого и второго, как бы мы не поворачивали фигурку, второй можно разбить на две вторых игры или на первую и третью. Третья разбивается на вторую и третью, четвертая тоже на вторую и третью. Симметричные варианты мы считаем одинаковыми так как они тождественны и на mex не повлияют, тем самым мы замечаем что третий и четвертый варианты абсолютно одинаковы, значит мы можем обойтись только одним из них, тем самым сократив нашу задачу. На рисунке ниже показаны способы разбиения:
![](https://habrastorage.org/r/w780q1/getpro/habr/post_images/bcf/401/069/bcf401069db42f71519ee93bd22234a4.jpg)
Учитывая сделанные замечания и аккуратно расставляя триминошки напишем три рекурсивные функции с запоминанием на языке java.
Теперь осталось только выдать наш ответ:
В разборе этой задачи я старался показать общий стиль мышления, он практически однотипен в решениях подобных задач. Разобранная выше задача взята из архива SPOJ и доступна по ссылке TRIOMINO.
Спасибо за внимание.
В этой статье я хочу немного рассказать про комбинаторные игры и разобрать решение одной из них. В жизни мы встречаемся именно с этим типом игр, формальное их определение звучит так:
Комбинаторная игра — это множество игровых позиций, про каждую из которых игрокам известно, как (в какие другие позиции) может ходить каждый из них. |
![](https://habrastorage.org/getpro/habr/post_images/b4b/e46/99b/b4be4699b04590d56d513df00366a2b3.png)
Легко видеть что наша игра с нормальным окончанием (проигрывает тот кто не может ходить) и обладает свойствами комбинаторных игр, значит её можно свести к игре «Ним», что мы и попытаемся сделать. Общий подход к подобного рода задачам заключается в том чтобы разбивать большую игру на множество типичных, но меньшего размера, для каждой из них вычислять nim value через сумму игр, потом найти mex(minimal exclusive) всех разбиений и сравнить его с нулем. В случае равенства получаем P позицию, иначе N позицию. Все было бы тривиально, но в нашей игре есть некоторые небольшие трудности, которые, немного подумав, можно обойти. А трудность вот в чем: первоначальное поле имеет форму прямоугольника, но поставив первую триминошку мы разбиваем игру на две другие, одна из которых точно такая же, а другая отличается тем что один из ее краев кривой. Поэтому в нашей задаче ничего другого не остается, как сделать несколько типов игр, их будет 4: I, II, III, IV. Схематически они изображены на картинке.
![](https://habrastorage.org/getpro/habr/post_images/625/abd/926/625abd92625a28c5cf569eb295e65398.jpg)
Каждый из этих типов мы будем рассматривать отдельно, ставя триминошку в любые позиции и разбивая на суммы других игр. Теперь нужно проследить на какие игры можно разбить каждый из наших типов. Первый тип разбивается на сумму первого и второго, как бы мы не поворачивали фигурку, второй можно разбить на две вторых игры или на первую и третью. Третья разбивается на вторую и третью, четвертая тоже на вторую и третью. Симметричные варианты мы считаем одинаковыми так как они тождественны и на mex не повлияют, тем самым мы замечаем что третий и четвертый варианты абсолютно одинаковы, значит мы можем обойтись только одним из них, тем самым сократив нашу задачу. На рисунке ниже показаны способы разбиения:
![](https://habrastorage.org/getpro/habr/post_images/bcf/401/069/bcf401069db42f71519ee93bd22234a4.jpg)
Учитывая сделанные замечания и аккуратно расставляя триминошки напишем три рекурсивные функции с запоминанием на языке java.
Copy Source | Copy HTML
- private static int M = 800;
- private static int[] I = new int[M+1],
- II = new int[M+1],III = new int[M+1];
- static {
- Arrays.fill(I, -1);
- Arrays.fill(II, -1);
- Arrays.fill(III, -1);
- }
-
- private static int I(int n) {
- if (I[n] >= 0) return I[n];
- if (n == 0 || n == 1) return I[n] = 0;
- boolean[] mex = new boolean[M+1];
- for (int i=1; i<n; i++)
- mex[I(i - 1) ^ II(n - i)] = true;
- for (int i= 0; i<=M; i++)
- if (!mex[i]) return I[n] = i;
- return I[n] = M+1;
- }
-
- private static int II(int n) {
- if (II[n] >= 0) return II[n];
- if (n == 0 || n == 1) return II[n] = 0;
- boolean[] mex = new boolean[M+1];
- for (int i=2; i<n; i++)
- mex[II(i - 1) ^ II(n - i)] = true;
- for (int i=2; i<=n; i++)
- mex[III(i - 1) ^ I(n - i)] = true;
- for (int i= 0; i<=M; i++)
- if (!mex[i]) return II[n] = i;
- return II[n] = M+1;
- }
-
- private static int III(int n) {
- if (III[n] >= 0) return III[n];
- if (n == 0 || n == 1) return III[n] = 0;
- boolean[] mex = new boolean[M+1];
- for (int i=2; i<n; i++)
- mex[III(i - 1) ^ II(n - i)] = true;
- for (int i= 0; i<=M; i++)
- if (!mex[i]) return III[n] = i;
- return III[n] = M+1;
- }
-
Теперь осталось только выдать наш ответ:
Copy Source | Copy HTML
- int n = nextInt();
- out.println(I(n) == 0 ? 'Y' : 'X');
В разборе этой задачи я старался показать общий стиль мышления, он практически однотипен в решениях подобных задач. Разобранная выше задача взята из архива SPOJ и доступна по ссылке TRIOMINO.
Спасибо за внимание.