11


5

シャッフルされたリストをインプレースでO(1)メモリで印刷するアルゴリズム

この質問私は疑問に思い始めました:オリジナルを変更またはコピーしないシャッフルアルゴリズムを持つことは可能ですか?リスト?

明確にするために:

オブジェクトのリストが与えられたと想像してください。 リストのサイズは任意ですが、かなり大きいと仮定します(10,000,000アイテムなど)。 リストのアイテムをランダムな順序で印刷する必要があり、できるだけ早くそれを行う必要があります。 ただし、次のことはできません。

  • 元のリストは非常に大きく、コピーすると 多くのメモリを無駄にします(おそらく利用可能なRAMの制限に達します)。

  • 何らかの方法で並べ替えられているため、元のリストを変更します 他の部分は後でソートされることに依存します。

  • インデックスリストを作成します。これも、リストが非常に大きく、 コピーには多くの時間とメモリが必要です。 (明確化:これは、元のリストと同じ要素数を持つ他のリストを意味します)。

これは可能ですか?

*追加:*その他の説明。

  1. リストをすべてランダムにシャッフルしてほしい 並べ替えも同様に可能です(もちろん、最初に適切なRand()関数があると仮定します)。

  2. ポインターのリスト、またはインデックスのリストを作成するという提案、または 元のリストと同じ数の要素を持つ他のリストは、元の質問によって非効率的であると明示的に見なされます。 必要に応じて追加のリストを作成できますが、元のリストよりも大幅に小さくする必要があります。

  3. 元のリストは配列のようなもので、任意のアイテムを取得できます O(1)のインデックスによって。 (したがって、二重にリンクされたリストはありません。リストを繰り返し処理して、目的のアイテムに到達する必要があります。)

*追加2 *:O​​K、このようにしましょう。各512バイトのサイズ(単一セクター)のデータ項目で満たされた1TB HDDがあります。 すべてのアイテムをシャッフルしながら、このすべてのデータを別の1TB HDDにコピーします。 できるだけ早くこれを行いたい(シングルパスオーバーデータなど)。 512 MBのRAMが使用可能であり、スワップに依存しません。 (これは理論的なシナリオであり、実際にはこのようなものはありません。 完璧なalgorithm.itemを見つけたいだけです。)

10 Answer


10


まあそれはシャッフルを除いてどのようなランダム性に少し依存します、すなわち すべてのシャッフルが可能性が高いか、分布が歪んでいる可能性があります。

N個の整数の「ランダムに見える」順列を生成する数学的な方法があるため、Pが0..N-1から0..N-1の順列である場合、xを0からN-1に反復するだけで、 L(x)の代わりにリスト項目L(P(x))を出力し、シャッフルを取得しました。 そのような順列は、例えば、取得することができます。 モジュラー演算を使用します。 たとえば、Nが素数の場合、P(x)=(x * k)mod Nは、0 <k <Nの順列です(ただし、0から0にマップされます)。 素数Nの類似性、たとえばP(x)=(x ^ 3)mod Nは順列である必要があります(ただし、0から0および1から1にマッピングされます)。 このソリューションは、Nより上の最小プライム(Mと呼ぶ)を選択し、Mまで並べ替え、Nより上の並べ替えられたインデックスを破棄することにより、非プライムNに簡単に拡張できます(以下同様)。

モジュラーべき乗は多くの暗号化アルゴリズムの基礎であることに注意する必要があります(例: RSA、Diffie-Hellman)、およびこの分野の専門家による強力な擬似ランダム操作と見なされています。

別の簡単な方法(素数は不要)は、最初にドメインを展開して、Nの代わりにMを検討することです。ここで、MはNの2のべき乗の最小値です。 だから例えば N = 12の場合、M = 16を設定します。 次に、全単射ビット演算を使用します。

P(x) = ((x ^ 0xf) ^ (x << 2) + 3) & 0xf

次に、リストを出力するときに、xを0からM-1まで繰り返し、P(x)が実際に<Nである場合にのみL(P(x))を出力します。

「真の不偏ランダム」ソリューションは、暗号的に強力なブロック暗号(例: AES)とランダムキー(k)を使用して、シーケンスを反復処理する

AES(k, 0), AES(k, 1), ...

