识别唯一自由多边形(或多边形散列)的算法

时间:2012-08-17 17:12:48

标签: algorithm hash coordinates

简而言之:如何散列免费的多米诺骨牌?

这可以推广到:如何有效地散列2D整数坐标的任意集合,其中集合包含唯一的非负整数对,并且当且仅当没有平移,旋转时,集合被认为是唯一的,或翻转可以将它映射到另一组?

对于不耐烦的读者,请注意我完全了解蛮力方法。我正在寻找一种更好的方法 - 或者是一种非常有说服力的证据,证明其他任何方式都不存在。

我正在研究一些不同的算法来生成随机polyominos。我想测试它们的输出以确定它们的随机性 - 即,给定订单的某些实例比其他订单更频繁地生成。在视觉上,很容易识别自由多边形的不同方向,例如下面的维基百科插图显示了“F”pentomino(Source)的所有8个方向:

The F pentimino

如何在这个多米诺骨牌上放一个数字 - 也就是说,哈希一个免费的多米诺骨牌?我不想依赖预先列出的“命名”多项式列表。无论如何,仅对订单4和5存在广泛认可的名称。

这不一定等于枚举给定订单的所有免费(或单边或固定)多边形。我只想计算给定配置出现的次数。如果生成算法永远不会产生某种多边形,那么就不会计算它。

计算的基本逻辑是:

testcount = 10000 // Arbitrary
order = 6         // Create hexominos in this test
hashcounts = new hashtable
for i = 1 to testcount
    poly = GenerateRandomPolyomino(order)
    hash = PolyHash(poly)
    if hashcounts.contains(hash) then  
        hashcounts[hash]++
    else
        hashcounts[hash] = 1 

我正在寻找的是一种有效的PolyHash算法。输入多边形简单地定义为一组坐标。 T tetronimo的一个方向可以是,例如:

[[1,0], [0,1], [1,1], [2,1]]:

 |012
-+---
0| X
1|XXX

您可以假设输入多联骨牌已经被标准化以与X和Y轴对齐并且仅具有正坐标。形式上,每一组:

  • 至少有一个坐标,其中x值为0
  • 至少有一个y值为0的坐标
  • 没有任何坐标,其中x&lt; 0或y <0 0

我真的在寻找能够避免一般暴力方法所需的整数运算数量增加的新算法,如下所述。

蛮力

强制解决方案建议herehere包括将每个集合散列为无符号整数,使用每个坐标作为二进制标志,并采用所有可能旋转的最小散列(在我的情况下)翻转),其中每个旋转/翻转也必须转换为原点。这导致每个输入集总共有23个设置操作来获得“免费”哈希:

  • 旋转(6x)
  • 翻转(1x)
  • 翻译(7x)
  • 哈希(8x)
  • 查找最小计算哈希值(1x)

获取每个哈希的操作序列是:

  1. 哈希
  2. 旋转,翻译,哈希
  3. 旋转,翻译,哈希
  4. 旋转,翻译,哈希
  5. 翻转,翻译,哈希
  6. 旋转,翻译,哈希
  7. 旋转,翻译,哈希
  8. 旋转,翻译,哈希

6 个答案:

答案 0 :(得分:11)

好吧,我提出了一种完全不同的方法。 (还要感谢corsiKa提供了一些有用的见解!)而不是对正方形进行散列/编码,对它们周围的路径进行编码。该路径由在绘制每个单元段之前执行的一系列“转弯”(包括无转弯)组成。我认为从正方形坐标获取路径的算法超出了这个问题的范围。

这非常重要:它会破坏我们不需要的所有位置和方向信息。获取翻转对象的路径也非常容易:只需反转元素的顺序即可。存储紧凑,因为每个元素只需要2位。

它确实引入了一个额外的约束:多边形不能有完全封闭的孔。 (形式上,它必须是simply connected。)大多数关于polyominos的讨论都认为存在一个洞,即使它只有两个接触角被密封,因为这可以防止与任何其他非平凡多边形的平铺。触摸边缘不会妨碍边缘(如带有孔的单个heptomino),但它不能像一个完整的环形八角形那样从一个外环跳到内环:

enter image description here

它还产生了一个额外的挑战:找到编码路径循环的最小排序。这是因为路径的任何旋转(在字符串旋转的意义上)都是有效的编码。要始终获取相同的编码,我们必须找到路径指令的最小(或最大)旋转。值得庆幸的是,这个问题已经解决了:例如参见http://en.wikipedia.org/wiki/Lexicographically_minimal_string_rotation

示例

