算法 – 程序员拼图:在整个游戏中编码棋盘状态

不是严格的问题,更多的是一个谜题…

多年来,我参与了一些对新员工的技术访谈。除了问标准“你知道X技术”的问题,我也试图得到他们如何解决问题的感觉。通常,我会在面试前一天通过电子邮件向他们发送问题,并期望他们在第二天提出解决方案。

通常,结果将是相当有趣 – 错误,但有趣 – 如果他们能解释为什么他们采取一种特定的方法,该人仍然会得到我的建议。

所以我想我会把我的一个问题在那里为Stack Overflow的观众。

问题:什么是最节省空间的方式,你可以认为编码一个象棋游戏(或其子集)的状态?也就是说,给定具有合法布置的棋子的棋盘,对该初始状态和游戏中玩家采取的所有后续法律移动进行编码。

没有答案所需的代码,只是您将使用的算法的描述。

编辑:作为海报之一指出,我没有考虑移动之间的时间间隔。随意地说明,作为一个可选的额外:)

EDIT2:只是为了更多的澄清…记住,编码器/解码器是规则感知。真正需要存储的唯一的事情是玩家的选择 – 任何其他可以假设为编码器/解码器知道。

编辑3:这将是很难选择一个优胜者在这里:)很多很棒的答案!

更新:我喜欢这个话题这么多我写了Programming Puzzles, Chess Positions and Huffman Coding.如果你读过这个我已经确定,存储一个完整的游戏状态的唯一方法是通过存储一个完整的动作列表。继续阅读为什么。所以我使用一个稍微简化版本的问题为片布局。

问题

这个图像说明了起始棋位置。国际象棋发生在一个8×8的棋盘上,每个玩家开始一个相同的一套16件,包括8个棋子,2个车,2个骑士,2个主教,1皇后和1国王,如图所示:

位置通常记录为列的字母,后面紧跟该行的编号,因此白色的女王在d1。移动通常存储在algebraic notation,这是明确的,一般只指定必要的最小信息。考虑这个开口:

> e4 e5
> Nf3 Nc6
> …

其转换为:

>白移动国王的棋子从e2到e4(它是唯一可以得到e4因此“e4”);
>黑色将国王的棋子从e7移动到e5;
>白色将骑士(N)移动到f3;
>黑色将骑士移动到c6。
> …

板子看起来像这样:

任何程序员的一个重要的能力是能够正确和明确地指定问题。

那么什么缺失或不明确?很多,事实证明。

板状态vs游戏状态

你需要确定的第一件事是你是否存储游戏的状态或棋子在棋盘上的位置。只是编码的位置是一件事,但问题说“所有后续的法律动议”。问题也没有说到知道到这一点的动作。这实际上是一个问题,我将解释。

城堡

游戏进行如下:

> e4 e5
> Nf3Nc6
> Bb5 a6
> Ba4 Bc5

板子看起来如下:

白色有castling的选择。对此的一部分要求是,国王和相关的车不可能移动,所以无论是国王还是每一边的车都移动了,需要存储。显然,如果他们不在他们的起始位置,他们移动否则需要指定。

有几种策略可以用于处理这个问题。

首先,我们可以存储额外的6位信息(每个车和国王1个),以指示该棋子是否移动。我们可以通过只存储一个位,这六个正方形之一,如果正确的块恰好在它的流水线。或者,我们可以把每个未移动的部分作为另一种类型,而不是每一侧6个类型(典当,车,骑士,主教,皇后和国王)有8(添加不动的车和不动的国王)。

En Passant

国际象棋的另一个特殊的和经常被忽视的规则是En Passant

游戏进行。

> e4 e5
> Nf3 Nc6
> Bb5 a6
> Ba4 Bc5
> O-O b5
> Bb3 b4
> c4

b4上的黑色棋子现在可以选择将他在pa4上的棋子移动到c4上的白棋子。这只发生在第一次机会意味着如果黑色传递的选项,现在他不能采取下一步。所以我们需要存储这个。

如果我们知道以前的举动,我们可以肯定地回答如果En Passant是可能的。或者,我们可以存储每个棋子在其第四排刚刚移动到那里与双向前进。或者,我们可以看看板上每个可能的En Passant位置,并有一个标志来指示它是否可能。

促销

这是白的举动。如果白色将他的棋子从h7移动到h8,它可以提升到任何其他棋子(但不是国王)。 99%的时间被提升为女王,但有时不是,通常是因为这可能会迫使僵局,否则你会赢。这写为:

> h8 = Q

这在我们的问题中很重要,因为这意味着我们不能指望每边有固定数量的块。如果所有8个棋子都得到晋升,那么一方完全有可能(但不可思议的是不可能)结束9个女王,10个白嘴鸦,10个主教或10个骑士。

僵局

当一个位置,你不能赢得你的最佳策略是尝试一个stalemate.最可能的变体是你不能做法律动作(通常是因为任何动作,当你的国王在检查)。在这种情况下,你可以要求抽奖。这一个很容易迎合。

第二个变体是通过threefold repetition.如果相同的棋盘位置在游戏中发生三次(或者在下一步移动时将发生第三次),则可以索赔。位置不需要以任何特定的顺序发生(意味着它不必具有重复三次的相同的移动序列)。这一点使问题变得非常复杂,因为你必须记住每一个板的位置。如果这是问题的要求,则该问题的唯一可能的解决方案是存储每个先前的移动。

最后,有fifty move rule.如果没有棋子移动,并且在前面的连续五十个动作中没有棋子,我们需要存储从棋子移动或取出棋子后有多少动作,这需要6位(0-63)。

该轮到谁啦?

当然,我们还需要知道它的轮到,这是一个单一的信息。

两个问题

由于僵局情况,存储游戏状态的唯一可行或合理的方法是存储导致该位置的所有移动。我会解决这个问题。板状态问题将简化为:将所有块的当前位置存储在板上,忽略castling,en passant,stalemate条件,并且它的轮次。

片段布局可以以两种方式之一广泛地处理:存储每个正方形的内容或通过存储每个片段的位置。

简单内容

有六种类型(典当,车,骑士,主教,皇后和国王)。每个块可以是白色或黑色,所以一个正方形可以包含12个可能的块之一或它可以是空的,因此有13种可能性。 13可以存储在4位(0-15)中。因此,最简单的解决方案是为每个平方乘以64个方块或256位信息存储4位。

这种方法的优点是操作是令人难以置信的容易和快速。这甚至可以通过增加3个可能性而不增加存储需求来扩展:在最后一圈移动2个空格的棋子,没有移动的国王和没有移动的车,这将满足很多的上述问题。

但我们可以做得更好。

基本13编码

将董事会位置视为非常大的数字通常是有帮助的。这通常在计算机科学中完成。例如,halting problem将计算机程序(正确地)视为大数。

第一个解决方案将位置视为64位的16位数字,但是这个信息中存在冗余(每个“数字”为3个未使用的可能性),因此我们可以将数字空间减少到64位13位数。当然这不能像基础16那样有效,但它会节省存储需求(并且最小化存储空间是我们的目标)。

在基数10中,数字234等于2×102 3×101 4×100。

在基数16中,数字0xA50等于10×162 5×161 0×160 = 2640(十进制)。

因此,我们可以将我们的位置编码为p0 x 1363 p1 x 1362 … p63 x 130其中pi表示方形i的内容。

2256等于大约1.16e77。 1364等于大约1.96e71,这需要237位的存储空间。节省仅仅7.5%的代价是大大增加操纵成本。

变量基本编码

在法律委员会中,某些棋子不能出现在某些方格中。例如,pawn不能出现在第一或第八排中,将这些正方形的可能性减少为11.这将可能的板减少到1116×1348 = 1.35e70(大约),需要233位的存储空间。

实际上编码和解码这样的值到十进制(或二进制)是有点更复杂,但它可以可靠地完成,并作为练习留给读者。

可变宽度字母

前两种方法都可以描述为固定宽度字母编码。字母表的11个,13个或16个成员中的每一个替换为另一个值。每个“字符”是相同的宽度,但是当你考虑每个字符不是同样可能的时效率可以提高。

考虑Morse code(如上图)。消息中的字符编码为一系列破折号和点。这些破折号和点通过无线电传输(通常),在它们之间有一个暂停,以定界它们。

注意字母E(the most common letter in English)是单点,最短的可能序列,而Z(最不频繁)是两个破折号和两个嘟嘟声。

这样的方案可以显着减小预期消息的大小,但是以增加随机字符序列的大小为代价。

应该注意的是,莫尔斯电码具有另一个内置特征:虚线长达三个点,所以上述代码是为了最小化使用破折号而创建的。由于1s和0s(我们的构建块)没有这个问题,它不是我们需要复制的功能。