AES(k、i)<Nの場合、対応する項目をシーケンスから出力します。 これは一定の空間(暗号に必要な内部メモリ)で実行でき、ランダムな置換(暗号の暗号特性のため)と区別できませんが、明らかに非常に遅いです。 AESの場合、i = 2 ^ 128になるまで繰り返す必要があります。


4


コピーの作成、変更、またはアクセスした要素の追跡は許可されていませんか? それは不可能だと言うつもりです。 3番目の基準を誤解していない限り。

私は、あなたが言うことを許されていないことを意味し、対応する要素を印刷したときにtrueに設定された10,000,000の対応するブール値の配列を作成します。 そして、10,000,000のインデックスのリストを作成し、リストをシャッフルし、その順序で要素を印刷することは許可されていません。


2


これらの10,000,000個のアイテムは、実際のアイテムへの参照(またはポインター)に過ぎないため、リストはそれほど大きくありません。 すべての参照とそのリストの内部変数のサイズに対して、32ビットアーキテクチャで最大40MBのみ。 アイテムが参照サイズよりも小さい場合は、リスト全体をコピーするだけです。


2


次のいずれかを行う必要があるため、_truly_乱数ジェネレーターでこれを行うことはできません。

  • どの番号がすでに選択されているかを覚えて、それらをスキップします ブール値のO(n)リストを必要とし、ますます数字をスキップするにつれて実行時間が徐々に悪化します);または

  • 各選択後にプールを減らします(どちらかが必要です 元のリストへの変更または変更する個別のO(n)リスト)。

どちらもあなたの質問の可能性ではないので、「いいえ、できません」と言わなければなりません。

この場合に使用する傾向があるのは、使用した値のビットマスクですが、スキップした場合ではありません。前述のように、使用した値が累積すると実行時間が悪化します。

ビットマスクは、元の39Gbのリスト(1000万ビットは約1.2Mのみ)よりも大幅に優れており、まだO(n)であるにもかかわらず、要求どおりに桁違いに小さくなります。

実行時の問題を回避するには、毎回1つの乱数のみを生成し、関連する「使用済み」ビットが既に設定されている場合は、設定されていないビットマスクが見つかるまでビットマスクをスキャンします。

つまり、あなたはぶらぶらしているわけではなく、乱数生成器がまだ使用されていない番号をあなたに与えるために必死です。 実行時間は、1.2Mのデータをスキャンするのにかかる時間と同じくらい悪くなります。

もちろん、これは、いつでも選択された特定の番号が、既に選択されている番号に基づいてスキューされることを意味しますが、それらの番号はとにかくランダムであるため、スキューはランダムです(そして、番号が最初から本当にランダムである場合、スキューは問題になりません)。

また、さらに多様性が必要な場合は、検索方向を上下にスキャンすることもできます。

結論:あなたが求めていることは実行可能であるとは思いませんが、妻が迅速かつ頻繁に証明するように、私は以前間違っていたことに留意してください:-)しかし、すべてのものと同様に、通常はそのような問題を回避してください。


2


以下は、PRNGスキームが機能しないという非常に単純な証拠です。

_ PRNGアイデアには2つのフェーズがあります。まず、PRNGとその初期状態を選択します。次に、PRNGを使用して出力をシャッフルします。 さて、_N!_可能な順列があるので、少なくとも_N!_可能な異なる開始状態が必要であり、フェーズ2に入ります。 つまり、フェーズ2の開始時には、少なくとも_log〜2〜N!_ビットの状態が必要です。これは許可されていません。 _

ただし、これは、アルゴリズムが環境から新しいランダムビットを受け取るスキームを除外しません。 たとえば、初期状態_lazily_を読み取り、まだ繰り返さないことが保証されているPRNGが存在する場合があります。 存在しないことを証明できますか?

完全なシャッフルアルゴリズムがあるとします。 実行を開始し、途中で終了したらコンピューターをスリープ状態にしたと想像してください。 これで、プログラムの完全な状態がどこかに保存されました。 _S_を、プログラムがこの中間点にある可能性のあるすべての可能な状態のセットとします。

