我想首先消除对数学的一定程度的困惑,然后讨论两种解决方案并为其中一种提供代码。
有一个称为#P 的计数类,它很像是-否类 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 # This is smack in the middle, a hard value
cachecutoff = 6 # Cache results when up to this many are assigned
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 = {}
# Assumes two sorted lists of the same length
def countprods(list1,list2,threshold):
if dotproduct(list1,list2) <= threshold: # They all work
return factorial[len(list1)]
if dotproduct(list1,reversed(list2)) > threshold: # None work
return 0
if (tuple(list2),threshold) in cache: # Already been here
return cache[(tuple(list2),threshold)]
total = 0
# Match the first element of list1 to each item in list2
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 公式计算此矩阵 M 的永久物,则以x 为底的永久物的第 k 位告诉您点积恰好为 k 的排列数。此外,Ryser 公式比直接对所有排列求和要快得多。(但它仍然是指数级的,所以它与计算永久物是#P-hard这一事实并不矛盾。)
另外,是的,排列集确实是对称群。如果你能以某种方式使用群论来加速这个计数问题,那就太好了。但据我所知,对这个问题的描述并没有那么深刻。
最后,如果您不想精确计算低于阈值的排列数,而只想近似该数字,那么游戏可能会完全改变。(您可以在多项式时间内近似永久值,但这在这里无济于事。)我必须考虑该怎么做;无论如何,这不是提出的问题。
我意识到上面的讨论和上面的代码中缺少另一种缓存/动态编程。代码中实现的缓存是早期缓存:如果仅将 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 的条目之间没有数字重合。Ryser 的算法在没有数值巧合的情况下也很讨厌这个问题,所以对于这种类型的输入,我只看到两个加速度: