我希望首先消除一些关于数学的混淆,然后讨论两个解决方案并给出其中一个的代码。
有一个称为#P的计数类,它非常像yes-no类NP。在定性意义上,它甚至比NP更难。没有特别的理由认为这个计数问题比#P-hard好,尽管证明它可能很难或很容易。
然而,许多#P-hard问题和NP-hard问题在实践中解决所需的时间差异很大,即使一个特定的难问题也可能因输入的属性不同而更难或更容易。NP-hard或#P-hard的含义是存在困难情况。一些NP-hard和#P-hard问题也有较少的困难情况,甚至有明显容易的情况。(其他问题则只有极少数情况比最难的情况容易得多。)
因此,实际问题可能会大大取决于感兴趣的输入。假设阈值较高或较低,或者您有足够的内存来缓存一定数量的结果。那么,有一个有用的递归算法利用了两个想法,其中一个已经提到:(1)在部分分配某些值之后,
剩余列表
片段的阈值可能排除所有排列,也可能允许它们全部。 (2)如果内存允许,则应缓存某些剩余阈值和某些列表片段的小计。为了改善缓存,您可以选择按顺序从一个列表中选择元素。
这是一个实现此算法的Python代码:
list1 = [1,2,3,4,5,6,7,8,9,10,11]
list2 = [1,2,3,4,5,6,7,8,9,10,11]
size = len(list1)
threshold = 396
cachecutoff = 6
def dotproduct(v,w):
return sum([a*b for a,b in zip(v,w)])
factorial = [1]
for n in xrange(1,len(list1)+1):
factorial.append(factorial[-1]*n)
cache = {}
def countprods(list1,list2,threshold):
if dotproduct(list1,list2) <= threshold:
return factorial[len(list1)]
if dotproduct(list1,reversed(list2)) > threshold:
return 0
if (tuple(list2),threshold) in cache:
return cache[(tuple(list2),threshold)]
total = 0
for n in xrange(len(list2)):
total += countprods(list1[1:],list2[:n] + list2[n+1:],
threshold-list1[0]*list2[n])
if len(list1) >= size-cachecutoff:
cache[(tuple(list2),threshold)] = total
return total
print 'Total permutations below threshold:',
print countprods(list1,list2,threshold)
print 'Cache size:',len(cache)
正如注释所说,我使用了一个硬阈值来测试这段代码。与对所有排列进行朴素搜索相比,它要快得多。
如果满足以下三个条件之一,则有另一种算法比此算法更好:(1)您没有足够的内存来进行良好的缓存,(2)列表条目是小的非负整数,(3)您对最困难的阈值感兴趣。第二种情况使用这个第二个算法是,如果你想要平坦地获得所有阈值的计数,不管是否满足其他条件。为了在两个长度为n的列表上使用此算法,首先选择一个基数x,它是大于n阶乘的10或2的幂。现在制作矩阵
M[i][j] = x**(list1[i]*list2[j])
如果使用
Ryser formula计算矩阵M的permanent,那么在基数为x的情况下,permanent的第k位数字告诉您点积恰好为k的排列数。此外,Ryser公式比直接对所有排列求和要快得多。(但它仍然是指数级的,因此它并不违反计算permanent是#P-hard的事实。)
另外,是的,排列集合是对称群。如果您能以某种方式使用群论加速这个计数问题,那就太好了。但据我所知,没有什么深刻的东西来自这个问题的描述。
最后,如果您只想近似计算低于阈值的排列数,那么游戏可能会完全改变。(您可以在多项式时间内近似计算permanent,但这在这里没有帮助。)我需要考虑该怎么做;在任何情况下,这都不是提出的问题。
我意识到以上讨论和代码中缺少另一种缓存/动态规划。代码中实现的缓存是早期缓存:如果只将list1的前几个值分配给list2,并且如果剩余的阈值出现多次,则缓存允许代码重用结果。如果list1和list2的条目是不太大的整数,则这很有效。但如果条目是典型的浮点数,则缓存将失败。
然而,您也可以在大部分list1的值被分配后的另一端进行预计算。在这种情况下,您可以为所有剩余值的子总数制作一个排序列表。记住,您可以按顺序使用list1,并在list2侧执行所有排列。例如,假设list1的最后三个条目是[4,5,6],并且假设list2中的三个值(位于中间某处)为[2.1,3.5,3.7]。然后,您将缓存六个点积的排序列表:
endcache[ [2.1, 3.5, 3.7] ] = [44.9, 45.1, 46.3, 46.7, 47.9, 48.1]
这对你有什么作用?如果查看我发布的代码,函数countprods(list1,list2,threshold)以子阈值递归地工作。第一个参数list1作为全局变量可能比作为参数更好。如果list2足够短,countprods可以通过在列表endcache [list2]中进行二进制搜索来更快地完成工作。(我刚从stackoverflow了解到这在Python的bisect模块中实现,尽管性能代码无论如何都不会用Python编写。)与头缓存不同,即使在list1和list2的条目中没有数字巧合,end cache也可以大大加速代码。对于没有数字巧合的这种输入,Ryser算法也很糟糕,因此我只看到两个加速:使用“all”测试和“none”测试锯掉搜索树的一支,并使用end cache。