如果我们随意为移动操作分配以下值:

  • 不转:1
  • 右转:2
  • 左转:3

这是顺时针跟踪的F pentomino:

enter image description here

F pentomino的任意初始编码是(从右下角开始):

2,2,3,1,2,2,3,2,2,3,2,1

编码的最小旋转次数为

1,2,2,3,1,2,2,3,2,2,3,2

如果有12个元素,如果每条指令使用两位,则该循环可以打包成24位,如果指令被编码为3的幂,则只能打包19位。即使使用2位元素编码也很容易适合单个无符号32位整数0x6B6BAE

   1- 2- 2- 3- 1- 2- 2- 3- 2- 2- 3- 2
= 01-10-10-11-01-10-10-11-10-10-11-10
= 00000000011010110110101110101110
= 0x006B6BAE

具有3个最重要幂的循环开始的base-3编码是0x5795F

    1*3^11 + 2*3^10 + 2*3^9 + 3*3^8 + 1*3^7 + 2*3^6 
  + 2*3^5  + 3*3^4  + 2*3^3 + 2*3^2 + 3*3^1 + 2*3^0
= 0x0005795F

订单n的多边形周围路径中的最大顶点数为2n + 2。对于2位编码,位数是移动数的两倍,因此所需的最大位为4n + 4。对于base-3编码,它是:

Base 3 Encoded max bits

“绞架”是天花板的功能。因此,直到9阶的任何多原子可以在单个32位整数中编码。知道了这一点,您可以相应地选择特定于平台的数据结构,以便在您将要散列的多项式的最大顺序的情况下进行最快的哈希比较。

答案 1 :(得分:4)

您可以将其减少到8个哈希操作,而无需翻转,旋转或重新翻译。

请注意,此算法假设您使用相对于自身的坐标进行操作。也就是说它不在野外。

而不是应用翻转,旋转和翻译的操作,而只是改变您散列的顺序。

例如,让我们采取上面的F pent。在简单的例子中,让我们假设散列操作是这样的:

int hashPolySingle(Poly p)
    int hash = 0
    for x = 0 to p.width
        fory = 0 to p.height
            hash = hash * 31 + p.contains(x,y) ? 1 : 0
    hashPolySingle = hash

int hashPoly(Poly p)
    int hash = hashPolySingle(p)
    p.rotateClockwise() // assume it translates inside
    hash = hash * 31 + hashPolySingle(p)
    // keep rotating for all 4 oritentations
    p.flip()
    // hash those 4

不是将函数应用于poly的所有8个不同方向,而是将8个不同的哈希函数应用于1个poly。

int hashPolySingle(Poly p, bool flip, int corner)
    int hash = 0
    int xstart, xstop, ystart, ystop
    bool yfirst
    switch(corner)
        case 1: xstart = 0
                xstop = p.width
                ystart = 0
                ystop = p.height
                yfirst = false
                break
        case 2: xstart = p.width
                xstop = 0
                ystart = 0
                ystop = p.height
                yfirst = true
                break
        case 3: xstart = p.width
                xstop = 0
                ystart = p.height
                ystop = 0
                yfirst = false
                break
        case 4: xstart = 0
                xstop = p.width
                ystart = p.height
                ystop = 0
                yfirst = true
                break
        default: error()
    if(flip) swap(xstart, xstop)
    if(flip) swap(ystart, ystop)

    if(yfirst)
        for y = ystart to ystop
            for x = xstart to xstop
                hash = hash * 31 + p.contains(x,y) ? 1 : 0
    else
        for x = xstart to xstop
            for y = ystart to ystop
                hash = hash * 31 + p.contains(x,y) ? 1 : 0
    hashPolySingle = hash

然后以8种不同的方式调用它。你也可以将hashPolySingle封装在for的循环中,然后在翻转周围。一切都一样。

int hashPoly(Poly p)
    // approach from each of the 4 corners
    int hash = hashPolySingle(p, false, 1)
    hash = hash * 31 + hashPolySingle(p, false, 2)
    hash = hash * 31 + hashPolySingle(p, false, 3)
    hash = hash * 31 + hashPolySingle(p, false, 4)
    // flip it
    hash = hash * 31 + hashPolySingle(p, true, 1)
    hash = hash * 31 + hashPolySingle(p, true, 2)
    hash = hash * 31 + hashPolySingle(p, true, 3)
    hash = hash * 31 + hashPolySingle(p, true, 4)
    hashPoly = hash

通过这种方式,您可以隐式地从每个方向旋转多边形,但实际上并没有执行旋转和平移。它执行8个哈希,这似乎是完全必要的,以便准确地哈希所有8个方向,但浪费没有通过聚合物实际上没有哈希。在我看来,这是最优雅的解决方案。

