Головоломка программиста: кодирование состояния шахматной доски на протяжении всей игры - PullRequest
91 голосов
/ 02 декабря 2009

Не совсем вопрос, скорее загадка ...

На протяжении многих лет я участвовал в нескольких технических интервью с новыми сотрудниками. Кроме того, что я задавал стандартные вопросы «Знаете ли вы X технологии», я также пытался понять, как они решают проблемы. Обычно я отправляю им вопрос по электронной почте за день до собеседования и ожидаю, что они примут решение к следующему дню.

Часто результаты были бы довольно интересными - неправильными, но интересными - и человек все равно получал бы мои рекомендации, если бы мог объяснить, почему они выбрали тот или иной подход.

Так что я решил написать один из своих вопросов для аудитории Stack Overflow.

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

Код не требуется для ответа, просто описание алгоритма, который вы бы использовали.

РЕДАКТИРОВАТЬ: Как указал один из плакатов, я не учел временной интервал между ходами. Не стесняйтесь учитывать это также в качестве дополнительной опции:)

EDIT2: просто для дополнительного пояснения ... Помните, кодер / декодер осведомлен о правилах. Единственные вещи, которые действительно должны быть сохранены, - это выбор игрока - предполагается, что кодировщик / декодер может знать все остальное.

РЕДАКТИРОВАТЬ3: Будет трудно выбрать победителя здесь :) Много хороших ответов!

Ответы [ 31 ]

131 голосов
/ 02 декабря 2009

Обновление: Мне очень понравилась эта тема, которую я написал Программирование головоломок, шахматных позиций и кодирования Хаффмана . Если вы прочитали это, я решил, что only способ сохранить полное состояние игры - это сохранить полный список ходов. Продолжайте читать, почему. Поэтому я использую слегка упрощенную версию задачи для разметки.

Проблема

Это изображение иллюстрирует начальную шахматную позицию. Шахматы происходят на доске 8х8, где каждый игрок начинает с одинакового набора из 16 фигур, состоящих из 8 пешек, 2 ладей, 2 коней, 2 слонов, 1 королевы и 1 короля, как показано здесь:

starting chess position

Позиции обычно записываются в виде буквы для столбца, за которой следует номер строки, поэтому ферзь белых находится на уровне d1. Ходы чаще всего хранятся в алгебраической записи , которая однозначна и, как правило, определяет только минимальную необходимую информацию. Рассмотрим это открытие:

  1. e4 e5
  2. Nf3 Nc6
  3. ...

, что означает:

  1. Белые перемещают пешку короля с e2 на e4 (это единственная фигура, которая может добраться до e4, следовательно, «e4»);
  2. черные перемещают пешку короля с е7 на е5;
  3. Белые перемещают коня (N) на f3;
  4. Черные перемещают коня в с6.
  5. ...

Доска выглядит так:

early opening

Важной возможностью для любого программиста является умение правильно и однозначно указать проблему .

Так чего не хватает или неоднозначно? Как оказалось много.

Состояние Совета против Game State

Первое, что вам нужно определить, - сохраняете ли вы состояние игры или расположение фигур на доске. Простое кодирование положения фигур - это одно, но проблема говорит о «всех последующих юридических шагах». Проблема также ничего не говорит о знании движений до этого момента. Это на самом деле проблема, как я объясню.

Рокировка

Игра прошла следующим образом:

  1. e4 e5
  2. Nf3 Nc6
  3. Bb5 a6
  4. Ba4 Bc5

Плата выглядит следующим образом:

later opening

Белый имеет опцию рокировка . Частью требований для этого является то, что король и соответствующая ладья никогда не могли двигаться, поэтому необходимо будет сохранить положение короля или любой ладьи с каждой стороны. Очевидно, что если они не находятся на своих начальных позициях, они переместились, в противном случае это необходимо указать.

Существует несколько стратегий, которые можно использовать для решения этой проблемы.

Во-первых, мы могли бы хранить дополнительные 6 битов информации (по 1 для каждой ладьи и короля), чтобы указать, перемещалась ли эта фигура. Мы могли бы упростить это, сохранив немного для одного из этих шести квадратов, если в нем окажется нужный фрагмент. В качестве альтернативы мы можем рассматривать каждую неподвижную фигуру как другой тип фигуры, поэтому вместо 6 типов фигуры на каждой стороне (пешка, ладья, рыцарь, слон, королева и король) их 8 (с добавлением неподвижной ладьи и короля).

En Passant

Другое своеобразное и часто игнорируемое правило в шахматах: En Passant .

en passant

Игра прогрессирует.

  1. e4 e5
  2. Nf3 Nc6
  3. Bb5 a6
  4. Ba4 Bc5
  5. O-O b5
  6. Bb3, b4
  7. c4

У пешки черных на b4 теперь есть возможность переместить пешку на b4 на с3, взяв белую пешку на с4. Это происходит только при первой возможности, то есть если черные переходят к варианту, то теперь они не могут сделать следующий ход. Таким образом, мы должны хранить это.

Если мы знаем предыдущий месяцМы можем определенно ответить, если En Passant возможен. В качестве альтернативы мы можем хранить информацию о том, перемещалась ли каждая пешка на ее 4-м уровне с двойным движением вперед. Или мы можем посмотреть на каждую возможную позицию En Passant на доске и иметь флаг, чтобы указать, возможно это или нет.

Promotion

pawn promotion

