最初に数学に関するある程度の混乱を解消し、次に 2 つの解決策について議論し、そのうちの 1 つのコードを示したいと思います。
yes-no クラス NP によく似た #P と呼ばれるカウント クラスがあります。質的にはNPよりも難しいです。このカウント問題が #P-hard よりも優れていると信じる特別な理由はありませんが、それを証明するのは難しいか簡単かもしれません.
ただし、多くの #P 困難問題と NP 困難問題は、実際に解決するのにかかる時間が大きく異なります。特定の困難な問題でさえ、入力のプロパティに応じて難しくなったり、簡単になったりすることがあります。NP 困難または #P 困難が意味するのは、困難なケースがあるということです。一部の NP 困難および #P 困難問題には、困難なケースが少ないか、完全に簡単なケースさえあります。(他のケースでは、最も難しいケースよりもはるかに簡単に見えるケースがほとんどありません。)
したがって、実際の問題は、関心のある入力に大きく依存する可能性があります。しきい値が高い側または低い側にある、または適切な数のキャッシュされた結果に対して十分なメモリがあるとします。次に、2 つのアイデアを利用する便利な再帰アルゴリズムがあります。そのうちの 1 つは既に述べたものです。そのうちの。(2) メモリが許す限り、残りのしきい値と一部のリスト フラグメントの小計をキャッシュする必要があります。キャッシングを改善するには、リストの 1 つから要素を順番に選択することもできます。
このアルゴリズムを実装する 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)
コメント行にあるように、しきい値のハード値を使用してこのコードをテストしました。すべての順列に対する単純な検索よりもかなり高速です。
3 つの条件が満たされる場合、このアルゴリズムよりも優れた別のアルゴリズムがあります: (1) 適切なキャッシュを作成するのに十分なメモリがない、(2) リスト エントリが小さな負でない整数である、(3)最も難しいしきい値に関心があります。この 2 番目のアルゴリズムを使用する 2 番目の状況は、他の条件が満たされているかどうかに関係なく、すべてのしきい値のカウントをフラットにしたい場合です。長さ n の 2 つのリストにこのアルゴリズムを使用するには、まず n 階乗より大きい 10 または 2 のべき乗である基数 x を選択します。今マトリックスを作る
M[i][j] = x**(list1[i]*list2[j])
Ryser の公式を使用してこの行列 M のパーマネントを計算すると、基数 x のパーマネントの k 桁目が、内積が正確に k である順列の数を示します。さらに、Ryser 式は、すべての順列を直接合計するよりもかなり高速です。(しかし、それでも指数関数的であるため、パーマネントの計算が #P 困難であるという事実と矛盾しません。)
また、順列の集合が対称群であることは事実です。このカウントの問題を加速するために、何らかの方法で群論を使用できれば素晴らしいことです。しかし、私の知る限り、その質問の説明からはそれほど深いものは何も得られません。
最後に、しきい値を下回る順列の数を正確にカウントするのではなく、その数を概算するだけでよい場合、おそらくゲームは完全に変わります。(パーマネントを多項式時間で概算できますが、ここでは役に立ちません。) どうすればよいかを考えなければなりません。いずれにせよ、それは提起された問題ではありません。
上記の説明と上記のコードに欠けている別の種類のキャッシング/動的プログラミングがあることに気付きました。コードに実装されているキャッシングは初期段階のキャッシングです。list1 の最初の数個の値だけが list2 に割り当てられ、残りのしきい値が複数回発生した場合、キャッシュによりコードは結果を再利用できます。これは、list1 と list2 のエントリが大きすぎない整数である場合にうまく機能します。ただし、エントリが典型的な浮動小数点数である場合は、失敗したキャッシュになります。
ただし、list1 のほとんどの値が割り当てられている場合は、反対側で事前計算することもできます。この場合、残りのすべての値の小計のソート済みリストを作成できます。そして、list1 を順番に使い切って、list2 側ですべての順列を実行できることを覚えておいてください。たとえば、list1 の最後の 3 つのエントリが [4,5,6] であり、list2 の 3 つの値 (中間のどこか) が [2.1,3.5,3.7] であるとします。次に、6 つの内積のソート済みリストをキャッシュします。
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 が十分に短い場合、リスト endcache[list2] でバイナリ検索を実行することにより、countprods ははるかに高速に作業を行うことができます。(スタックオーバーフローから、これが Python の bisect モジュールに実装されていることを知りましたが、いずれにせよパフォーマンス コードは Python では記述されません。) ヘッド キャッシュとは異なり、エンド キャッシュはコードを大幅に高速化できます。 list1 と list2 のエントリ間に数字の一致はありません。Ryser のアルゴリズムは、数値の一致がなければこの問題にも悪臭を放ちます。そのため、このタイプの入力では、2 つの加速度しか見られません。