配列をインターリーブするためのインプレースアルゴリズム


62

要素の配列が与えられます2n

a1,a2,,an,b1,b2,bn

タスクは、結果の配列が次のようになるようにインプレースアルゴリズムを使用して配列をインターリーブすることです

b1,a1,b2,a2,,bn,an

インプレース要件がなければ、新しい配列を簡単に作成し、要素をコピーして時間アルゴリズムを提供できます。O(n)

インプレース要件では、分割統治アルゴリズムはアルゴリズムを上げ。θ(nlogn)

質問は次のとおりです。

あるその場でもあり、時間アルゴリズムは、?O(n)

(注:均一コストのWORD RAMモデルを想定できるため、インプレースはスペース制限に変換されます)。O(1)


1
これはstackoverflowにあります が、高品質のソリューションを提供しません。トップクラスの答えは次のとおりです。「この問題は、のように簡単ではありません人々があることを作るよう宿題がLOLあります。?。arXivのオンソリューション」しかし、arXivのソリューションは、他の論文では、いくつかの数論+参照証明が必要です。ここに簡潔な解決策があると便利です。
ジョー


スタックオーバーフロー上の別のスレッド:stackoverflow.com/questions/15996288/...
Nayuki

回答:


43

以下は、Joeによってリンクされた論文のアルゴリズムを詳しく説明した答えです。http//arxiv.org/abs/0805.1598

まず、分割統治を使用するアルゴリズムを考えてみましょう。Θ(nlogn)

1)分割統治

与えられます

a1,a2,,b1,b2,bn

分割と征服を使用するために、いくつかのについて、配列を取得しようとしますm=Θ(n)

[a1,a2,,am,b1,b2,,bm],[am+1,,an,bm+1,bn]

そして再帰。

一部のことに注意してくださいの巡回シフトであります

b1,b2,bm,am+1,an

am+1,an,b1,bm

よる場所。m

これは古典的なもので、3回の反転によってインプレースで、時間で実行できます。O(n)

したがって、分割統治により、似た再帰を伴うアルゴリズムが得られます。Θ(nlogn)T(n)=2T(n/2)+Θ(n)

2)順列サイクル

さて、この問題に対する別のアプローチは、順列を互いに素なサイクルのセットとして考えることです。

順列は(から始まると仮定)によって与えられます1

j2jmod2n+1

一定の余分なスペースを使用してサイクルが何であるかを正確に知っていれば、要素選択して置換を実現でき、その要素がどこに行くかを決定し(上記の式を使用して)、ターゲットの場所にある要素を一時スペースに入れ、置く要素をそのターゲット位置に配置し、サイクルに沿って続行します。1つのサイクルが完了したら、次のサイクルの要素に移動し、そのサイクルをたどります。AA

これにより、時間アルゴリズムが得られますが、「正確なサイクルが何であるかを何とか知っていた」と仮定し、スペース制限内でこのブックキーピングを実行しようとしますこの問題を難しくしているのはそのためです。O(n)O(1)

これは、論文が数論を使用する場所です。

とき、それは場合に、それを示すことができる、位置における要素、、異なるサイクルであり、サイクルごとに要素が含まれ位置。2n+1=3k13,32,,3k13m,m0

これは、がジェネレーターであるという事実を使用しています。2(Z/3k)

したがって、場合、フォローサイクルアプローチは時間アルゴリズムを提供します。各サイクルについて、どこから開始するかを正確に知っています:べき乗(を含む)空間で計算できます)。2n+1=3kO(n)31O(1)

3)最終アルゴリズム

ここで、上記の2つを組み合わせます:分割と征服+順列サイクル。

我々は、分割を行うと征服が、ピックするようにの電源である及び。m2m+13m=Θ(n)

そのため、代わりに両方の「半分」で再帰的に、一方だけで再帰し、余分な作業を行います。Θ(n)

これにより、繰り返し(いくつかの)が得られるため、時間、空間アルゴリズム!T(n)=T(cn)+Θ(n)0<c<1O(n)O(1)


4
それは美しいです。
ラファエル

1
非常に素晴らしい。順列の例を見てみると、今ではそのほとんどが理解できます。2つの質問:1.どのようにして実際に値mを見つけますか?紙はO(log n)が必要だと主張していますが、それはなぜですか?2.同様のアプローチを使用して配列をDEインターリーブすることは可能ですか?
num3ric