最后,有两种休息在莫尔斯电码。短暂的休息(点的长度)用于区分点和破折号。较长的间隙(短划线的长度)用于定界字符。

那么这如何适用于我们的问题?

Huffman编码

有一种用于处理称为Huffman coding的可变长度码的算法。霍夫曼编码创建可变长度码替换,通常使用符号的期望频率来向较常见的符号分配较短的值。

在上面的树中,字母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×1 16×3 12×5 4×6 = 164位。

状态差异

另一种可能的方法是将第一种方法与霍夫曼编码相结合。这是基于这样的假设,即大多数预期的棋盘(而不是随机生成的棋盘)更可能至少部分地类似于开始位置。

所以你做的是将256位当前板位置与256位起始位置进行异或,然后对其进行编码(使用Huffman编码或者说,run length encoding的某种方法)。显然,这将是非常有效的开始(64 0可能对应于64位),但增加存储所需的游戏进行。

件位置

如上所述,攻击这个问题的另一种方式是替代地存储玩家具有的每个片段的位置。这对于大多数正方形都是空的终止位置特别有效(但是在霍夫曼编码方法中,空的正方形只使用1位)。

每边将有一个国王和0-15其他的棋子。因为促销,那些片段的确切组成可以变化足够,使您不能假定基于起始位置的数字是最大值。

划分的逻辑方式是存储一个由两边(白色和黑色)组成的位置。每边有:

>一个国王:6位的位置;
>有pawns:1(yes),0(no);
>如果是,则典当数:3位(0-7 1 = 1-8);
>如果是,每个pawn的位置被编码:45位(见下文);
>非兵的数量:4位(0-15);
>对于每件:类型(2位为皇后,车,骑士,bishop)和位置(6位)

对于典当位置,典当只能在48个可能的正方形(不像其他64)。因此,最好不要浪费额外的16个值,每个pawn使用6位。所以如果你有8个棋子,有488种可能性,等于28,179,280,429,056。你需要45位来编码这么多的值。

这是每边105位或总共210位。起始位置是这种方法的最坏情况,然而,它会得到更好的,当你删除件。

应该指出的是,存在少于488个可能性,因为棋子不能都在同一个正方形第一个有48个可能性,第二个47等。 48×47×…×41 = 1.52e13 = 44位存储。

你可以通过消除被其他部分(包括另一边)占据的方块来进一步改善这一点,这样你可以先放置白色非典当,然后放置黑色非典当,然后放置白色棋子,最后放置黑色棋子。在起始位置,这将存储要求减少到44位用于白色和42位用于黑色。

组合方法

另一种可能的优化是这些方法中的每一种具有其优点和缺点。你可以说,选择最好的4,然后在前两个位编码一个方案选择器,然后在方案特定的存储。

由于开销很小,这将是最好的方法。

游戏状态

我回到存储游戏的问题,而不是一个位置。由于三重重复,我们必须存储已经发生到这一点的移动列表。

注释

你必须确定的一件事是你只是存储一个移动列表或者你注释的游戏?象棋游戏经常注释,例如:

> Bb5! Nc4?

White的举动由两个惊叹号标记为辉煌,而Black的被视为错误。见Chess punctuation

此外,您还可能需要存储自由文本作为移动描述。

我假设移动足够,所以没有注释。

代数符号

我们可以简单地存储移动的文本(“e4”,“Bxb5”等)。包括一个终止字节,你看每个移动(最坏情况)大约6字节(48位)。这不是特别有效。

第二个尝试是存储起始位置(6位)和结束位置(6位),因此每次移动12位。这是明显更好。

或者,我们可以从我们选择的可预测和确定的方式和状态确定从当前位置的所有合法移动。然后返回到上述可变基本编码。白色和黑色有20个可能的移动,每个在他们的第一个动作,更多的第二个,以此类推。

结论

这个问题没有绝对正确的答案。存在许多可能的方法,上述仅是少数几种。

我喜欢这个和类似的问题是,它需要对任何程序员重要的能力,如考虑使用模式,准确确定需求和思考角落情况。

国际象棋位置作为截图从Chess Position Trainer

http://stackoverflow.com/questions/1831386/programmer-puzzle-encoding-a-chess-board-state-throughout-a-game

本站文章除注明转载外,均为本站原创或编译
转载请明显位置注明出处:算法 – 程序员拼图:在整个游戏中编码棋盘状态