Это ход белых. Если белые перемещают свою пешку на h7 на h8, ее можно повысить на любую другую фигуру (но не на короля). В 99% случаев он повышается до королевы, но иногда нет, обычно потому, что это может привести к тупику, если в противном случае вы выиграете. Это написано как:

Это важно в нашей задаче, потому что это означает, что мы не можем рассчитывать на то, что на каждой стороне будет определенное количество фигур. Вполне возможно (но невероятно маловероятно), что одна сторона получит 9 ферзей, 10 ладей, 10 слонов или 10 рыцарей, если все 8 пешек получат повышение.

Пат

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

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

Наконец, есть правило пятьдесят ходов . Игрок может претендовать на ничью, если пешка не сдвинута, и ни одной фигуры не было взято в течение предыдущих пятидесяти последовательных ходов, поэтому нам нужно было бы сохранить количество ходов с тех пор, как пешка была перемещена или взята фигура (последний из двух. Это требует 6 бит (0-63).

Чей это поворот?

Конечно, нам также нужно знать, чья это очередь, и это один бит информации.

Две проблемы

Из-за патовой ситуации единственный реальный или разумный способ сохранить состояние игры - сохранить все ходы, которые привели к этой позиции. Я займусь этой проблемой. Задача о состоянии доски будет упрощена до этого: сохранит текущую позицию всех фигур на доске, игнорируя условия рокировки, прохождения, патовые состояния и чей ход .

Компоновка фигуры может широко рассматриваться одним из двух способов: путем сохранения содержимого каждого квадрата или путем сохранения положения каждой фигуры.

Простое содержание

Существует шесть типов фигур (пешка, ладья, рыцарь, слон, королева и король). Каждая часть может быть Белой или Черной, поэтому квадрат может содержать одну из 12 возможных частей, или он может быть пустым, так что есть 13 возможностей. 13 может храниться в 4 битах (0-15). Поэтому самое простое решение - хранить 4 бита для каждого квадрата, умноженного на 64 квадрата или 256 битов информации.

Преимущество этого метода в том, что невероятно легко и быстро. Это можно даже расширить, добавив еще 3 возможности без увеличения требований к хранилищу: пешка, которая переместилась на 2 пробела в последний ход, король, который не сдвинулся, и ладья, которая не сдвинулась, что обслужит много ранее упомянутых вопросов.

Но мы можем сделать лучше.

База 13 Кодировка

Часто полезно думать о позиции доски как об очень большом числе. Это часто делается в информатике. Например, проблема остановки рассматривает компьютерную программу (по праву) как большое число.

ПервыйВ решении рассматривается позиция как 64-значное число из 16 основных чисел, но, как было продемонстрировано, в этой информации есть избыточность (то есть 3 неиспользуемые возможности на «цифру»), поэтому мы можем сократить пространство до 64 базовых 13 цифр. Конечно, это не может быть сделано так же эффективно, как базовая 16, но это сэкономит на требованиях к хранилищу (и наша цель - минимизировать объем хранилища).

В базе 10 число 234 эквивалентно 2 x 10 2 + 3 x 10 1 + 4 x 10 0 .

В базе 16 число 0xA50 эквивалентно 10 x 16 2 + 5 x 16 1 + 0 x 16 0 = 2640 (десятичное число).

Таким образом, мы можем закодировать нашу позицию как p 0 x 13 63 + p 1 x 13 62 + ... + p 63 x 13 0 , где p i представляет содержимое квадрата i .

2 256 равно приблизительно 1,16e77. 13 64 равно приблизительно 1,96e71, что требует 237 бит дискового пространства. Эта экономия всего 7,5% достигается за счет значительно повышенных затрат на манипуляции.

Переменная базовая кодировка

В юридических блоках определенные фигуры не могут появляться в определенных квадратах. Например, пешки не могут появляться в первом или восьмом разрядах, уменьшая возможности для этих квадратов до 11. Это уменьшает возможные доски до 11 16 x 13 48 = 1.35e70 ( приблизительно), требуется 233 бита дискового пространства.

На самом деле кодирование и декодирование таких значений в десятичную (или двоичную) и из нее немного сложнее, но это можно сделать надежно и оставить читателю в качестве упражнения.

Алфавиты переменной ширины

Оба предыдущих метода можно описать как буквенное кодирование фиксированной ширины . Каждый из 11, 13 или 16 членов алфавита заменяется другим значением. Каждый «символ» имеет одинаковую ширину, но эффективность можно улучшить, если учесть, что каждый символ не одинаково вероятен.

morse code

Рассмотрим Азбука Морзе (на фото выше). Символы в сообщении закодированы как последовательность штрихов и точек. Эти тире и точки передаются по радио (обычно) с паузой между ними, чтобы разграничить их.

Обратите внимание, что буква E ( самая распространенная буква на английском языке ) представляет собой одну точку, кратчайшую возможную последовательность, тогда как буква Z (наименее частая) - две черты и два звуковых сигнала.

Такая схема может значительно уменьшить размер ожидаемого сообщения *1254*, но за счет увеличения размера случайной последовательности символов.

Следует отметить, что азбука Морзе имеет еще одну встроенную функцию: штрихи имеют длину до трех точек, поэтому приведенный выше код создается с учетом этого, чтобы минимизировать использование штрихов. Поскольку 1 и 0 (наши строительные блоки) не имеют этой проблемы, нам не нужно копировать эту функцию.

Наконец, есть два вида отдыха в азбуке Морзе. Короткий отдых (длина точки) используется для различения точек и тире. Длинный пробел (длина тире) используется для разделения символов.

Так, как это относится к нашей проблеме?

Код Хаффмана

Существует алгоритм для работы с кодами переменной длины, который называется Кодирование Хаффмана . Кодирование Хаффмана создает подстановку кода переменной длины, обычно использует ожидаемую частоту символов для назначения более коротких значений более распространенным символам.

Huffman code tree

В приведенном выше дереве буква E кодируется как 000 (или слева-слева-слева), а S равно 1011. Должно быть ясно, что эта схема кодирования однозначна .

Это важное отличие от азбуки Морзе. В коде Морзе есть символьный разделитель, поэтому в противном случае он может выполнять неоднозначную подстановку (например, 4 точки могут быть H или 2 Is), но у нас есть только 1 и 0, поэтому мы выбираем однозначную подстановку.

Ниже приведенпростая реализация:

private static class Node {
  private final Node left;
  private final Node right;
  private final String label;
  private final int weight;

  private Node(String label, int weight) {
    this.left = null;
    this.right = null;
    this.label = label;
    this.weight = weight;
  }

  public Node(Node left, Node right) {
    this.left = left;
    this.right = right;
    label = "";
    weight = left.weight + right.weight;
  }

  public boolean isLeaf() { return left == null && right == null; }

  public Node getLeft() { return left; }

  public Node getRight() { return right; }

  public String getLabel() { return label; }

  public int getWeight() { return weight; }
}

со статическими данными:

private final static List<string> COLOURS;
private final static Map<string, integer> WEIGHTS;

static {
  List<string> list = new ArrayList<string>();
  list.add("White");
  list.add("Black");
  COLOURS = Collections.unmodifiableList(list);
  Map<string, integer> map = new HashMap<string, integer>();
  for (String colour : COLOURS) {
    map.put(colour + " " + "King", 1);
    map.put(colour + " " + "Queen";, 1);
    map.put(colour + " " + "Rook", 2);
    map.put(colour + " " + "Knight", 2);
    map.put(colour + " " + "Bishop";, 2);
    map.put(colour + " " + "Pawn", 8);
  }
  map.put("Empty", 32);
  WEIGHTS = Collections.unmodifiableMap(map);
}

и

private static class WeightComparator implements Comparator<node> {
  @Override
  public int compare(Node o1, Node o2) {
    if (o1.getWeight() == o2.getWeight()) {
      return 0;
    } else {
      return o1.getWeight() < o2.getWeight() ? -1 : 1;
    }
  }
}

private static class PathComparator implements Comparator<string> {
  @Override
  public int compare(String o1, String o2) {
    if (o1 == null) {
      return o2 == null ? 0 : -1;
    } else if (o2 == null) {
      return 1;
    } else {
      int length1 = o1.length();
      int length2 = o2.length();
      if (length1 == length2) {
        return o1.compareTo(o2);
      } else {
        return length1 < length2 ? -1 : 1;
      }
    }
  }
}

public static void main(String args[]) {
  PriorityQueue<node> queue = new PriorityQueue<node>(WEIGHTS.size(),
      new WeightComparator());
  for (Map.Entry<string, integer> entry : WEIGHTS.entrySet()) {
    queue.add(new Node(entry.getKey(), entry.getValue()));
  }
  while (queue.size() > 1) {
    Node first = queue.poll();
    Node second = queue.poll();
    queue.add(new Node(first, second));
  }
  Map<string, node> nodes = new TreeMap<string, node>(new PathComparator());
  addLeaves(nodes, queue.peek(), &quot;&quot;);
  for (Map.Entry<string, node> entry : nodes.entrySet()) {
    System.out.printf("%s %s%n", entry.getKey(), entry.getValue().getLabel());
  }
}

public static void addLeaves(Map<string, node> nodes, Node node, String prefix) {
  if (node != null) {
    addLeaves(nodes, node.getLeft(), prefix + "0");
    addLeaves(nodes, node.getRight(), prefix + "1");
    if (node.isLeaf()) {
      nodes.put(prefix, node);
    }
  }
}

Один из возможных выходных данных:

         White    Black
Empty          0 
Pawn       110      100
Rook     11111    11110
Knight   10110    10101
Bishop   10100    11100
Queen   111010   111011
King    101110   101111

Для начальной позиции это равно 32 x 1 + 16 x 3 + 12 x 5 + 4 x 6 = 164 бита.

Разница состояний

Другой возможный подход - объединить самый первый подход с кодированием Хаффмана. Это основано на предположении, что большинство ожидаемых шахматных досок (а не сгенерированных случайным образом) с большей вероятностью, по крайней мере частично, напоминают исходную позицию.

Итак, вы делаете XOR для текущей 256-битной позиции платы с 256-битной начальной позицией и затем кодируете ее (используя кодирование Хаффмана или, скажем, некоторый метод кодирование длины выполнения ). Очевидно, что это будет очень эффективно для начала (64 0, вероятно, соответствуют 64 битам), но увеличение памяти требуется по ходу игры.

Piece Position

Как уже упоминалось, еще один способ решения этой проблемы - вместо этого сохранить положение каждой фигуры, которую имеет игрок. Это особенно хорошо работает с позициями эндшпиля, где большинство квадратов будут пустыми (но в подходе кодирования Хаффмана пустые квадраты в любом случае используют только 1 бит).

У каждой стороны будет король и 0-15 других фигур. Из-за продвижения точный состав этих фигур может варьироваться настолько, что вы не сможете предположить, что числа, основанные на начальных позициях, являются максимумами.

Логический способ разделить это - сохранить позицию, состоящую из двух сторон (белой и черной). Каждая сторона имеет:

  • Король: 6 бит для локации;
  • Имеет пешки: 1 (да), 0 (нет);
  • Если да, количество пешек: 3 бита (0-7 + 1 = 1-8);
  • Если да, местоположение каждой пешки кодируется: 45 бит (см. Ниже);
  • Количество не пешек: 4 бита (0-15);
  • Для каждой фигуры: тип (2 бита для ферзя, ладьи, рыцаря, слона) и местоположение (6 бит)

Что касается расположения пешек, то пешки могут быть только на 48 возможных клетках (не 64, как остальные). Таким образом, лучше не тратить лишние 16 значений, которые использовались бы при использовании 6 битов на пешку. Таким образом, если у вас есть 8 пешек, то есть 48 8 возможностей, что равно 28 179 280 429 056. Вам нужно 45 бит для кодирования такого количества значений.

Это 105 бит на сторону или всего 210 бит. Стартовая позиция - наихудший вариант для этого метода, и он будет значительно лучше, когда вы снимаете фигуры.

Следует отметить, что существует менее 48 8 возможностей, поскольку все пешки не могут быть в одном квадрате. У первого есть 48 возможностей, у второго 47 и так далее. 48 x 47 x… x 41 = 1.52e13 = 44-битное хранилище.

Вы можете улучшить это, убрав квадраты, которые заняты другими фигурами (включая другую сторону), чтобы вы могли сначала разместить белых непешек, затем черных непешек, затем белых пешек и наконец черных пешек , На начальной позиции это снижает требования к памяти до 44 бит для белых и 42 бит для черных.

Комбинированные подходы

Другая возможная оптимизация заключается в том, что каждый из этих подходов имеет свои сильные и слабые стороны. Можно, скажем, выбрать 4 лучших, а затем закодировать селектор схемы в первых двух битах, а затем хранилище, специфичное для схемы.

С такими маленькими накладными расходами это, безусловно, будет лучшим подходом.

Состояние игры

Я возвращаюсь к проблеме хранения игры , а не позиции . Из-за тройного повторения мы должны сохранить список ходов, которые произошли к этой точке.

Аннотации

Одна вещь, которую вы должны определить: вы просто храните список ходов или комментируете игру? Шахматные игры часто аннотируются, например:

  1. Bb5 !! Nc4

WhЭтот ход отмечен двумя восклицательными знаками как блестящий, тогда как ход черных считается ошибкой. См. Шахматная пунктуация .

Кроме того, вам также может понадобиться сохранить свободный текст, как описано в движениях.

Я предполагаю, что ходов достаточно, поэтому аннотаций не будет.

алгебраическая запись

Мы могли бы просто сохранить здесь текст движения («e4», «Bxb5» и т. Д.). Включая завершающий байт, вы смотрите примерно 6 байтов (48 бит) за ход (наихудший случай). Это не особенно эффективно.

Второе, что нужно попробовать, это сохранить начальное местоположение (6 бит) и конечное местоположение (6 бит), поэтому 12 бит на ход. Это значительно лучше.

В качестве альтернативы мы можем определить все законные шаги от текущей позиции предсказуемым и детерминированным способом и состоянием, которое мы выбрали. Затем возвращается к кодированию переменной базы, указанному выше. У белых и черных по 20 возможных ходов каждый на первый ход, больше на второй и т. Д.

Заключение

Нет абсолютно правильного ответа на этот вопрос. Есть много возможных подходов, из которых вышеперечисленные являются лишь некоторыми.

Что мне нравится в этой и других подобных проблемах, так это то, что она требует способностей, важных для любого программиста, таких как учет схемы использования, точное определение требований и размышление над угловыми случаями.

Шахматные позиции, взятые как скриншоты из Шахматный Позиционный Тренер .

48 голосов
/ 02 декабря 2009

Лучше всего хранить шахматные игры в удобочитаемом, стандартном формате.

Portable Game Notation предполагает стандартную стартовую позицию (хотя не обязательно ) и просто перечисляет ходы, по очереди. Компактный, читаемый человеком, стандартный формат.

1009 * Е.Г. *

[Event "F/S Return Match"]
[Site "Belgrade, Serbia Yugoslavia|JUG"]
[Date "1992.11.04"]
[Round "29"]
[White "Fischer, Robert J."]
[Black "Spassky, Boris V."]
[Result "1/2-1/2"]

1. e4 e5 2. Nf3 Nc6 3. Bb5 {This opening is called the Ruy Lopez.} 3... a6
4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 O-O 9. h3 Nb8  10. d4 Nbd7
11. c4 c6 12. cxb5 axb5 13. Nc3 Bb7 14. Bg5 b4 15. Nb1 h6 16. Bh4 c5 17. dxe5
Nxe4 18. Bxe7 Qxe7 19. exd6 Qf6 20. Nbd2 Nxd6 21. Nc4 Nxc4 22. Bxc4 Nb6
23. Ne5 Rae8 24. Bxf7+ Rxf7 25. Nxf7 Rxe1+ 26. Qxe1 Kxf7 27. Qe3 Qg5 28. Qxg5
hxg5 29. b3 Ke6 30. a3 Kd6 31. axb4 cxb4 32. Ra5 Nd5 33. f3 Bc8 34. Kf2 Bf5
35. Ra7 g6 36. Ra6+ Kc5 37. Ke1 Nf4 38. g3 Nxh3 39. Kd2 Kb5 40. Rd6 Kc5 41. Ra6
Nf2 42. g4 Bd3 43. Re6 1/2-1/2

Если вы хотите уменьшить его, то просто застегните его . Работа выполнена!

14 голосов
/ 02 декабря 2009

Отличная головоломка!

Я вижу, что большинство людей хранят положение каждой фигуры. Как насчет более простого подхода и хранения содержимого каждого квадрата ? Это позаботится о продвижении и захваченных фигурах автоматически.

И это допускает кодирование Хаффмана . На самом деле, начальная частота фигур на доске почти идеально подходит для этого: половина квадратов пуста, половина оставшихся квадратов пешки и т. Д.

Учитывая частоту каждого фрагмента, я построил дерево Хаффмана на бумаге, которое я не буду здесь повторять. Результат, где c обозначает цвет (белый = 0, черный = 1):

  • 0 для пустых квадратов
  • 1c0 за пешку
  • 1c100 для ладьи
  • 1c101 для рыцаря
  • 1c110 для слона
  • 1c1110 для королевы
  • 1c1111 для короля

Для всей доски в исходном положении у нас есть

  • пустых квадрата: 32 * 1 бит = 32 бита
  • пешки: 16 * 3 бита = 48 бит
  • ладьи / рыцари / слоны: 12 * 5 бит = 60 бит
  • королев / королей: 4 * 6 бит = 24 бита

Всего: 164 бит для начального состояния платы. Значительно меньше, чем 235 битов ответа, получившего наибольшее количество голосов. И она будет уменьшаться по мере продвижения игры (кроме как после повышения).

Я смотрел только на расположение фигур на доске; дополнительное состояние (чей ход, кто бросил, en passant, повторяющиеся ходы и т. д.) должно быть закодировано отдельно. Может быть, еще 16 бит, так что 180 бит для всего игрового состояния. Возможные оптимизации:

  • Оставляя менее частые фигуры и сохраняя их положение отдельно. Но это не поможет ... замена короля и королевы пустым квадратом экономит 5 битов, а это именно те 5 бит, которые необходимы для кодирования их положения другим способом.
  • «Никаких пешек на заднем ряду» можно легко кодировать, используя другую таблицу Хаффмана для задних рядов, но я сомневаюсь, что это сильно помогает. Вы, вероятно, все равно получите то же дерево Хаффмана.
  • «Один белый, один черный слон» можно кодировать, вводя дополнительные символы, не имеющие бита c, которые затем можно вывести из квадрата, в котором находится слон. (Пешки, повышенные до епископов, нарушают эту схему ...)
  • Повторы пустых квадратов могут быть закодированы по длине прогона путем введения дополнительных символов, скажем, для «2 пустых квадратов в ряд» и «4 пустых квадратов в ряд». Но оценить частоту таких случаев не так просто, и если вы ошибетесь, это скорее повредит, чем поможет.
9 голосов
/ 03 декабря 2009

Действительно большой подход к таблице поиска

Позиция - 18 байт
Расчетное количество юридических позиций 10 43
Просто перечислите их все, и позиция может быть сохранена всего в 143 битах. Требуется еще 1 бит, чтобы указать, какая из сторон должна играть дальше

Перечисление, конечно, не практично, но это показывает, что требуется как минимум 144 бита.

Ходы - 1 байт
На каждую позицию обычно приходится около 30-40 законных ходов, но число может достигать 218 Перечислим все законные ходы для каждой позиции. Теперь каждый ход может быть закодирован в один байт.

У нас все еще есть достаточно места для специальных ходов, таких как 0xFF, для представления отставки.

4 голосов
/ 02 декабря 2009

Атакование подзадачи кодирования шагов после того, как была закодирована начальная позиция. Подход заключается в создании «связанного списка» шагов.

Каждый шаг в игре кодируется как пара «старая позиция -> новая позиция». Вы знаете начальную позицию в начале шахматной игры; Обходя связанный список шагов, вы можете перейти в состояние после перемещения X.

Для кодирования каждого шага вам нужно 64 значения для кодирования начальной позиции (6 бит на 64 квадрата на доске - 8x8 квадратов) и 6 бит на конечную позицию. 16 бит на 1 ход каждой стороны.

Количество места, которое займет кодирование данной игры, пропорционально числу ходов:

10 x (количество ходов белых + количество ходов черных) биты.

ОБНОВЛЕНИЕ: потенциальное осложнение с продвинутыми пешками. Должен быть в состоянии указать, на что продвигается пешка - могут потребоваться специальные биты (для этого потребуется серый код, чтобы сэкономить место, поскольку продвижение пешки крайне редко).

ОБНОВЛЕНИЕ 2: Вам не нужно кодировать полные координаты конечной позиции. В большинстве случаев перемещаемая часть может перемещаться не более чем в X мест. Например, пешка может иметь максимум 3 варианта перемещения в любой заданной точке. Реализуя это максимальное количество ходов для каждого типа фигуры, мы можем сохранить биты в кодировке «места назначения».

Pawn: 
   - 2 options for movement (e2e3 or e2e4) + 2 options for taking = 4 options to encode
   - 12 options for promotions - 4 promotions (knight, biship, rook, queen) times 3 squares (because you can take a piece on the last row and promote the pawn at the same time)
   - Total of 16 options, 4 bits
Knight: 8 options, 3 bits
Bishop: 4 bits
Rook: 4 bits
King: 3 bits
Queen: 5 bits

Таким образом, пространственная сложность за ход черного или белого становится

6 бит для начальной позиции + (переменное количество бит в зависимости от типа перемещаемой вещи).

4 голосов
/ 02 декабря 2009

В каждой позиции получить количество всех возможных ходов.

следующий ход генерируется как

index_current_move =n % num_of_moves //this is best space efficiency
n=n/num_of_moves

очевидно лучшая эффективность использования пространства для хранения случайно сгенерированной игры и требует в среднем около 5 бит / ход, поскольку у вас есть 30-40 возможных ходов. Сборка хранилища просто генерирует n в обратном порядке.

Положение хранения сложнее взломать из-за большой избыточности. (Для одного сайта на доске может быть до 9 ферзей, но в этом случае нет пешек и слонов, если на доске находятся разноцветные квадраты), но обычно это похоже на хранение комбинации одинаковых фигур над оставшимися квадратами.)