2
@ num3ric:1)あなたは最高のパワー見つけるされ。したがって、ます。2)。はい、それは可能です、私はどこかでstackoverflowに関する答えを追加したと思います。その場合のサイクルリーダーは、( =累乗)であると思います。3<nO(logn)2a3b2m+13
アルヤバタ

@Aryabhataなぜ2つの「半分」ではなく、「半分」だけを再帰するのですか?
sinoTrinity

1
@Aryabhataこのアルゴリズムを拡張して、3つ以上の配列をインターリーブできますか?たとえば、をまたは類似のもの。a1,a2,,an,b1,b2,,bn,c1,c2,,cnc1,b1,a1,c2,b2,a2,,cn,bn,an
ダウ

18

数論やサイクル理論に依存しないアルゴリズムを見つけたと確信しています。解決すべき詳細がいくつかあることに注意してください(おそらく明日)が、うまくいくと確信しています。私は問題を隠そうとしているからではなく、寝ていることになっているので私は手を振っている:)

Let Aは最初の配列、B2番目の配列とし、簡単に|A| = |B| = NするN=2^kためにsome を仮定しkます。みましょうA[i..j]の部分配列であることAの指標とiに至るまでj包括的。配列は0ベースです。ましょRightmostBitPos(i)の「1」の右端のビットの(0系)の位置を返すi右から数えを、。アルゴリズムは次のように機能します。

GetIndex(i) {
    int rightPos = RightmostBitPos(i) + 1;
    return i >> rightPos;
}

Interleave(A, B, N) {
    if (n == 1) {
        swap(a[0], b[0]);
    }
    else {
        for (i = 0; i < N; i++)
            swap(A[i], B[GetIndex(i+1)]);

        for (i = 1; i <= N/2; i*=2)
            Interleave(B[0..i/2-1], B[i/2..i-1], i/2);

        Interleave(B[0..N/2], B[N/2+1..N], n/2);
    }
}

16個の数字の配列を取り、スワップを使用してインターリーブを開始し、何が起こるかを見てみましょう。

1 2 3 4 5 6 7 8    | 9 10 11 12 13 14 15 16
9 2 3 4 5 6 7 8    | 1 10 11 12 13 14 15 16
9 1 3 4 5 6 7 8    | 2 10 11 12 13 14 15 16
9 1 10 4 5 6 7 8   | 2 3 11 12 13 14 15 16
9 1 10 2 5 6 7 8   | 4 3 11 12 13 14 15 16
9 1 10 2 11 6 7 8  | 4 3 5 12 13 14 15 16
9 1 10 2 11 3 7 8  | 4 6 5 12 13 14 15 16
9 1 10 2 11 3 12 8 | 4 6 5 7 13 14 15 16
9 1 10 2 11 3 12 4 | 8 6 5 7 13 14 15 16

特に興味深いのは、2番目の配列の最初の部分です。

|
| 1
| 2
| 2 3
| 4 3
| 4 3 5
| 4 6 5
| 4 6 5 7
| 8 6 5 7

パターンは明確である必要があります。最後に数字を追加し、最小の数字を高い数字に置き換えます。既にある最大数よりも1つ大きい数を常に追加することに注意してください。いつの時点でどの番号が最も低いかを正確に把握できた場合、それは簡単に行えます。

次に、パターンを見ることができるかどうかを確認するために、より大きな例に進みます。上記の例を作成するために配列のサイズを修正する必要はないことに注意してください。ある時点で、この構成を取得します(2行目はすべての数値から16を引きます)。

16 24 20 28 18 22 26 30 17 19 21 23 25 27 29 31
0   8  4 12  2  6 10 14  1  3  5  7  9 11 13 15

これは、「1 3 5 7 9 11 13 15」がすべて2離れ、「2 6 10 14」がすべて4離れ、「4 12」が8離れているパターンを明確に示しています。そのため、次に小さい数字が何であるかを示すアルゴリズムを考案できます。このメカニズムは、2進数の動作とほぼ同じです。配列の最後の半分に少し、第2四半期に少し、というようになります。

したがって、これらのビットを格納するのに十分なスペースが許可されている場合(ビットが必要ですが、計算モデルではこれが可能です-配列へのポインタにもビットが必要です)、償却された時間。lognlognO(1)

