哈希有序排列的最佳方法[1,9]

时间:2015-12-29 12:12:20

标签: python algorithm hash permutation 8-puzzle

我试图实施一种方法来保持8个谜题的访问状态不再生成 我最初的方法是将每个访问过的模式保存在列表中,并在每次算法想要生成子项时进行线性检查 现在我希望在O(1)时间通过列表访问来执行此操作。 8拼图中的每个模式都是1到9之间的数字的有序排列(9是空白块),例如125346987:

  

1 2 5
  3 4 6
  _ 8 7

此类所有可能排列的数量约为363,000(9!)。将这些数字散列到该大小列表的索引的最佳方法是什么?

9 个答案:

答案 0 :(得分:7)

您可以将N个项目的排列映射到N个项目的所有排列列表中的索引(按字典顺序排列)。

这是执行此操作的一些代码,并演示了它为4个字母序列的所有排列生成索引0到23一次。

import itertools

def fact(n):
    r = 1
    for i in xrange(n):
        r *= i + 1
    return r

def I(perm):
    if len(perm) == 1:
        return 0
    return sum(p < perm[0] for p in perm) * fact(len(perm) - 1) + I(perm[1:])

for p in itertools.permutations('abcd'):
    print p, I(p)

理解代码的最佳方法是证明其正确性。对于长度为n的数组,有(n-1)!首先出现阵列最小元素的排列,(n-1)!首先出现第二个最小元素的排列,依此类推。

因此,要查找给定排列的索引,请参阅计算有多少项小于排列中的第一项并将其乘以(n-1)!.然后递归地添加置换的其余部分的索引,被认为是(n-1)个元素的置换。基本情况是你有一个长度为1的排列。显然只有一个这样的排列,所以它的索引是0。

一个有效的例子:[1324]

  • [1324]:首先出现1,这是数组中最小的元素,因此得到0 *(3!)
  • 删除1会给我们[324]。第一个元素是3.有一个元素更小,所以给我们1 *(2!)。
  • 删除3会给我们[24]。第一个元素是2.那是剩下的最小元素,所以它给我们0 *(1!)。
  • 删除2会给我们[4]。只有一个元素,所以我们使用基本情况并得到0。

加起来,我们得到0 * 3! + 1 * 2! + 0 * 1! + 0 = 1 * 2!因此,[1324]位于4个排列的排序列表中的索引2处。这是正确的,因为在索引0为[1234]时,索引1为[1243],并且按字典顺序排列的下一个排列为[1324]

答案 1 :(得分:2)

我相信您要求的功能是将排列映射到数组索引。此字典将数字1-9的所有排列映射到0到9!-1的值。

import itertools
index = itertools.count(0)
permutations = itertools.permutations(range(1, 10))

hashes = {h:next(index) for h in permutations}

例如,哈希[(1,2,5,3,4,6,9,8,7)]的值为1445.

如果您需要字符串而不是元组,请使用:

permutations = [''.join(x) for x in itertools.permutations('123456789')]

或整数:

permutations = [int(''.join(x)) for x in itertools.permutations('123456789')]

答案 2 :(得分:1)

看起来你只对你是否已经访问了排列感兴趣。

您应该使用set。它授予您感兴趣的O(1)查找。

答案 3 :(得分:1)

使用

我为这个特定案例开发了一个启发式函数。这不是一个完美的散列,因为映射不在[0,9!-1]之间,而在[1,767359]之间,但它是O(1)

假设我们已经有一个文件/保留内存/ 767359位设置为0的任何内容(例如,mem = [False] * 767359)。让8puzzle模式映射到python字符串(例如,'125346987')。然后,哈希函数由下式确定:

def getPosition( input_str ):
data = []
opts = range(1,10)
n = int(input_str[0])
opts.pop(opts.index(n))
for c in input_str[1:len(input_str)-1]:
    k = opts.index(int(c))
    opts.pop(k)
    data.append(k)
ind = data[3]<<14 | data[5]<<12 | data[2]<<9 | data[1]<<6 | data[0]<<3 | data[4]<<1 | data[6]<<0
output_str = str(ind)+str(n)
output = int(output_str)
return output

即,为了检查是否已经使用了8puzzle模式= 125346987,我们需要:

pattern = '125346987'
pos = getPosition(pattern)
used = mem[pos-1] #mem starts in 0, getPosition in 1.

完美的哈希,我们需要9个!用于存储布尔值的位。在这种情况下,我们需要多2倍(767359/9! = 2.11),但请记住它甚至不是1Mb(几乎 100KB )。

请注意,该功能很容易反转。

检查