EDIT:

Точка сохранения ходов - хранить только индекс хода. Вместо того, чтобы хранить Kc1-c2 и пытаться уменьшить эту информацию, мы должны добавить только индекс движения, сгенерированный из детерминированного генератора движения (позиция)

На каждом ходу мы добавляем информацию размером

num_of_moves = get_number_of_possible_moves(postion) ;

в пуле, и это число не может быть уменьшено

генерирующий информационный пул

n=n*num_of_moves+ index_current_move

дополнительный

Если в финальной позиции доступен только один ход, сохраните как количество ранее выполненных вынужденных ходов. Пример: если начальная позиция имеет 1 принудительный ход для каждой стороны (2 хода), и мы хотим сохранить это как игру за один ход, сохраните 1 в пуле n.

пример хранения в информационном пуле

Предположим, что мы знаем стартовые позиции и делаем 3 хода.

На первом ходу есть 5 доступных ходов, и мы берем индекс хода 4. На втором ходу есть 6 доступных ходов, и мы берем индекс позиции 3, а на 3-м ходу доступно 7 ходов для этой стороны, и он решил выбрать индекс хода 2.

векторная форма; Индекс = [4,3,2] n_moves = [5,6,7]

Мы кодируем эту информацию в обратном направлении, поэтому n = 4 + 5 * (3 + 6 * (2)) = 79 (умножение на 7 не требуется)