したがって、時間とスワップで配列の前半をインターリーブ状態にすることができます。ただし、配列の後半部分を修正する必要がありますが、これはすべて混乱しているように見えます( "8 6 5 7 13 14 15 16")。O(n)O(n)

ここで、この2番目の部分の前半を「ソート」できる場合、「5 6 7 8 13 14 15 16」になります。この半分を再帰的にインターリーブすると、トリックが実行されます。配列をインターリーブします。time(再帰呼び出しは、それぞれが入力サイズを半分にします)。これらの呼び出しは末尾再帰であるため、スタックを必要としないことに注意してください。そのため、スペース使用量はままです。O(n)O(logn)O(1)

さて、質問は次のとおりです。ソートする必要のある部分に何らかのパターンがありますか?32個の数字を試してみると、「16 12 10 14 9 11 13 15」になります。ここにはまったく同じパターンがあることに注意してください!「9 11 13 15」、「10 14」、および「12」は、先ほど見たのと同じ方法でグループ化されます。

さて、トリックはこれらのサブパートを再帰的にインターリーブすることです。「16」と「12」を「12 16」にインターリーブします。「12 16」と「10 14」を「10 12 14 16」にインターリーブします。「10 12 14 16」と「9 11 13 15」を「9 10 11 12 13 14 15 16」にインターリーブします。これにより、最初の部分がソートされます。

上記と同様に、この操作の総コストはです。これらすべてを合計しても、合計実行時間はます。O(n)O(n)

例:

Interleave the first half:
1 2 3 4 5 6 7 8    | 9 10 11 12 13 14 15 16
9 2 3 4 5 6 7 8    | 1 10 11 12 13 14 15 16
9 1 3 4 5 6 7 8    | 2 10 11 12 13 14 15 16
9 1 10 4 5 6 7 8   | 2 3 11 12 13 14 15 16
9 1 10 2 5 6 7 8   | 4 3 11 12 13 14 15 16
9 1 10 2 11 6 7 8  | 4 3 5 12 13 14 15 16
9 1 10 2 11 3 7 8  | 4 6 5 12 13 14 15 16
9 1 10 2 11 3 12 8 | 4 6 5 7 13 14 15 16
9 1 10 2 11 3 12 4 | 8 6 5 7 13 14 15 16
Sort out the first part of the second array (recursion not explicit):
8 6 5 7 13 14 15 16
6 8 5 7 13 14 15 16
5 8 6 7 13 14 15 16
5 6 8 7 13 14 15 16
5 6 7 8 13 14 15 16
Interleave again:
5 6 7 8   | 13 14 15 16
13 6 7 8  | 5 14 15 16
13 5 7 8  | 6 14 15 16
13 5 14 8 | 6 7 15 16
13 5 14 6 | 8 7 15 16
Sort out the first part of the second array:
8 7 15 16
7 8 15 16
Interleave again:
7 8 | 15 16
15 8 | 7 16
15 7 | 8 16
Interleave again:
8 16
16 8
Merge all the above:
9 1 10 2 11 3 12 4 | 13 5 14 6 | 15 7 | 16 8

面白い。正式な証明を試してみてください。ビットを処理する別のアルゴリズム(Joeが見つけた論文で言及されている)があることは知っています。おそらくあなたはそれを再発見したでしょう!
アルヤバタ

1

これは、余分なストレージなしで配列の2つの半分をインターリーブする線形時間アルゴリズムの非再帰的なインプレースです。

一般的な考え方は単純です。配列の前半分を左から右に見て、正しい値を所定の位置に入れ替えます。進むにつれて、まだ使用されていない左の値が、右の値によって空けられたスペースにスワップされます。唯一のトリックは、それらを再び引き出す方法を考え出すことです。

まず、サイズNの配列を2つのほぼ等しい半分に分割します。
[ left_items | right_items ]
処理するにつれて、
[ placed_items | remaining_left_items| swapped_left_items | remaining_right_items]

スワップスペースは、次のパターンで拡大します。A)隣接する右側のアイテムを削除し、左側から新しいアイテムを交換することでスペースを拡大します。B)最も古いアイテムを左から新しいアイテムと交換します。左の項目に1..Nの番号が付けられている場合、このパターンは次のようになります