我可以用数学方式证明你为什么会这样做以及为什么不会有任何碰撞,但是因为这是一个编程论坛,所以让我们为每一个可能的排列运行它并检查所有的哈希值(位置)确实是不同的:

def getPosition( input_str ):
data = []
opts = range(1,10)
n = int(input_str[0])
opts.pop(opts.index(n))
for c in input_str[1:len(input_str)-1]:
    k = opts.index(int(c))
    opts.pop(k)
    data.append(k)
ind = data[3]<<14 | data[5]<<12 | data[2]<<9 | data[1]<<6 | data[0]<<3 | data[4]<<1 | data[6]<<0
output_str = str(ind)+str(n)
output = int(output_str)
return output


#CHECKING PURPOSES
def addperm(x,l):
    return [ l[0:i] + [x] + l[i:]  for i in range(len(l)+1) ]

def perm(l):
    if len(l) == 0:
        return [[]]
    return [x for y in perm(l[1:]) for x in addperm(l[0],y) ]

#We generate all the permutations
all_perms = perm([ i for i in range(1,10)])
print "Number of all possible perms.: "+str(len(all_perms)) #indeed 9! = 362880

#We execute our hash function over all the perms and store the output.
all_positions = [];
for permutation in all_perms:
    perm_string = ''.join(map(str,permutation))
    all_positions.append(getPosition(perm_string))

#We wan't to check if there has been any collision, i.e., if there
#is one position that is repeated at least twice.
print "Number of different hashes: "+str(len(set(all_positions))) 
#also 9!, so the hash works properly.

它是如何工作的?

这背后的想法与树有关:在一开始它有9个分支到9个节点,每个节点对应一个数字。从这些节点中的每个节点,我们有8个分支到8个节点,每个节点对应于除其父节点之外的数字,然后是7,依此类推。

我们首先将输入字符串的第一个数字存储在一个单独的变量中,并从我们的“节点”列表中弹出,因为我们已经采用了对应于第一个数字的分支。

然后我们有8个分支,我们选择与第二个数字对应的分支。注意,由于有8个分支,我们需要3个比特来存储我们选择的分支的索引,它可以采用的最大值是111第8个分支(我们将分支1-8映射到二进制{{1 }})。一旦我们选择并存储了分支索引,我们就会弹出该值,以便下一个节点列表不再包含该数字。

对于分支7,6和5,我们以相同的方式进行。注意,当我们有7个分支时,我们仍然需要3个比特,尽管最大值将是000-111。当我们有5个分支时,索引最多为二进制110

然后我们到达4个分支,我们注意到它只能存储2位,3个分支相同。对于2个分支,我们只需要1比特,对于最后一个分支,我们不需要任何位:只有一个分支指向最后一个数字,这将是我们1-9原始列表中的剩余部分。

那么,我们到目前为止:存储在分离变量中的第一个数字和表示分支的7个索引的列表。前4个索引可以用3比特表示,以下2个索引可以用2比特表示,最后一个索引用1比特表示。

我们的想法是以位形式连接所有这些索引以创建更大的数字。由于我们有17位,因此该数字最多为100。现在我们只将我们存储的第一个数字添加到该数字的末尾(最多这个数字将是9),我们可以创建的最大数字是2^17=131072

但是我们可以做得更好:回想一下,当我们有5个分支时,我们需要3个比特,尽管最大值是二进制1310729。如果我们安排我们的位,以便那些有0的人先来?如果是这样,在最坏的情况下,我们的最终位数将是以下的连接:

100

以十进制表示100 10 101 110 111 11 1。然后我们像以前一样继续(最后添加9),我们得到的最大可能生成数是76735,这是我们需要的位数并且对应于输入字符串767359,而最低可能的数字是987654321,它对应于输入字符串1

刚刚完成:有人可能想知道为什么我们将第一个数字存储在一个单独的变量中并在最后添加它。原因是如果我们保留了它,那么开头的分支数就是9,所以为了存储第一个索引(1-9),我们需要4位(123456789到{{1} })。这将使我们的映射效率降低,因为在这种情况下,最大可能的数量(因此需要的内存量)将是

0000

是十进制的1125311(1.13Mb对768Kb)。有趣的是,1000与四位的比率与仅添加十进制值(1000 100 10 101 110 111 11 1)相比有一定的意义,这很有意义(差异到期)事实上,在第一种方法中,我们没有完全使用4位)。

答案 4 :(得分:1)

此问题的空间以及查找高效结构是一种特里式结构,因为它将使用公共空间进行任何字典匹配 排列。 即在1234年和1235年用于“123”的空间将是相同的。

为简单起见,让我们假设0代替你的例子中的'_'。