Как отменить это? Сначала у нас есть позиция, и мы узнаем, что доступно 5 ходов. Так

index=79%5=4
n=79/5=15; //no remainder

Мы берем индекс хода 4 и снова изучаем позицию, и с этого момента мы обнаруживаем, что существует 6 возможных ходов.

index=15%6=3
n=15/6=2

И мы берем индекс хода 3, который переводит нас в позицию с 7 возможными ходами.

index=2%7=2
n=2/7=0

Мы делаем последний ход с индексом 2 и достигаем конечной позиции.

Как видите, сложность времени равна O (n), а сложность пространства равна O (n). Изменить: сложность времени на самом деле O (n ^ 2), потому что число, которое вы умножаете на увеличивается, но не должно быть проблем с хранением игр до 10000 ходов.


сохранение позиции

Можно сделать близко к оптимальному.

Когда мы узнаем об информации и храним информацию, позвольте мне поговорить об этом подробнее. Общая идея заключается в уменьшении избыточности (об этом я расскажу позже). Предположим, что не было никаких поощрений и взятия, поэтому есть 8 пешек, 2 ладьи, 2 коня, 2 слона, 1 король и 1 ферзь с каждой стороны.

Что мы должны сохранить: 1. положение каждого мира 2. возможности рокировки 3. возможности en-passant 4. сторона, имеющая ход

