6

{1,2,...n} (n と r はパラメーター) からの r 要素のすべての組み合わせを与えるジェネレーター (次の実行をサポートするイテレーター、おそらく Python で yield を使用する) を作成しようとしています。 r 個の要素を選択した場合、連続する要素は 2 つとありません。

たとえば、r = 2 および n = 4 の場合

生成された組み合わせは{1,3}, {1,4}, {2, 4}.

すべての組み合わせを (反復子として) 生成し、基準を満たさない組み合わせをフィルター処理することはできますが、不要な作業を行うことになります。

nextO(1)(それが不可能な場合はO(r)またはO(n))であるような生成アルゴリズムはありますか?

セットが返される順序は関係ありません (うまくいけば、O(1) アルゴリズムが許可されます)。

注: python のタグを付けましたが、言語に依存しないアルゴリズムも役に立ちます。

アップデート:

純粋な組み合わせを生成するためにそれをマッピングする方法を見つけました! Web 検索では、O(1) の組み合わせが可能であることがわかります (複雑に見えますが)。

これがマッピングです。

との組み合わせがあるとx_1, x_2, ... , x_rします。x_1 + 1 < x_2, x_2 + 1 < x_3, ...

次のようにマッピングしy_1, y_2, ..., y_rます

y_1 = x_1

y_2 = x_2 - 1

y_3 = x_3 - 2

...

y_r = x_r - (r-1)

このようにしてy_1 < y_2 < y_3 ... 、非連続制約なしでそれを実現できます!

これは基本的に、n-r+1 から r 個の要素を選択することになります。したがって、必要なのは (n-r+1 choose r) の生成を実行することだけです。

私たちの目的では、物事が生成された後にマッピングを使用するだけで十分です。

svkcr の回答を選択する理由

すべての素晴らしい回答ですが、svkcr の回答を選択しました。

ここにいくつかの理由があります

  1. それは事実上ステートレスです(より正確には「マルコフ」)。次の順列は、前の順列から生成できます。それはある意味でほぼ最適です: O(r) 空間と時間。

  2. それは予測可能です。組み合わせが生成される順序(辞書式)を正確に知っています。

これら 2 つのプロパティにより、生成の並列化 (予測可能なポイントでの分割とデリゲート) が容易になり、フォールト トレランスが導入されます (CPU/マシンに障害が発生した場合に、最後に生成された組み合わせから選択できます)。

申し訳ありませんが、並列化については以前は言及されていませんでした。これは、質問を書いたときに思い浮かびませんでしたが、後でそのアイデアを得たからです。

4

5 に答える 5

3

これは楽しいです!これはどう:

def nonconsecutive_combinations(r, n):
  # first combination, startng at 1, step-size 2
  combination = range(1, r*2, 2)
  # as long as all items are less than or equal to n
  while combination[r-1] <= n:
    yield tuple(combination)
    p = r-1 # pointer to the element we will increment
    a = 1   # number that will be added to the last element
    # find the rightmost element we can increment without
    # making the last element bigger than n
    while p > 0 and combination[p] + a > n:
      p -= 1
      a += 2
    # increment the item and
    # fill the tail of the list with increments of two
    combination[p:] = range(combination[p]+1, combination[p] + 2*(r-p), 2)

next()呼び出しにはO(r)が必要です..これが自然数にどのように変換されるかを考えながら考えましたが、詳細を正しく理解するにはかなりの時間がかかりました。

> list(nonconsecutive_combinations(2, 4))
[(1, 3), (1, 4), (2, 4)]
> list(nonconsecutive_combinations(3, 6))
[(1, 3, 5), (1, 3, 6), (1, 4, 6), (2, 4, 6)]

これがどのように機能するかを説明しようと思います。

結果セットの一部となる要素をc持つタプルの条件:r

  1. タプルの任意の要素は、少なくとも前の要素に2を加えたものと同じ大きさです。(c[x] >= c[x-1] + 2
  2. すべての要素が。以下ですn1.のため、最後の要素は。以下であると言えば十分nです。(c[r-1] <= n

結果セットの一部となる可能性のある最小のタプルはです(1, 3, 5, ..., 2*r-1)。タプルが別のタプルよりも「小さい」と言うときは、辞書式順序を想定しています。

Blckknghtが指摘しているように、可能な限り最小のタプルでさえ、条件2を満たすには大きすぎる可能性があります。

上記の関数には、2つのwhileループが含まれています。

  • 外側のループは結果をステップスルーし、結果が辞書式順序で表示され、条件1を満たしていると想定します。問題のタプルが条件2に違反するとすぐに、結果セットを使い果たして完了したことがわかります。

    combination = range(1, r*2, 2)
    while combination[r-1] <= n:
    

    最初の行は、条件1に従って、結果タプルを最初の可能な結果で初期化します。2行目は、条件2に正直に変換されます。

  • 内側のループは、条件1を満たす次の可能なタプルを見つけます。

    yield tuple(combination)
    

    条件(2)が真であり、結果が条件1を満たしていることを確認したのでwhile、現在の結果タプルを生成できます。

    次に、辞書式順序で次のタプルを見つけるために、最後の要素に「1」を追加します。

    # we don't actually do this:
    combination[r-1] += 1
    

    ただし、条件2が早すぎる可能性があります。したがって、その操作で条件2が破られる場合は、前の要素をインクリメントし、それに応じて最後の要素を調整します。これは、10を底とする整数を数えるのと少し似ています。「最後の桁が9より大きい場合は、前の桁をインクリメントして、最後の桁を0にします。」ただし、ゼロを追加する代わりに、条件1が真になるようにタプルを埋めます。

    # if above does not work
    combination[r-2] += 1
    combination[r-1]  = combination[r-2] + 2
    

    問題は、2行目が条件2を再び破る可能性があることです。つまり、実際に行うことは、最後の要素を追跡し、それがで行われることaです。また、p変数を使用して、調べているインデックスの現在の要素を参照します。

    p = r-1
    a = 1
    while p > 0 and combination[p] + a > n:
      p -= 1
      a += 2
    

    結果のタプルの項目を右から左(p = r-1、p-= 1)で繰り返します。最初は最後のアイテム(a = 1)に1を追加しますが、タプルをステップスルーするときに、実際には最後のアイテムを前のアイテムの値に、2つのアイテム間の距離を加え2*xた値に置き換えます。x(a + = 2、combination [p] + a)

    最後に、インクリメントするアイテムを見つけ、ステップサイズ2で、インクリメントされたアイテムから始まるシーケンスでタプルの残りの部分を埋めます。

    combination[p:] = range(combination[p]+1, combination[p] + 2*(r-p), 2)
    

    以上です。最初に考えたときはとても簡単に思えましたが、関数全体のすべての算術演算は、1つずつエラーを発生させるのに最適な場所であり、説明するのは本来よりも困難です。その内側のループを追加したとき、私は困っていることを知っていたはずです:)

パフォーマンスについて..

残念ながら、算術演算で満たされたループは、Pythonで記述するのに最も効率的なものではありません。他のソリューションはその現実を受け入れ、リスト内包表記またはフィルタリングを使用して、Pythonランタイムに重労働を押し下げます。これは私には正しいことのように思えます

一方、これがCの場合、私のソリューションはほとんどのソリューションよりもはるかに優れたパフォーマンスを発揮すると確信しています。内側のwhileループはO(log r)であり、結果をインプレースで同じO(log r )。追加のスタックフレームを消費せず、結果と2つの変数以外のメモリも消費しません。しかし、明らかにこれはCではないので、これは実際には重要ではありません。

于 2013-03-15T01:09:17.087 に答える
3

これが私の再帰ジェネレーターです(アイテムが選択されているn+1場合、アイテムをスキップするだけnです):

def non_consecutive_combinator(rnge, r, prev=[]):
    if r == 0:
        yield prev

    else:
        for i, item in enumerate(rnge):
            for next_comb in non_consecutive_combinator(rnge[i+2:], r-1, prev+[item]):
                yield next_comb

print list(non_consecutive_combinator([1,2,3,4], 2))
#[[1, 3], [1, 4], [2, 4]]
print list(non_consecutive_combinator([1,2,3,4,5], 2))
#[[1, 3], [1, 4], [1, 5], [2, 4], [2, 5], [3, 5]]
print list(non_consecutive_combinator(range(1, 10), 3))
#[[1, 3, 5], [1, 3, 6], [1, 3, 7], [1, 3, 8], [1, 3, 9], [1, 4, 6], [1, 4, 7], [1, 4, 8], [1, 4, 9], [1, 5, 7], [1, 5, 8], [1, 5, 9], [1, 6, 8], [1, 6, 9], [1, 7, 9], [2, 4, 6], [2, 4, 7], [2, 4, 8], [2, 4, 9], [2, 5, 7], [2, 5, 8], [2, 5, 9], [2, 6, 8], [2, 6, 9], [2, 7, 9], [3, 5, 7], [3, 5, 8], [3, 5, 9], [3, 6, 8], [3, 6, 9], [3, 7, 9], [4, 6, 8], [4, 6, 9], [4, 7, 9], [5, 7, 9]]

効率について:

このコードは、スタックをトラバースし、各反復で新しいコレクションを構築するため、O(1) にすることはできません。また、再帰ジェネレーターは、アイテムの組み合わせrを取得するために最大スタック深度を使用する必要があることを意味します。rこれは、 lowrを使用すると、スタックを呼び出すコストが非再帰生成よりも高くなる可能性があることを意味します。と が十分nrあれば、 itertools ベースのソリューションよりもはるかに効率的である可能性があります。

私はこの質問で2つのアップロードされたコードをテストしました:

from itertools import ifilter, combinations
import timeit

def filtered_combi(n, r):
    def good(combo):
        return not any(combo[i]+1 == combo[i+1] for i in range(len(combo)-1))
    return ifilter(good, combinations(range(1, n+1), r))

def non_consecutive_combinator(rnge, r, prev=[]):
    if r == 0:
        yield prev

    else:
        for i, item in enumerate(rnge):
            for next_comb in non_consecutive_combinator(rnge[i+2:], r-1, prev+[item]):
                yield next_comb

def wrapper(n, r):
    return non_consecutive_combinator(range(1, n+1), r)   

def consume(f, *args, **kwargs):
    deque(f(*args, **kwargs))

t = timeit.timeit(lambda : consume(wrapper, 30, 4), number=100)
f = timeit.timeit(lambda : consume(filtered_combi, 30, 4), number=100)

結果とその他の結果(編集)(windows7、python 64bit 2.7.3、8GB RAMのコアi5アイビーブリッジ):

(n, r)  recursive   itertools
----------------------------------------
(30, 4) 1.6728046   4.0149797   100 times   17550 combinations
(20, 4) 2.6734657   7.0835997   1000 times  2380 combinations
(10, 4) 0.1253318   0.3157737   1000 times  35 combinations
(4, 2)  0.0091073   0.0120918   1000 times  3 combinations
(20, 5) 0.6275073   2.4236898   100 times   4368 combinations
(20, 6) 1.0542227   6.1903468   100 times   5005 combinations
(20, 7) 1.3339530   12.4065561  100 times   3432 combinations
(20, 8) 1.4118724   19.9793801  100 times   1287 combinations
(20, 9) 1.4116702   26.1977839  100 times   220 combinations

ご覧のとおり、再帰的なソリューションと itertools.combination ベースのソリューションの間のギャップは、上になるほど大きくnなります

実際、両方のソリューション間のギャップが大きく依存しているため、rより大きなrものは、から生成されたより多くの組み合わせを破棄する必要があることを意味しますitertools.combinations。例えば ​​の場合n=20, r=9: 167960(20C9) の組み合わせの中から 220 の組み合わせのみをフィルタリングして取得します。nrが小さい場合、 itertools.combinationsr が少ないほど効率的であり、説明したようにスタックを使用しないため、使用が高速になります。(ご覧のとおり、 itertools は非常に最適化されています ( 、 、および一連のジェネレーターとリストの内包表記でロジックを記述した場合、foritertoolsifwhile抽象化されたものほど速くはなりません)。 python が大好き - コードをより高いレベルに引き上げると、報酬が得られます。そのための言語は多くありません。

于 2013-03-14T23:13:38.190 に答える
2

再帰ジェネレーターでの私の試みは次のとおりです。

def combinations_nonseq(r, n):
    if r == 0:
        yield ()
        return

    for v in range(2*r-1, n+1):
        for c in combinations_nonseq(r-1, v-2):
            yield c + (v,)

これは thkang の再帰ジェネレーターとほぼ同じアルゴリズムですが、より優れたパフォーマンスを発揮します。nが に近い場合r*2-1、改善は非常に大きく、小さいr値 ( に対してn) の場合は小さな改善です。また、svckr のコードよりも少し優れていますが、n値への明確な接続はありませんr

私が得た重要な洞察は、nが より小さい場合2*r-1、隣接する値を持たない組み合わせはあり得ないということでした。これにより、私のジェネレーターは thkang のものよりも少ない作業を行うことができます。

ここにいくつかのタイミングがあります.thkangのtestコードの修正版を使って実行してください. このモジュールを使用してtimeit、ジェネレーターの内容全体を 10 回消費するのにかかる時間を調べます。この#列は、コードによって生成された値の数を示しています (他のすべての値は同じであると確信しています)。

( n, r)      # |abarnert |  thkang |   svckr |BlckKnght| Knoothe |JFSebastian
===============+=========+=========+=========+=========+=========+========
(16, 2)    105 |  0.0037 |  0.0026 |  0.0064 |  0.0017 |  0.0047 |  0.0020
(16, 4)    715 |  0.0479 |  0.0181 |  0.0281 |  0.0117 |  0.0215 |  0.0143
(16, 6)    462 |  0.2034 |  0.0400 |  0.0191 |  0.0125 |  0.0153 |  0.0361
(16, 8)      9 |  0.3158 |  0.0410 |  0.0005 |  0.0006 |  0.0004 |  0.0401
===============+=========+=========+=========+=========+=========+========
(24, 2)    253 |  0.0054 |  0.0037 |  0.0097 |  0.0022 |  0.0069 |  0.0026
(24, 4)   5985 |  0.2703 |  0.1131 |  0.2337 |  0.0835 |  0.1772 |  0.0811
(24, 6)  27132 |  3.6876 |  0.8047 |  1.0896 |  0.5517 |  0.8852 |  0.6374
(24, 8)  24310 | 19.7518 |  1.7545 |  1.0015 |  0.7019 |  0.8387 |  1.5343

の値が大きい場合n、abarnert のコードは時間がかかりすぎたため、次のテストから除外しました。

( n, r)      # |  thkang |   svckr |BlckKnght| Knoothe |JFSebastian
===============+=========+=========+=========+=========+========
(32, 2)    465 |  0.0069 |  0.0178 |  0.0040 |  0.0127 |  0.0064
(32, 4)  23751 |  0.4156 |  0.9330 |  0.3176 |  0.7068 |  0.2766
(32, 6) 296010 |  7.1074 | 11.8937 |  5.6699 |  9.7678 |  4.9028
(32, 8)1081575 | 37.8419 | 44.5834 | 27.6628 | 37.7919 | 28.4133

私がテストしてきたコードはhereです。

于 2013-03-15T06:18:02.027 に答える
2

O(1) ですべての組み合わせを生成する方法があれば、生成してフィルタリングするだけで O(r) でこれを行うことができます。itertools.combinationsO(1) があると仮定するとnext、スキップする最大 r-1 値があるため、最悪の場合は r-1 × O(1) ですよね?

混乱を避けるために少し先に進みますが、O(1) の実装はないと思います。combinationsしたがって、これはO(r)ではありません。しかし、それが可能なことはありますか?わからない。とにかく…</p>

そう:

def nonconsecutive_combinations(p, r):
    def good(combo):
        return not any(combo[i]+1 == combo[i+1] for i in range(len(combo)-1))
    return filter(good, itertools.combinations(p, r))

r, n = 2, 4
print(list(nonconsecutive_combinations(range(1, n+1), r))

これは以下を出力します:

[(1, 3), (1, 4), (2, 4)] 

ドキュメントは、O(1) があるitertoolsことを保証しません。しかし、可能性のある O(1) アルゴリズムがあれば、彼らはそれを使用するように思われます。combinationsnext

ソースコードを読むか、時間を計ることができます...しかし、それを行うつもりです。全体の時間を計りましょう。

http://pastebin.com/ydk1TMbDには、私のコード、thkang のコード、およびテスト ドライバーがあります。印刷回数は、シーケンス全体を反復するコストをシーケンスの長さで割ったものです。

n4 ~ 20 の範囲でr2 に固定すると、両方の時間が減少することがわかります。(シーケンスを反復する合計時間はもちろん増加していることを思い出してthe total lengthください。nr

12にn固定しr、2 から 5 の範囲で、両方の時間は 2 から 5 まで直線的に増加しますが、1 と 6 の時間は予想よりもはるかに高くなります。

よく考えてみると、924 個の値のうち、適切な値は 6 個しかありません。nextそして、それが、時間あたりが上がるにつれて下がっていた理由nです。合計時間は増加していますが、生成される値の数はさらに速く増加しています。

したがって、combinationsO(1) はありませんnext。それが持っているのは複雑なものです。私のアルゴリズムには O(r) がありませんnext。それはまた複雑なことです。パフォーマンスの保証は、per よりもイテレーション全体で指定する方がはるかに簡単だと思いますnext(計算方法を知っていれば、値の数で割るのは簡単です)。

いずれにせよ、テストした 2 つのアルゴリズムのパフォーマンス特性はまったく同じです。return(奇妙なことに、ラッパーを切り替えてyield from、再帰的なものをより速くし、フィルタリングのものをより遅くしました...しかし、とにかくそれは小さな一定の要因なので、誰が気にしますか?)

于 2013-03-14T23:17:40.123 に答える
1

@thkangの回答に似たソリューションですが、明示的なスタックを使用しています:

def combinations_stack(seq, r):
    stack = [(0, r, ())]
    while stack:
        j, r, prev = stack.pop()
        if r == 0:
            yield prev
        else:
            for i in range(len(seq)-1, j-1, -1):
                stack.append((i+2, r-1, prev + (seq[i],)))

例:

print(list(combinations_stack(range(1, 4+1), 2)))
# -> [(1, 3), (1, 4), (2, 4)]

一部の値については、私のマシンのベンチマーク(n, r)によると、これが最速のソリューションです。

name                                time ratio comment
combinations_knoothe           17.4 usec  1.00 8 4
combinations_blckknght         17.9 usec  1.03 8 4
combinations_svckr             20.1 usec  1.16 8 4
combinations_stack             62.4 usec  3.59 8 4
combinations_thkang            69.6 usec  4.00 8 4
combinations_abarnert           123 usec  7.05 8 4
name                                time ratio comment
combinations_stack              965 usec  1.00 16 4
combinations_blckknght         1e+03 usec  1.04 16 4
combinations_knoothe           1.62 msec  1.68 16 4
combinations_thkang            1.64 msec  1.70 16 4
combinations_svckr             1.84 msec  1.90 16 4
combinations_abarnert           3.3 msec  3.42 16 4
name                                time ratio comment
combinations_stack               18 msec  1.00 32 4
combinations_blckknght         28.1 msec  1.56 32 4
combinations_thkang            40.4 msec  2.25 32 4
combinations_knoothe           53.3 msec  2.96 32 4
combinations_svckr             59.8 msec  3.32 32 4
combinations_abarnert          68.3 msec  3.79 32 4
name                                time ratio comment
combinations_stack             1.84  sec  1.00 32 8
combinations_blckknght         2.27  sec  1.24 32 8
combinations_svckr             2.83  sec  1.54 32 8
combinations_knoothe           3.08  sec  1.68 32 8
combinations_thkang            3.29  sec  1.79 32 8
combinations_abarnert            22  sec 11.99 32 8

combinations_knoothe、質問で説明されているアルゴリズムの実装です。

import itertools
from itertools import imap as map

def _combinations_knoothe(n, r):
    def y2x(y):
        """
        y_1 = x_1
        y_2 = x_2 - 1
        y_3 = x_3 - 2
        ...
        y_r = x_r - (r-1)
        """
        return tuple(yy + i for i, yy in enumerate(y))
    return map(y2x, itertools.combinations(range(1, n-r+1+1), r))

def combinations_knoothe(seq, r):
    assert seq == list(range(1, len(seq)+1))
    return _combinations_knoothe(len(seq), r)

その他の関数は、対応する回答からのものです (統一された形式で入力を受け入れるように変更されています)。

于 2013-03-15T07:06:42.170 に答える