更新:我非常喜欢这个话题,我写了《编程难题》,《国际象棋位置》和《霍夫曼编码》。如果您通读此书,我确定存储完整游戏状态的唯一方法是存储完整的移动列表。继续阅读为什么。因此,我将问题的简化版本用于样板布局。
问题
此图显示了起始象棋位置。国际象棋发生在8x8的棋盘上,每位玩家从16组相同的棋子开始,包括8个棋子,2个新手,2个骑士,2个主教,1个皇后和1个国王,如下所示:
位置通常记录为该列的字母,后跟该行的数字,因此怀特的女王/王后在d1。移动通常以代数表示法存储,该代数表示法是明确的,并且通常仅指定必要的最少信息。考虑一下这个开口:
- e4 e5
- Nf3 Nc6
- …
转换为:
- White将国王的棋子从e2移到e4(这是可以到达e4的唯一片段,因此为“ e4”);
- 布莱克将国王的棋子从e7移到e5;
- 白方将骑士(N)移至f3;
- 布莱克将骑士移至c6。
- …
董事会看起来像这样:
对于任何程序员来说,一项重要功能就是能够正确,明确地指出问题。
那么,什么缺失或模棱两可?事实证明。
董事会状态与游戏状态
您需要确定的第一件事是要存储游戏的状态还是棋盘上棋子的位置。仅对片段的位置进行编码是一回事,但问题是“随后的所有合法动作”。问题也无济于事。正如我将解释的,这实际上是一个问题。
卡斯特
游戏进行如下:
- e4 e5
- Nf3 Nc6
- BB5 A6
- Ba4 Bc5
该板看起来如下:
白方可以选择漂流。对此的部分要求是国王和相关车队永远都不能移动,因此需要存储国王或每边的车队是否已经移动。显然,如果他们不在开始位置,那么他们已经移动了,否则需要指定。
有几种策略可用于处理此问题。
首先,我们可以存储额外的6位信息(每个车和国王1位),以指示该信息是否已移动。如果恰好在其中,我们可以只为这六个正方形之一存储一点来简化此过程。或者,我们可以将每个未移动的棋子视为另一种棋子类型,因此在每一侧(典当,车队,骑士,主教,皇后和国王)的6个棋子类型,将有8个(添加未移动的车队和未移动的国王)。
En Passant
国际象棋中的另一个特殊且经常被忽略的规则是“ En Passant”。
游戏进行了。
- e4 e5
- Nf3 Nc6
- BB5 A6
- Ba4 Bc5
- OO b5
- bb3 b4
- c4
Black在b4上的棋子现在可以选择将b4上的棋子移动到c3,而在c4上使用白棋子。这仅在第一次机会时发生,这意味着如果布莱克现在通过了该期权,他将无法采取下一步行动。所以我们需要存储它。
如果我们知道前面的举动,我们肯定可以回答“ En Passant”是否可行。或者,我们可以存储第4位的每个棋子是否都向前移动了两次。或者,我们可以查看板上每个可能的“通过”位置并有一个标记,以指示是否可能。
晋升
这是怀特的举动。如果怀特将他的棋子从h7移到h8,则可以将其提升为其他任何棋子(但不是国王)。99%的时间被晋升为女王,但有时却没有,通常是因为这样做可能会在您获胜时造成僵局。编写为:
- h8 = Q
这对我们的问题很重要,因为这意味着我们不能指望每边都有固定数量的零件。如果将全部8个棋子都提升,一侧完全有可能(但极不可能)有9个皇后,10个新人,10个主教或10个骑士。
僵持状态
当您无法赢得最佳策略时,就会陷入僵局。最可能的变体是您无法采取法律行动(通常是因为在检查国王时采取了任何行动)。在这种情况下,您可以要求平局。这很容易满足。
第二种是三重重复。如果同一局的位置在游戏中出现3次(或在下一个动作中第三次出现),则可以要求平局。位置不需要以任何特定顺序出现(这意味着它不必重复相同的移动顺序重复三次)。这个问题使问题变得更加复杂,因为您必须记住每个先前的电路板位置。如果这是问题的要求,则唯一可行的解决方案是存储每个先前的动作。
最后,有五十步法则。如果没有棋子移动并且在前五十次连续移动中没有抓取任何棋子,则玩家可以要求平局,因此我们需要存储自移动棋子或截取一块棋子以来的棋子(两次中的最新一个)。这需要6位(0-63)。
该轮到谁啦?
当然,我们还需要知道轮到谁了,这是一小部分信息。
两个问题
由于僵局,存储游戏状态的唯一可行或明智的方法是存储导致该位置的所有移动。我将解决这个问题。棋盘状态问题将简化为:将所有棋子的当前位置存储在棋盘上,而无须考虑连铸,传球,胶着状态以及轮到的情况。
作品布局可以通过以下两种方式之一广泛处理:存储每个正方形的内容或存储每个作品的位置。
简单内容
有六种类型(棋子,车子,骑士,主教,皇后和国王)。每块可以是白色或黑色,因此一个正方形可能包含12种可能的块之一,也可能是空的,因此有13种可能性。13可以以4位(0-15)的形式存储,因此最简单的解决方案是每平方乘以4位或256位信息存储4位。
这种方法的优点是操作非常简单快捷。甚至可以在不增加存储需求的情况下增加3种可能性来扩展:在最后一圈移动了2个空格的棋子,没有移动的国王和没有移动的车子,这将满足很多需求前面提到的问题。
但是我们可以做得更好。
Base 13编码
将董事会职位视为一个很大的数字通常会很有帮助。这通常是在计算机科学中完成的。例如,暂停问题将计算机程序(正确地)视为大量程序。
第一种解决方案将位置视为64位基数的16位数字,但是如所示,此信息中存在冗余(每个“数字”有3个未使用的可能性),因此我们可以将数字空间减少到64位基数的13位数字。当然,这不能像base 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个成员中的每一个都替换为另一个值。每个“字符”具有相同的宽度,但是当您考虑每个字符的可能性不同时,可以提高效率。
考虑莫尔斯电码(如上图所示)。消息中的字符被编码为破折号和点的序列。这些破折号和圆点通过无线电传送(通常),并在它们之间停顿以定界。
请注意,字母E(英语中最常见的字母)如何是单个点,可能的序列最短,而Z(最不频繁)是两个破折号和两次蜂鸣声。
这样的方案可以显着减小期望消息的大小,但是以增加随机字符序列的大小为代价。
应该注意的是,摩尔斯电码还有另一个内在的功能:破折号只要三个点,因此在创建上述代码时要尽量减少破折号的使用。由于1和0(我们的构建基块)不存在此问题,因此它不是我们需要复制的功能。
最后,莫尔斯电码中有两种类型的休止符。短暂休息(点的长度)用于区分点和破折号。较长的间隙(破折号的长度)用于分隔字符。
那么这如何适用于我们的问题呢?
霍夫曼编码
有一种用于处理可变长度代码的算法,称为霍夫曼编码。霍夫曼编码创建可变长度代码替换,通常使用符号的预期频率将较短的值分配给较常见的符号。
在上面的树中,字母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(), "");
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位。
状态差异
另一种可能的方法是将第一种方法与霍夫曼编码相结合。这是基于以下假设:大多数预期的国际象棋棋盘(而不是随机生成的棋盘)更有可能至少部分类似于起始位置。
因此,您要做的是将256位当前板位置与256位起始位置进行异或,然后对其进行编码(使用霍夫曼编码或游程长度编码的某种方法)。显然,从(64个0可能对应于64位)开始非常有效,但是随着游戏的进行,所需的存储空间也会增加。
件位置
如前所述,解决该问题的另一种方法是存储玩家拥有的每个棋子的位置。这对于大多数正方形将为空的残局位置特别有效(但在霍夫曼编码方法中,无论如何,空正方形仅使用1位)。
每边将有一个国王和0-15个其他棋子。由于晋升,这些作品的确切组成可能会变化很大,以至于您不能假设基于开始位置的数字是最大值。
逻辑上将其划分为一个位置,该位置包含两个面(白色和黑色)。每边都有:
- 国王:位置为6位;
- 有典当:1(是),0(否);
- 如果是,则棋子数:3位(0-7 + 1 = 1-8);
- 如果是,则对每个棋子的位置进行编码:45位(请参见下文);
- 非典当数量:4位(0-15);
- 每件作品:类型(女王,车王,骑士,主教2位)和位置(6位)
至于棋子的位置,棋子只能位于48个可能的正方形上(不能像其他正方形那样位于64个正方形上)。因此,最好不要浪费每个典当使用6位将使用的额外16个值。因此,如果您有8个典当,则有48种8种可能性,等于28,179,280,429,056。您需要45位才能对那么多值进行编码。
那是每边105位或总共210位。起始位置对于这种方法来说是最坏的情况,但是当您移开碎片时,起始位置会变得更好。
应当指出,少于8个8种可能性,因为典当不能全部都在同一个正方形中。第一种具有48种可能性,第二种具有47种,依此类推。48 x 47 x…x 41 = 1.52e13 = 44位存储空间。
您可以通过消除其他棋子(包括另一侧)所占据的正方形来进一步改善此效果,这样您可以首先放置白色非棋子,然后放置黑色非棋子,再放置白色棋子,最后放置黑色棋子。在开始位置,这将存储要求降低为白色的44位和黑色的42位。
组合方法
另一个可能的优化是,每种方法都有其优点和缺点。您可以说,选择最佳的4个,然后在前两位编码方案选择器,然后再编码方案特定的存储。
由于开销很小,这将是最好的方法。
游戏状态
我回到存储游戏而不是位置的问题。由于三重重复,我们必须存储到目前为止发生的移动的列表。
注解
您必须确定的一件事是只是存储一系列动作,还是要注释游戏?国际象棋游戏通常带有注释,例如:
- BB5!Nc4?
怀特的举动被两个感叹号标记为出色,而布莱克的举动被视为错误。参见国际象棋标点符号。
另外,在描述动作时,您可能还需要存储自由文本。
我假设这些动作已经足够,所以将没有注释。
代数符号
我们可以简单地将移动文本存储在此处(“ e4”,“ Bxb5”等)。包括一个终止字节,您每次移动(最坏的情况)约为6个字节(48位)。这不是特别有效。
要尝试的第二件事是存储开始位置(6位)和结束位置(6位),因此每移动12位。那好多了。
或者,我们可以以可预测的确定性方式和状态来确定当前位置的所有合法举动。然后返回到上面提到的变量基础编码。白人和黑人在他们的第一个举动中有20个可能的举动,在第二个举动中还有更多,依此类推。
结论
这个问题没有绝对正确的答案。以上仅是几种可能的方法。
我对这个问题和类似问题的满意之处在于,它要求任何程序员都具备重要的能力,例如考虑使用模式,准确确定需求并考虑极端情况。
象棋位置作为象棋位置培训师的屏幕截图。