Предположим, что каждая фигура может стоять где угодно, но не 2 штуки в одном месте. Количество способов размещения на доске 8 пешек одного цвета: C (64/8) (бином), которое составляет 32 бита, затем 2 ладьи 2R-> C (56/2), 2B -> C (54/2) , 2N-> C (52/2), 1Q-> C (50/1), 1K-> C (49/1) и то же для другого сайта, но начиная с 8P -> C (48/8) и т. Д. .

Умножив это вместе для обоих сайтов, мы получим номер 4634726695587809641192045982323285670400000, который составляет приблизительно 142 бита, мы должны добавить 8 для одного возможного прохода (пешка прохода может быть в одном из 8 мест), 16 (4 бита) для ограничения рокировки и один бит для сайта, который имеет движение. В итоге получается 142 + 3 + 4 + 1 = 150бит

Но теперь давайте отправимся на охоту за избыточностью на доске с 32 фигурами и без взятия.

  1. и черные, и белые пешки находятся на одной колонне и обращены друг к другу. Каждая пешка стоит перед другой пешкой, что означает, что белая пешка может быть не более 6-го ранга. Это приносит нам 8 * C (6/2) вместо C (64/8) * C (48/8), которые уменьшают информацию на 56 бит.

  2. возможность рокировки также избыточна. Если ладьи не находятся на стартовой позиции, у этой ладьи нет возможности рокировки. Таким образом, мы можем добавить 4 квадрата на доску, чтобы получить дополнительную информацию, если возможна рокировка с этой ладьей, и удалить 4 элемента рокировки. Таким образом, вместо C (56/2) * C (40/2) * 16 у нас есть C (58/2) * C (42/2), и мы потеряли 3,76 бита (почти все 4 бита)

  3. ан-походя: Когда мы храним одну из 8 пассивных возможностей, мы знаем положение черной пешки и уменьшаем информационное переориентацию (если это ход белых и энсант 3-й пешки, это означает, что черная пешка находится на с5, а белая пешка - либо с2, с3 или c4) поэтому в C (6/2) у нас есть 3, и мы потеряли 2,3 бита. Мы уменьшаем некоторую избыточность, если сохраняем число en-passant также со стороны, с которой это можно сделать (3 варианта -> влево, вправо, оба), и мы знаем возможность пешки, которая может занять en passant. (например, из предыдущего примера en passant с черным на c5, который может быть слева, справа или обоими. если он находится на одном сайте, у нас есть 2 * 3 (3 для хранения псисибилитов и 2 возможных хода для черной пешки 7 или 6 ранга) ) вместо C (6/2), и мы уменьшаем на 1,3 бита, а если с обеих сторон мы уменьшаем на 4,2 бита. Таким образом, мы можем уменьшить на 2,3 + 1,3 = 3,6 бита на проход.

  4. епископы: бисопы могут быть только на опоститных квадратах, это уменьшает избыточность на 1 бит для каждого сайта.