アルゴリズムは正しく、終了が保証されているため、保存されたプログラムの状態と十分な長さのビット文字列を指定すると、シャッフルを完了するディスク読み取りおよび書き込みの有効なシーケンスを生成する関数_f_があります。 コンピューター自体がこの機能を実装しています。 しかし、数学関数として考えてください:

f(S×ビット)→読み取りと書き込みのシーケンス

そして、些細なことに、保存されたプログラム状態のみを与えて、まだ読み書きされていないディスク位置のセットを生成する関数_g_が存在します。 (単純に任意のビット文字列を_f_に渡してから、結果を見てください。)

gS読み書きする場所のセット

証明の残りの部分は、アルゴリズムの選択に関係なく、g_のドメインに少なくとも〜N〜C〜N / 2〜_の異なるセットが含まれていることを示すことです。 それが当てはまる場合、_S_の要素が少なくともそれだけ存在する必要があるため、プログラムの状態には、中間マークに少なくとも_log〜2〜〜N〜C〜N / 2〜_ビットが含まれている必要があります。要件。

ただし、最後のビットを証明する方法はわかりません。なぜなら、アルゴリズムによっては、「読み取り場所のセット」または「書き込み場所のセット」が低エントロピーになる可能性があるからです。 結び目を切ることができる情報理論の明白な原理があると思う。 誰かが提供することを期待して、このコミュニティwikiをマークします。


1


不可能に思えます。

ただし、10,000,000個の64ビットポインターは約76MBにすぎません。


1


線形フィードバックシフトレジスタは、ほとんど何でもできます。ある制限までの数のリストを(合理的に)ランダムな順序で生成します。 生成されるパターンは、ランダム性を試すことから期待されるものと統計的に類似していますが、暗号的に安全ではありません。 Berlekamp-Masseyアルゴリズムを使用すると、出力シーケンスに基づいて同等のLFSRをリバースエンジニアリングできます。

〜10,000,000個のアイテムのリストの要件を考えると、24ビットの最大長LFSRが必要であり、リストのサイズよりも大きい出力を単純に破棄します。

LFSRは、同じ期間の典型的な線形合同PRNGと比較して、非常に高速です。 ハードウェアでは、LFSRは非常に単純で、NビットレジスタとM個の2入力XORで構成されます(Mはタップの数で、場合によっては数個、まれに半ダース以上)。


0


十分なスペースがある場合、ノードのポインターを配列に格納し、ビットマップを作成して、次に選択されたアイテムを指すランダムな整数を取得できます。 既に選択されている場合(ビットマップに保存する場合)、アイテムがなくなるまで最も近いもの(左または右、ランダム化できます)を取得します。

十分なスペースがない場合は、ノードのポインターを保存せずに同じことを行うことができますが、時間がかかります(これは時間と空間のトレードオフです☺)。


0


ブロック暗号を使用して、擬似乱数の「安全な」置換を作成できます - see http://blog.notdot.net/2007/9/Damn-Cool-Algorithms-Part-2-Secure-permutations-with-block-ciphers [こちら]。 それらの重要な洞察は、nビット長のブロック暗号が与えられた場合、「折り畳み」を使用してm <nビットに短縮し、その後、すでに述べたトリックantti.huimaは、膨大な量を費やさずにそれからより小さな順列を生成することができます範囲外の値を破棄する時間。


0


本質的に必要なのは、それぞれ0回から1回だけ番号0..n-1を生成する乱数ジェネレータです。

これは中途半端なアイデアです:nよりわずかに大きい素数pを選択し、次に乗法群mod pの順序がp-1である1からp-1の間のランダムなxを選択することにより、かなりうまくいくでしょうi <p-1でx ^ i!= 1を満たすものをテストします。1つを見つける前にいくつかをテストするだけです。 xはグループを生成するため、1 ⇐ i ⇐ p-2のx ^ i mod pを計算するだけで、2〜p-1のp-2個の異なるランダムな(ish)数値が得られます。 2を減算し、1以上を投げます。これにより、印刷するインデックスのシーケンスが得られます。

これはそれほどランダムではありませんが、同じ手法を複数回使用して、上記のインデックス(+1)を取得し、別の素数p2を法とする別のジェネレータx2の指数として使用できます(n <p2 <pが必要です) 、 等々。 何十回も繰​​り返すと、物事はかなりランダムになるはずです。