高效存储棋局位置

13

我已经阅读了大量与此问题相关的网络信息,但仍未找到明确的答案。

我的目标是建立一个国际象棋棋局数据库,能够识别移位(通常是哪些棋子在哪些位置)。

编辑:它还应该能够识别相似(但不完全相同)的棋局。

这是一个20年前的讨论(当时空间是个问题):https://groups.google.com/forum/#!topic/rec.games.chess.computer/wVyS3tftZAA

其中一位参与者谈到了如何将棋子编码到一个方形矩阵中,使用4 x 64位加上额外信息(如王车易位、吃兵等)的几个比特: 有六种棋子(步兵、车、马、象、后、王)加上一个空格,需要3比特(2^3),再加上1比特来表示棋子的颜色。

总共会有4个64位数字,再加上一些额外信息。

问题:是否有其他更有效的存储国际象棋棋局的方法?

我应该提到这个问题是以数据库为中心,而不是以游戏为中心的(也就是说,我的唯一兴趣是高效地存储和检索,而不是创建任何人工智能或生成任何棋招)。

谢谢, Adrian

9个回答

6
棋盘上有32个棋子,64个方格。每个方格的索引可以用一个6位数字表示,因此为了表示每个棋子的位置,需要32个6位数字,或者总共192位,少于4x64。
考虑到并非所有位置都可能存在(例如兵无法到达自己颜色的最后一行),在这些情况下可以使用小于6位的位置表示。此外,已被其他棋子占据的位置对其他棋子不可用。
由于棋盘上的某些棋子可能完全不存在,应该从国王的位置开始,因为他们总是存在的。将另一个棋子的位置编码为国王的位置意味着该棋子已被拿走。
以下是各种棋子可能位置的简要分析:
- 国王、皇后、骑士和车可以在棋盘上任何地方(64个位置) - 主教被限制在每个32个位置 - 兵的位置分别限制在21、26、30、32、32、30、26和21个位置(A-H列)
因此,这组合法的国际象棋位置可以用一个整数来描述,从零到(64^12 * 32^4 * 21^4 * 26^4 * 30^4 * 32^8)-1,或391935874857773690005106949814449284944862535808450559999,可以装入188位。将一个位置编码为整数并从中解码非常简单。由于存在多个数字可以解码成相同的位置,因此无法用一种简单的方法对其进行唯一编码。
由于没有两个棋子可以占据同一个方格,所以存在更紧密的限制,但是编码和解码都有点困难,因此在实际应用中可能没有用。此外,上述数字与2^188非常接近,因此我认为即使使用更紧密的编码也不会适合187位。

嗯...可能是这样吧。 我应该理解,以片为中心的存储方式总是比以板为中心的存储方式更高效吗? - Adrian
可能是 - 虽然无法证明。最新编辑中描述的编码对我来说似乎非常紧凑,至少没有存储太多冗余信息。 - jlahd
棋子中心编码的效率与棋盘中心编码的效率取决于游戏中棋子数量与棋盘大小之间的比例。国际象棋有32个棋子和64个位置的棋盘。如果我没记错,只要满足以下条件,棋子中心编码就更加经济实惠:SIZE * log PIECES > PIECES * log SIZE - Panu
每个64个方格只能有13种可能的棋子(不是32个)...黑色的“rhbqkp”,白色的“RHBQKP”或一个空方块,所有这些都可以表示为该字符或附加的位值最大为“1100”,其中0尚未包括。除了兵以外还有几个例外。主教并不重要,因为任何方格都可以被主教占据。虽然主教是32个位置限制,但有2个主教可以占据任何方格。 - Ace Caserya
@AlvinCaseria:我正在对不同棋子位置进行编码,而不是每个位置上的棋子——这就是我发帖的全部意义。如果你对48个方格的13种可能值和第一行和最后一行的12种可能值进行编码,你最终会得到12^16*13^48种组合,这大约是我所提出的编码方式的10^14倍。 - jlahd
如果您正在为国际象棋数据库存储位置(来自OP),则需要存储轮到谁了,王车易位的权利以及过路兵的文件。使用以棋子为中心的表示法,您可以将此信息存储为“替代棋子”,而不是分别存储,尽管我还没有计算出这是否更好。 - qwr

4
如果您不需要可解码的位置表示来进行比较,那么您可以考虑使用Zobrist哈希。这被国际象棋引擎用于生成一个64位单向哈希值,以便在搜索树中检测置换。由于它是单向哈希,您显然无法从哈希值反向还原位置。哈希的大小是可调的,但64位似乎是接受的最小大小,可以产生很少的冲突。它将作为数据库索引键的理想选择,具有仅8字节的固定长度。尽管偶尔会发生冲突,但您可以进行第二次比较,将实际位置与已散列到相同值的任何位置进行过滤,如果这是一个问题的话。我在自己的应用程序(使用SQLite)中使用Zobrist哈希来管理我的开局,并且可以轻松地找到置换。