Если подвести итог, нам нужно 150-56-4-3.6-2 = 85 бит для хранения шахматной позиции, если не было взяток

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

4 голосов
/ 03 декабря 2009

Было бы интересно оптимизировать размер средний случай для типичных игр, в которые играют люди, вместо наихудшего случая. (В заявлении о проблеме не указано, какие из них; большинство ответов предполагают наихудший случай.)

Для последовательности ходов, хороший шахматный движок генерирует ходы из каждой позиции; он выдаст список k возможных ходов, упорядоченный по рангу их качества. Люди обычно выбирают хорошие ходы чаще, чем случайные, поэтому нам необходимо изучить сопоставление каждой позиции в списке с вероятностью того, что люди выберут «хороший» ход. Используя эти вероятности (основанные на корпусе игр из некоторой базы данных по шахматам в Интернете), кодируйте ходы с помощью арифметического кодирования . (Декодер должен использовать тот же шахматный движок и отображение.)

Для исходной позиции подход ралу будет работать. Мы могли бы также уточнить это с помощью арифметического кодирования, если бы у нас был какой-то способ взвешивать выборки по вероятности & mdash; например фигуры часто появляются в конфигурациях, защищая друг друга, а не случайно. Труднее увидеть простой способ включить эти знания. Одна идея: вместо этого воспользуйтесь приведенной выше кодировкой ходов, начиная со стандартной позиции открытия и находя последовательность, которая заканчивается на желаемой доске. (Вы можете попробовать поиск A * с эвристическим расстоянием, равным сумме расстояний фигур от их конечных позиций, или чем-то вдоль этих линий.) Это меняет некоторую неэффективность из-за чрезмерной спецификации последовательности ходов и эффективности из-за использования шахмат знания. (Вы можете исправить некоторые неэффективности, исключив варианты ходов, которые привели бы к ранее исследованной позиции в поиске A *: они могут получить вес 0 в арифметическом коде.)