step swapspace index changed
1    A: 1         0
2    B: 2         0
3    A: 2 3       1
4    B: 4 3       0     
5    A: 4 3 5     2
6    B: 4 6 5     1
7    A: 4 6 5 7   3
...

インデックスが変更されたシーケンスは正確にOEIS A025480であり、簡単なプロセスで計算できます。これにより、これまでに追加されたアイテムの数のみを指定してスワップ場所を見つけることができます。これは、配置される現在のアイテムのインデックスでもあります。

これが、シーケンスの前半を線形時間で取り込むために必要なすべての情報です。

中間点に到達すると、配列には3つの部分 [ placed_items | swapped_left_items | remaining_right_items] があります。交換されたアイテムのスクランブルを解除できる場合、問題を半分のサイズに減らして、繰り返すことができます。

スワップ領域のスクランブルを解除するには、次のプロパティを使用しますN。append操作とswap_oldest操作を交互に作成するシーケンスにN/2は、年齢がで指定されるアイテムが含まれA025480(N/2)..A025480(N-1)ます。 (整数除算、より小さい値は古い)。

たとえば、左半分に元々値1..19が保持されていた場合、スワップ領域にはが含まれます[16, 12, 10, 14, 18, 11, 13, 15, 17, 19]。A025480(9..18)はです[2, 5, 1, 6, 3, 7, 0, 8, 4, 9]。これは、最も古いアイテムから最も新しいアイテムのインデックスのリストです。

だから我々はそれを進めると交換することにより、当社のスワップ領域のスクランブルを解除することができますS[i]S[ A(N/2 + i)]。これも線形時間です。

残りの問題は、最終的には正しい値がより低いインデックスにあるはずの位置に到達するが、既にスワップアウトされているということです。新しい場所を見つけるのは簡単です。もう一度インデックスの計算を行うだけで、アイテムの交換先を見つけることができます。スワップされていない場所が見つかるまで、チェーンを数ステップ実行する必要がある場合があります。

この時点で、配列の半分をマージし、残りの半分のマージされていない部分の順序を正確にN/2 + N/4スワップして維持しました。N + N/4 + N/8 + ....厳密に未満のスワップの 合計に対して、配列の残りの部分を続行でき3N/2ます。

A025480を計算する方法:
これはOEISで定義されているa(2n) = n, a(2n+1) = a(n).代替製剤がありますa(n) = isEven(n)? n/2 : a((n-1)/2)。これにより、ビット演算を使用した単純なアルゴリズムが実現します。

index_t a025480(index_t n){
    while (n&1) n=n>>1;
    return n>>1;  
}

これは、Nのすべての可能な値に対する償却O(1)操作です(1/2は1シフト、1/4は2、1 / 8は3、...を必要とします)。小さいルックアップテーブルを使用して最下位のゼロビットの位置を見つける、さらに高速な方法があります。

それを踏まえて、Cでの実装を次に示します。

static inline index_t larger_half(index_t sz) {return sz - (sz / 2); }
static inline bool is_even(index_t i) { return ((i & 1) ^ 1); }

index_t unshuffle_item(index_t j, index_t sz)
{
  index_t i = j;
  do {
    i = a025480(sz / 2 + i);
  }
  while (i < j);
  return i;
}

void interleave(value_t a[], index_t n_items)
{
  index_t i = 0;
  index_t midpt = larger_half(n_items);
  while (i < n_items - 1) {

    //for out-shuffle, the left item is at an even index
    if (is_even(i)) { i++; }
    index_t base = i;

    //emplace left half.
    for (; i < midpt; i++) {
      index_t j = a025480(i - base);
      SWAP(a + i, a + midpt + j);
    }

    //unscramble swapped items
    index_t swap_ct  = larger_half(i - base);
    for (index_t j = 0; j + 1 < swap_ct ; j++) {
      index_t k = unshuffle_item(j, i - base);
      if (j != k) {
        SWAP(a + midpt + j, a + midpt + k);
      }
    }
    midpt += swap_ct;
  }
}

3つのデータ位置のうち2つが連続してアクセスされ、処理されるデータ量が厳密に減少しているため、これはかなりキャッシュに優しいアルゴリズムでなければなりません。このメソッドは、ループの開始時にテストを無効にすることで、アウトシャッフルからインシャッフルに切り替えることができis_evenます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.