哈希算法如果只需要转置就能完美无缺了……但是确实需要解码位置。类似但不完全相同的位置搜索会很好,但似乎越来越难以实现。 - Adrian

3
看看Forsyth-Edwards记号(FEN)。可以在这里找到它的描述。许多引擎和国际象棋程序都支持并使用它。
这是起始位置的FEN:
rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
FEN被分成6个部分。
第1段包含棋子。黑色棋子用小写字母表示,白色棋子用大写字母表示。
第2段标明轮到谁走棋。(w或b)
第3段是关于王车易位的。KQkq表示双方都可以在两侧进行王车易位。K表示白色王侧,q表示黑色后侧。
第4段是有关吃过路兵的。如果没有可吃的过路兵,则为“-”。如果一个兵刚刚走了两步,则这是该兵的“后方”位置。无论是否有兵处于位置以进行吃过路兵的操作,都将记录此位置。
第5段是半步数:自上次吃子或兵前进以来的半步数。这用于确定是否可以根据五十步规则宣布平局。
第6段是全步数:从1开始计数,每次黑方走棋后递增。

我当然知道FEN,但这绝对不是一种有效的存储位置的方式。想象一下,在数千万个FEN位置的数据库中搜索一个特定的相似(但并非完全相同)的位置会花费多少代价...! - Adrian
实际上类似于FEN的概念,除了第一段。修订后的第一段为(8813) + 第二段(2) + 第三段(15或4位) + 第四段(表示过路兵位置的8*8) + 第五和第六段(100或更多,每半个计为1个值)。 - Ace Caserya

3

这是一个22字节的快速内部建议(受到哈夫曼编码的启发)。解码/编码不是很容易,但也不难。严肃的程序可能有更好的技巧。

  • 最初我们有32个空方格,每种颜色分别为8P、2k、2B、2R、1K、1Q;
  • 对于这些内容的哈夫曼编码将使用1、3、5、5、5、6、6位;例如:empty=0,白色P=100,k=10100,B=10101,R=10110,K=101110,Q=101111,黑色与白色相同,但以11开头而不是10;
  • 因此,我们的64个符号(每个方格一个)将使用:32 + 2 x(8x3 + 5x2 + 5x2 + 5x2 + 6x1 + 6x1)= 164位。

现在来看特殊情况:

  • 1位用于记录当前棋子的颜色;
  • 王车易位:为了编码一个可以进行王车易位的车,我们可以重复使用兵符号(不会混淆:第一行没有兵)。这样只需要3位而不是5位,所以在这里我们没有损失!
  • 吃过路兵:只需提到列数即可。有7种选择或更少。因此只需要3位(000表示没有吃过路兵,其他任何值指定列数);
  • 升变:每个兵大约消失2个,以便被一个自由方格和一个皇后替换,因此最坏情况下需要(-3 - 3 + 1 + 6)= 1位。

因此,不考虑升变的情况下:164 + 1 + 3 = 168位 = 21字节; 如果考虑到8个皇后升变的极端情况:22字节;

这是我能够为内部解决方案做出的最好贡献。


谢谢Gaetan,有一个小注释:可能会有两个过路兵,例如如果黑方在b4和e4上各有两个兵,在白方走c2-c4时,两个黑兵都可以进行过路兵吃子。否则,在紧凑存储和解码容易性之间存在艰难的权衡。这样的数据库应该在潜在的数十亿存储位置中搜索相同(或相似)的设置。位置被识别所需的时间越短,越好,但在这种情况下,存储空间可能更大。不容易... - Adrian
你好Adrian,恭喜您在7年后仍然留言。被吃掉的en-passant兵是孤独的,所以3位就足够了:位置的其余部分告诉我们1或2个敌人是否可以吃掉它。背景:几年前我卡在24字节处。因此,上个月当关于王车易位的想法出现时,我对“节省”的位感到高兴,对象棋编程的真实世界如何运作感到好奇,找到了这个旧问题,并留下了描述,以防它引起任何人的兴趣(我现在没有编写任何与国际象棋相关的程序)。 - Gaetan
哦,这个问题时不时地一直回到我脑海中,虽然我还没有开始编写代码,但这是一个有趣的问题。关键是,这样的存储需要同时满足两个条件:1)易于解码以最小化搜索时间,2)适合64位的倍数。你的提议可以放入三个64位整数(这很棒),但哈夫曼编码正是如此:它进行编码,而当出现潜在的数十亿个存储位置时,解码会大幅增加搜索时间。它必须既要极简又要快速,而最近空间似乎比速度便宜。 - Adrian
关于您7月15日的评论:正如KP的想法,我们首先使用64位整数来告诉我们棋子的位置,然后最多使用32个半字节来告诉它们是什么。普通的棋子是12个“值”之一:白色P、R、k、B、Q、K或黑色P、R、k、B、Q、K,对于相同类型的棋子重复出现。对于特殊情况(轮换,过路兵),我有16-12=4个未使用的半字的价值;你所需的请求似乎更适合数据库。因此,讨论转移到了索引数据库的高效存储,这是一个庞大的(更一般的)主题。 - Gaetan
是的,完全正确。原帖确实提到了兴趣是“以数据库为中心”的。也就是说,高效地存储和检索职位(从数据库中)。 - Adrian
显示剩余3条评论