Также довольно сложно оценить, какую экономию это принесло бы вам в средней сложности, без сбора некоторой статистики из фактического корпуса. Но отправная точка со всеми одинаково вероятными ходами, я думаю, уже превзошла бы большинство предложений здесь: арифметическое кодирование не требует целого числа битов на ход.

4 голосов
/ 04 декабря 2009

Я видел этот вопрос прошлой ночью, и он заинтриговал меня, поэтому я сел в постели, обдумывая решения. Мой окончательный ответ на самом деле очень похож на int3.

Базовое решение

Если принять стандартную игру в шахматы и вы не закодируете правила (как белые всегда идут первыми), то вы можете сэкономить много, кодируя только ходы, которые делает каждая фигура.

Всего 32 штуки, но на каждом ходу вы знаете, какой цвет движется, поэтому есть только 16 квадратов для беспокойства, что составляет 4 бита , для которых фигура движется в этом ходу.

У каждой фигуры есть только ограниченный набор движений, который вы могли бы перечислить каким-либо образом.

  • Пешка: 4 варианта, 2 бита (1 шаг вперед, 2 шага вперед, 1 по диагонали)
  • Ладья: 14 опций, 4 бита (максимум 7 в каждом направлении)
  • Епископ: 13 вариантов, 4 бита (если у вас есть 7 в одной диагонали, у вас есть только 6 в другой)
  • Рыцарь: 8 вариантов, 3 бита
  • Королева: 27 вариантов, 5 бит (Ладья + Епископ)
  • Король: 9 опций, 4 бита (8 ходов за один шаг плюс опция рокировки)

Для повышения предлагается 4 фигуры (Ладья, Епископ, Рыцарь, Королева), поэтому на этом ходу мы добавим 2 бита , чтобы указать это. Я думаю, что все остальные правила покрываются автоматически (например, en passant).

Дальнейшая оптимизация

