すのものの「風変わりなソートアルゴリズム(15 パズルに関連)」

これは,バブルソートに似たアルゴリズムである。 しかし,一つの余分な操作を加えなければ,ソートアルゴリズムにならない。 バブルソートよりも効率はよくない。 しかし,15 パズルを解くための,やや実践的なアルゴリズムを与える。

int 型の配列 a[0], a[1], ..., a[N-1] を非・減少順にソートする例で示す。

アルゴリズム: もしも i ≥ 2 であり "a[i-2] > a[i] または a[i-1] > a[i]" が成り立つ i があれば, a[i] に二つの元 a[i-2]a[i-1] を飛び越させる。

この操作は次のコードで実現できる。

    if (a[i-2] > a[i] || a[i-1] > a[i]) {
        int tmp = a[i];

        a[i] = a[i-1], a[i-1] = a[i-2], a[i-2] = tmp;
    }

よく知られているように,配列 a[ ]転倒数 inv は次のコードで計算される。

    int inv = 0;

    for (j = 1; j < N; j++) {
        for (i = 0; i < j; i++) {
            if (a[i] > a[j]) {
                inv++;
            }
        }
    }

転倒数は次のコードでも計算できる。

    int inv2 = 0;

    for (j = 1; j < N; j++) {
        n = 0;
        for (i = 0; i < j; i++) {
            if (a[i] > a[j]) {
                n++;
            }
        }
        inv2 += n;
    }

上の操作は転倒数を増加させない。 もしも a[ ] が 0, 1, 2, ..., N-1 の並び替えならば, 上の操作は転倒数 modulo 2 を変えない。 よって,ソートアルゴリズムではない。

定義: 配列 a[ ]三つ組転倒数 trinv は次のコードで計算される。

    int trinv = 0;

    for (k = 2; k < N; k++) {
        for (j = 1; j < k; j++) {
            for (i = 0; i < j; i++) {
                if (a[i] <= a[k] && a[j] > a[k] || a[i] > a[k] && a[j] <= a[k]) {
                    trinv++;
                }
            }
        }
    }

三つ組転倒数は次のコードでも計算できる。

    int trinv = 0;

    for (k = 2; k < N; k++) {
        int less_equal = 0;
        int greater = 0;

        for (j = 0; j < k; j++) {
            if (a[j] <= a[k]) {
                less_equal++;
            } else {
                greater++;
            }
        }
        trinv += less_equal * greater;
    }

以上をテストするサンプルプログラムである。 配列は rand() 関数で初期化される。 種は現在時刻である。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define N 32

int a[N];

int main() {
    int i, j, k, n;
    int inv, inv2, trinv, trinv2;

    srand((unsigned)time(NULL));
    for (i = 0; i < N; i++) {
        a[i] = rand() % 16;
    }

    for (i = 0; i < N; i++) {
        printf("%x", a[i]);
    }
    putchar('\n');

    inv = 0;
    for (j = 1; j < N; j++) {
        for (i = 0; i < j; i++) {
            if (a[i] > a[j]) {
                inv++;
            }
        }
    }
    printf("%d ", inv);

    inv2 = 0;
    for (j = 1; j < N; j++) {
        n = 0;
        for (i = 0; i < j; i++) {
            if (a[i] > a[j]) {
                n++;
            }
        }
        inv2 += n;
    }
    printf("%d ", inv2);

    trinv = 0;
    for (k = 2; k < N; k++) {
        for (j = 1; j < k; j++) {
            for (i = 0; i < j; i++) {
                if (a[i] <= a[k] && a[j] > a[k] || a[i] > a[k] && a[j] <= a[k]) {
                    trinv++;
                }
            }
        }
    }
    printf("%d ", trinv);

    trinv2 = 0;
    for (k = 2; k < N; k++) {
        int less_equal = 0;
        int greater = 0;

        for (j = 0; j < k; j++) {
            if (a[j] <= a[k]) {
                less_equal++;
            } else {
                greater++;
            }
        }
        trinv2 += less_equal * greater;
    }
    printf("%d ", trinv2);
    putchar('\n');
}

命題: もしも上の操作が転倒数を減らさないなら, 三つ組転倒数が減る。

証明: 二つの場合がある。 (i) a[i-2] < a[i] && a[i-1] > a[i] と (ii) a[i-2] > a[i] && a[i-1] < a[i] である。

ケース (i). 次のように置く。

三つ組転倒数の第二の定義を思い出そう。 三つの元 a[i-2], a[i-1], a[i] の三つ組転倒数への寄与は次のようになる:

操作後は次のようになる:

「操作後」-「操作前」は - Sle + Mle + Mgt + 1 - Lgt になる。 Mle + Mgtj-3 に等しく Sle + Lgtj-3 以下である。 ゆえに,これは正である。

ケース (ii). 次のように置く。

三つの元 a[i-2], a[i-1], a[i] の三つ組転倒数への寄与は次のようになる:

操作後は次のようになる:

あとはケース (i) と同じである。 [証明終]

ゆえに,上の操作は転倒数か三つ組転倒数を減少させる。 よって,上の操作は永遠には続かない。 もしも転倒数が正なら, 上の操作をおこなうことができる。 ただし,転倒数が 1 であり a[0] > a[1] の場合を除く。

次は,このソートのサンプルプログラムである。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define N 64

int a[N];

int main() {
    int i, j, k, n;
    int inv, trinv;

    srand((unsigned)time(NULL));
    for (i = 0; i < N; i++) {
        a[i] = rand() % 16;
    }

    while (1) {
        inv = 0;
        for (j = 1; j < N; j++) {
            for (i = 0; i < j; i++) {
                if (a[i] > a[j]) {
                    inv++;
                }
            }
        }

        trinv = 0;
        for (k = 2; k < N; k++) {
            for (j = 1; j < k; j++) {
                for (i = 0; i < j; i++) {
                    if (a[i] <= a[k] && a[j] > a[k] || a[i] > a[k] && a[j] <= a[k]) {
                        trinv++;
                    }
                }
            }
        }

        printf("inv. %5d: 3inv. %5d: ", inv, trinv);
        for (i = 0; i < N; i++) {
            printf("%x", a[i]);
        }
        putchar('\n');

        for (i = N - 1; i >= 2; i--) {
            if (a[i-2] > a[i] || a[i-1] > a[i]) {
                int tmp = a[i];

                a[i] = a[i-1], a[i-1] = a[i-2], a[i-2] = tmp; break;  /* for */
            }
        }
        if (i == 1) {
            break;      /* while */
        }
    }
}

15 パズルとの関連については, 「すのものの「15 パズル(およびその拡張)を解く」」を見られたい。


すのもの