请注意,可能有更好的hashPolySingle()算法可供使用。我使用的笛卡尔耗尽算法大约为O(n^2)。最糟糕的情况是L形,这将导致仅N/2 * (N-1)/2元素的N大小的正方形,或效率1:(N-1)/4,与I形状相比1:1。它也可能是由架构强加的固有不变量实际上使其效率低于朴素算法。

我怀疑通过将节点集转换为可以遍历的双向图来模拟笛卡尔耗尽可以缓解上述问题,导致节点被击中的顺序与我更加天真的相同散列算法,忽略空格。这将使算法降至O(n),因为图表应该能够在O(n)时间内构建。因为我没有这样做,我不能肯定地说,这就是为什么我说这只是一种怀疑,但是应该有办法去做。

答案 2 :(得分:3)

这是我的DFS(深度优先搜索)解释:

从最顶层的单元格开始(最左边作为决胜局)。将其标记为已访问。每次访问一个单元格时,请检查所有四个方向的未访问邻居。始终按此顺序检查四个方向:向上,向左,向下,向右。

实施例

enter image description here

在此示例中,向上和向左失败,但向下成功。到目前为止,我们的输出是001,我们递归搜索“向下”单元格。

我们将新的当前单元格标记为已访问(当我们完成搜索此单元格时,我们将完成搜索原始单元格)。这里,up = 0,left = 1.

我们搜索最左边的单元格,没有未观察到的邻居(up = 0,left = 0,down = 0,right = 0)。到目前为止,我们的总产量为001010000。

enter image description here

我们继续搜索第二个单元格。 down = 0,right = 1。我们搜索右边的单元格。

enter image description here

up = 0,left = 0,down = 1。搜索向下单元格:全0。到目前为止的总产量是001010000010010000.然后,我们从下行单元返回...

enter image description here

右= 0,返回。返回。 (现在,我们在起始单元格。)right = 0。完成!

因此,总输出为20(N * 4)位:00101000001001000000。

编码改进

但是,我们可以节省一些比特。

最后访问过的单元格将始终为其四个方向编码0000。因此,不要对最后访问的单元进行编码以保存4位。

另一个改进:如果你通过向左移动到达一个单元格,不要检查右侧的单元格。因此,我们每个单元只需要3位,第一个单元格除了4位,最后一个单元格需要0位。

第一个单元格永远不会有一个向上或左边的邻居,所以省略这些位。所以第一个单元格需要2位。

因此,通过这些改进,我们仅使用N * 3-4比特(例如,5个单元 - > 11比特; 9个单元 - > 23比特)。

如果你真的想要,你可以通过注意到N-1位将是“1”来压缩一点。

买者

是的,你需要编码polyomino的所有8次旋转/翻转,并选择最少来获得规范编码。

我怀疑这仍然比大纲方法更快。此外,多米诺骨牌中的洞不应成为问题。

答案 3 :(得分:1)

我最近解决了同样的问题。我相当简单地解决了这个问题 (1)为多边形生成唯一的ID,使得每个相同的多边形具有相同的UID。例如,找到边界框,规范化边界框的角,并收集非空单元格集。 (2)通过旋转(和翻转,如果合适的话)多边形来生成所有可能的排列,并查找重复项。

除了简单之外,这种粗暴方法的优点在于它仍然有效 多边形可以通过其他方式区分,例如,如果其中一些是彩色的或编号的。

答案 4 :(得分:1)

您可以设置类似trie的内容来唯一标识(而不仅仅是哈希)多媒体。取你的规范化多边形并设置一个二叉搜索树,其中根分支是否(0,0)是否有一个设置像素,下一级分支是否(0,1)是否有一个设置像素,依此类推。当你查找多边形时,只需将其标准化,然后走树。如果你在trie中找到它,那么你就完成了。如果没有,请将该polyomino指定为唯一ID(仅增加计数器),生成所有8种可能的旋转和翻转,然后将这些8添加到trie中。

在特里小姐身上,你必须产生所有的旋转和反射。但是对于特里击中它应该花费更少(对于k-polyominos来说是O(k ^ 2))。

为了使查找更加高效,您可以一次使用几个位并使用更宽的树而不是二叉树。

答案 5 :(得分:0)

一个有效的哈希函数,如果你真的害怕哈希冲突,就是为坐标做一个哈希函数x + order * y然后循环通过一个片段的所有坐标,添加(order ^ i)* hash (coord [i])到片段哈希。这样,您可以保证不会发生任何哈希冲突。