Сначала, после захвата 8 фрагментов одного цвета, вы можете уменьшить кодировку фрагмента до 3 битов, затем 2 бита для 4 фрагментов и т. Д.

Основная оптимизация заключается в том, чтобы перечислять только возможных ходов в каждой точке игры. Предположим, мы храним ходы Пешки как {00, 01, 10, 11} для 1 шага вперед, 2 шага вперед, диагонали влево и диагонали вправо соответственно. Если некоторые ходы невозможны, мы можем удалить их из кодировки для этого хода.

Мы знаем состояние игры на каждом этапе (от отслеживания всех ходов), поэтому после прочтения того, какая часть будет двигаться, мы всегда можем определить, сколько бит нам нужно прочитать. Если мы понимаем, что только движения пешки в этой точке захватываются по диагонали вправо или движутся вперед вперед, мы знаем, что нужно прочитать только 1 бит.

Короче говоря, битовое хранилище, указанное выше для каждого фрагмента, составляет максимум . Почти у каждого хода будет меньше вариантов и часто меньше битов.

3 голосов
/ 02 декабря 2009

Является ли проблемой дать кодировку, наиболее эффективную для типичных шахматных игр, или кодировку с наименьшим кодированием в худшем случае?

Для последнего самый эффективный способ также является наиболее непрозрачным: создайте перечисление всех возможных пар (начальная доска, правильная последовательность ходов), которые с позицией ничья на трижды повторенных и без более пятидесяти ходов с момента, когда правила последней пешки-движения-или-захвата были рекурсивными. Тогда индекс позиции в этой конечной последовательности дает самое короткое кодирование для наихудшего случая, а также и одинаково длинное кодирование для типичных случаев и, как я ожидаю, очень дорого вычисляет. Предполагается, что самая длинная игра в шахматы должна быть более 5000 ходов, при этом каждому игроку доступно по 20-30 ходов (хотя и меньше, когда осталось несколько фигур) - это дает примерно 40000 битов, необходимых для этого кодирования.

Идея перечисления может быть применена, чтобы дать более гибкое решение, как описано в предложении Хенка Холтермана для кодирования, описанном выше. Мое предложение: не минимальное, но более короткое, чем приведенные выше примеры, и разумное решение:

  1. 64 бита для представления того, какие квадраты заняты (матрица занятости), а также список фигур в каждом занятом квадрате (может иметь 3 бита для пешек и 4 бита для других фигур): это дает 190 бит для Начальная позиция. Поскольку на плате не может быть более 32 элементов, кодирование матрицы занятости является избыточным, поэтому можно кодировать что-то вроде общих позиций плат, например, в виде 33 заданных бит плюс индекс платы из списка общих плат.

  2. 1 бит, чтобы сказать, кто делает первый ход

  3. Код движется в соответствии с предложением Хенка: обычно 10 бит на пару ходов белых / черных, хотя некоторые ходы будут занимать 0 битов, когда у игрока нет альтернативных ходов.

Это предлагает 490 битов для кодирования типичной игры с 30 ходами и будет достаточно эффективным представлением для типичных игр.

Абзац, кодирующий положение «трижды повторяемая позиция» и «не более пятидесяти ходов» с момента правил «последняя пешка-движение-или-захват»: если вы кодируете прошедшие ходы, возвращаются к последнему ходу или захвату пешки тогда у вас будет достаточно информации, чтобы решить, применяются ли эти правила: нет необходимости во всей истории игры.

3 голосов
/ 02 декабря 2009

Большинство людей кодируют состояние платы, но сами касаются ходов. Вот описание битовой кодировки.

Биты за штуку:

  • Piece-ID: Макс. 4 бита для идентификации 16 штук на сторону. Белый / черный может быть выведен. Определите порядок на части. Поскольку число частей падает ниже соответствующих степеней двух, используйте меньше битов, чтобы описать оставшиеся части.
  • Пешка: 3 возможности на первом ходу, так что +2 бита (вперед на один или два квадрата, en passant.) Последующие ходы не позволяют двигаться вперед на два, поэтому достаточно +1 бита , Продвижение может быть выведено в процессе декодирования, если заметить, что пешка достигла последнего ранга. Если известно, что пешка продвигается, декодер ожидает еще 2 бита, указывающих, к какому из 4 основных кусков она была повышена.
  • Епископ: +1 бит для используемой диагонали, до +4 битов для расстояния по диагонали (16 вариантов). Декодер может определить максимально возможное расстояние, на которое кусок может перемещаться по этой диагонали, поэтому, если это более короткая диагональ, используйте меньше битов.
  • Рыцарь: 8 возможных ходов, +3 бита
  • Ладья: +1 бит для горизонтали / вертикали, +4 бита для расстояния вдоль линии.
  • Король: 8 возможных ходов, +3 бита. Укажите рокировку «невозможным» ходом - поскольку рокировка возможна только тогда, когда король находится на первом ранге, закодируйте этот ход инструкцией, чтобы переместить короля «назад» - т.е. вне доски.
  • Королева: 8 возможных направлений, + 3 бита. До +4 больше битов для расстояния вдоль линии / диагонали (меньше, если диагональ короче, как в случае слона)

Предполагая, что все фигуры находятся на доске, это биты за ход: пешка - 6 битов на первом ходу, 5 впоследствии. 7, если повысится. Епископ: 9 бит (максимум), Рыцарь: 7, Ладья: 9, Король: 7, Королева: 11 (максимум).

...