2

紧凑且相对易懂,最坏情况下(或者总是这样方便的话)完整位置规范需要25个字节:

  • 64位用于标识哪些字段为空,哪些不为空。'on'位数定义了实际编码的最多32个碎片中有多少个。
  • 使用最多32×4位来标识每个碎片(包括颜色)。它们按照棋盘顺序提到。王车易位权利和吃过路兵信息也可以编码进此处(仍可进行易位的车与不能进行易位的车有不同的编码,刚刚向上移动2个位置的兵有其自己的编码):

000:普通兵;001:刚刚向上移动两个位置的兵;010:马;011:象;100:未移动过的车或其王;101:已经移动过的车或其王;110:后;111:王。

附加信息需要1个字节:

  • 轮到谁下:1位,
  • 从上次兵或吃子以来的走步数(最多100):7位

谢谢KP,这似乎接近最小值,但我不确定你如何考虑en-passant兵。无论如何,在64位架构中,您的200位(25字节)使用3.125个整数。然而,在数据库中,如果不能使用3 * 64 = 192位存储,则必须使用4 * 64 = 256位。这似乎是所需的空间量,还有足够的位来存储任何其他信息。 - Adrian

1
您可以使用修改后的游程长度编码,其中每个片段都被编码为一个片段号码(3位),其中0y111用于跳过空格。由于许多情况下片段是相邻的,因此您省略了位置信息:
         All pieces are followed by color bit
0y000c 0 Pawn
0y001c 1 Rook
0y010c 2 Knight
0y011c 3 Bishop
0y100c 4 Queen
0y101c 5 King
0y110 6 Empty space
0y111 7 Repeat next symbol (count is next 6 bits, then symbol)

解码器从a1开始向右移动,到达一行的末尾时向上移动,因此起始棋盘的编码方式如下:
12354321      Literal white encoding from a1 to h1    32 bits
7 8 0         repeat white pawn 8 times               13 bits
7 32 6        repeat 32 empty spaces                  12 bits
7 8 8         repeat black pawn 8 times               13 bits
9abcdba9      Literal encoding of black               32 bits
                                                    ---------
                                                     102 bits total

话虽如此,变长编码的额外复杂性和不确定性可能不值得节省空间。此外,在某些情况下,它可能比固定宽度格式更糟糕。


我同意RLE是一种较简单的编码方式,但它仍然是一种“编码”方式。FEN本身对于空方格是一种半RLE编码方式,(与RLE相同)它非常适合识别完全相同的位置。然而,如果目标是识别“相似”(但不完全相同)的位置,则需要至少两个步骤:解码和比较...这需要大量的计算时间。我正在寻找的是最小空间和最大速度的完美“混合”。 - Adrian
你问了“存储棋盘位置的更有效方法”,如果你想知道“如何比较两个棋盘之间的差异以产生距离”,那么你应该提出另一个问题。我的初步答案是匹配n-gram。 - Mitch
哦,你说得完全正确...我在原来的问题中没有提到这个关键方面(现在已经编辑过了)。谢谢,我会看看n-grams,它听起来像是一个非常先进的贝叶斯统计应用,但这是一个有趣的想法。 - Adrian
Mitch,我在想,起始位置是一种“简单情况”,可以重复32次空方块。那么“最坏情况”怎么样呢?如果每个32个棋子都完美地散布在棋盘上,它们都被一个空方块分开,那么最多需要多少位? - Adrian
谢谢你,Mitch。你真的很有帮助。这确实是一个复杂的问题,但我从你的帖子中获得了很多见解。 - Adrian
显示剩余12条评论