<强>存储

  • 你的trie将是一个布尔树,根节点将是一个空节点,然后每个节点将包含9个布尔标志设置为false的子节点,9个子节点指定数字0到8和_。
  • 您可以随时创建trie,因为遇到排列,并通过将bool设置为true将遇到的数字存储为trie中的布尔值。

<强>查找

  • 根据排列的数字将trie从根遍历到子节点,如果节点已标记为true,则表示排列已经发生过。查找的复杂性只有9个节点跃点。

以下是trie如何寻找4位数的例子:

enter image description here

Python trie 这个特里可以很容易地存储在一个布尔列表中,比如myList。 其中myList [0]是根,如下面的概念所述:

https://webdocs.cs.ualberta.ca/~holte/T26/tree-as-array.html

列表中的最终trie大约为9 + 9 ^ 2 + 9 ^ 3 .... 9 ^ 8位,即所有查找小于10 MB。

答案 5 :(得分:0)

请注意,如果键入hash(125346987),则返回125346987.这是有原因的,因为将整数散列为整数以外的任何值都没有意义。

你应该做的是,当你发现一个模式将它添加到字典而不是列表时。这将提供您需要的快速查找,而不是像现在一样遍历列表。

所以说你找到了你可以做的模式125346987:

foundPatterns = {}
#some code to find the pattern
foundPatterns[1] = 125346987
#more code
#test if there?
125346987 in foundPatterns.values()
True

答案 6 :(得分:0)

如果必须始终拥有O(1),那么似乎有点数组可以完成这项工作。您只需要存储363,000个元素,这似乎是可行的。虽然注意到在实践中它并不总是更快。最简单的实现如下:

创建数据结构

visited_bitset = [False for _ in xrange(373000)]

测试当前状态并添加(如果尚未访问)

if !visited[current_state]:
    visited_bitset[current_state] = True

答案 7 :(得分:0)

保罗的answer可能有效。

Elisha的answer是完全有效的哈希函数,可以保证哈希函数不会发生冲突。对于保证无碰撞哈希函数,9!将是一个纯粹的最小值,但是(除非有人纠正我,保罗可能有)我不相信存在一个函数来将每个板映射到一个值域[0, 9!],更不用说只有O(1)的哈希函数。

如果你有1GB的内存来支持864197532(又名987654321-12346789)索引的布尔数组。您保证(计算上)O(1)要求。

实际上(意味着当你在真实系统中运行时)说这不是缓存友好的,但在纸面上这个解决方案肯定会有用。即使确实存在一个完美的函数,也要怀疑它也是缓存友好的。

使用sethashmap之类的预编译(抱歉,我暂时没有编程Python,所以不记得数据类型)必须有一个摊销的0(1) 。但是使用其中一个具有次优哈希函数(如n % RANDOM_PRIME_NUM_GREATER_THAN_100000)可能会提供最佳解决方案。

答案 8 :(得分:0)

首先。没有比布尔列表更快的了。您的任务总共有9! == 362880个可能的排列,这是存储在内存中的相当少量的数据:

visited_states = [False] * math.factorial(9)

或者,您可以使用array字节,这些字节稍慢(虽然不是很多)并且具有更低的内存占用(至少是一个数量级)。但是,考虑到下一步,使用数组可以节省大量内存。

第二。您需要将特定的排列转换为它的索引。有一些算法可以做到这一点,关于这个主题的最好的StackOverflow问题之一可能是这个:

Finding the index of a given permutation

你有固定的排列大小n == 9,所以无论算法有多复杂,在你的情况下它都等同于O(1)。

但是为了产生更快的结果,您可以预先填充一个映射字典,它将为您提供O(1)查找:

all_permutations = map(lambda p: ''.join(p), itertools.permutations('123456789'))
permutation_index = dict((perm, index) for index, perm in enumerate(all_permutations))

这本词典将消耗大约50 Mb的内存,实际上并没有那么多。特别是因为你只需要创建一次。

完成所有这些操作后,请检查您的特定组合:

visited = visited_states[permutation_index['168249357']]

将其标记为已访问是以相同的方式完成的:

visited_states[permutation_index['168249357']] = True

请注意,使用任何排列索引算法都会比映射字典慢得多。这些算法中的大多数都具有O(n 2 )复杂度,在您的情况下,即使对额外的python代码本身进行折扣,它也会导致性能降低81倍。因此,除非您有大量内存限制,否则使用映射字典可能是速度方面的最佳解决方案。

附录。正如Palec所指出的那样,实际上根本不需要visited_states列表 - 它完全可以存储True / {{ 1}}值直接在False字典中,这节省了一些内存和额外的列表查找。