1
考虑棋盘上最多32个棋子。每个棋子可以在64个方格中的一个上面。以预定顺序独立地表示棋子位置需要32 * 6 = 192位。
除此之外,每个兵可晋升为车、象、马或后,因此我们需要在3个附加位(4种可能的棋子类型和正常兵)中编码其状态。
32 * 6 + 16 * 3 = 240位/30字节
在许多情况下,您需要有关游戏状态/变体的其他信息:
吃过路兵的文件:4位(8个文件和无)
王车易位的权利:4位(短/长白色/黑色)
走棋方:1位(白色/黑色)
这总共是249位/32字节。
这可能不是最紧凑的表示,但易于编解码。

你不需要考虑兵可以在当前位置晋升为什么棋子。如果你存储了32个棋子的每一个位置,你必须有一些值来代表不在棋盘上的情况。 - qwr
@qwr 你可以先存储国王的位置,然后将国王的位置视为不在棋盘上,因为任何棋子都不能占据与国王相同的方格。 - AspectOfTheNoob

0

存储棋盘信息有两种简单方法:一种是存储每个棋子的位置,另一种是为每个方格存储其中的内容。

正如Mitch所解释的那样,可以使用RLE进行一定程度的压缩,但给出的示例是起始位置,这特别容易描述。在棋子分布在棋盘上的另一种情况下,你可能会看到空间和棋子交替,而RLE不会压缩任何东西。因此,除非使用更复杂的算法,否则无法进行压缩。

我认为jlahd在计算中犯了一个错误,将中心兵算了两次,因此实际上存储每个棋子位置所需的空间不是188位,而是168位。此外,还需要存储兵是否已晋升。因此,就兵而言,有(32 + 4x64)^16种可能性。总共需要223位=28字节。

如果我们为每个方格存储其内容,我们需要计算一个方格上的可能性。对于大多数方格,有6种可能的白色棋子和同样数量的黑色棋子。对于顶部和底部行,一种颜色的兵不能出现。因此,中心方格有13种可能性,顶部和底部方格有12种可能性,因此有13^48 x 12^16 种可能性。吃过路兵的位置有17种可能性。因此,大约需要240位。

总之,似乎通过存储棋子位置而不是每个方格的内容可以节省12.5%的空间。


0

我的两分钱

简短版

  • 选择易于计算两个位置相似度的数据格式。
  • 将位置数据存储在搜索程序附近(可能是内存中)。
  • 在搜索类似位置时,通过暴力搜索所有位置。
  • 可能将搜索分成多个线程/进程。

详细版

32字节(4 * 64位)是相当少的数据量。1000万个国际象棋位置可以放入30GB中。192位是24字节,这将变成23GB。可能数据库使用某种压缩技术,因此磁盘上的数据可能比这些数字少。我不知道存储的限制是什么,但由于这些编码似乎非常紧凑,所以尝试进一步最小化可能没有价值。

因为需要查找相似位置,所以我认为编码应该使比较不同位置变得容易。最好能够在不解码的情况下进行计数。为了实现这一点,编码可能应该是恒定长度的(无法想到使用可变长度编码的简单方法)。

索引可能加速相似性搜索。天真的方法是通过数据库中的位置索引所有位置。这将使得有32个索引(也许还有其他附加信息)。至少在理论上,这将使搜索变得非常快。

索引需要占用相当多的空间。可能比实际的位置数据还要多。而且它们可能并没有那么大的帮助。例如,查找黑色国王所在或接近e4的位置需要使用索引进行9次搜索,然后在30GB的位置信息中跳来跳去,这很可能需要随机访问磁盘位置。而且可能需要为一个以上的棋子查找类似的位置...

如果存储格式高效,仅仅通过暴力搜索所有位置数据并逐个检查相似位置就足够了。这将有效地利用CPU缓存。此外,由于记录长度恒定,因此可以轻松地将工作分配给多个处理器或计算机。

使用以棋子为中心或基于棋盘的存储格式取决于您将如何计算两个位置之间的相似性。以棋子为中心的方法可以轻松计算两个不同位置中一枚棋子的距离。然而,在以棋子为中心的方法中,每个棋子都是单独标识的,因此在特定位置找到一个兵并不容易。必须检查每个兵的位置。如果棋子的身份不是很重要,则基于棋盘的存储使得仅需检查兵是否在所需位置变得容易。另一方面,无法检查确切的兵是哪一个。


嗨Panu,非常感谢你。你提到的那篇文章很有启发性,特别是对于“如果你有一个野蛮的问题(和大量的力量),那么暴力破解就能奏效”的观点。然而,该文章所提到的性能是在操作RAM中的所有数据时才会出现,这对于国际象棋数据库来说不太可能发生(虽然将工作分配到多个节点上可以提高性能)。 一定会记住你的更长版本,感谢你的建议(它们很有价值!) - Adrian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接