これは,バブルソートに似たアルゴリズムである。 しかし,一つの余分な操作を加えなければ,ソートアルゴリズムにならない。 バブルソートよりも効率はよくない。 しかし,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 + Mgt は j-3 に等しく Sle + Lgt は j-3 以下である。 ゆえに,これは正である。
ケース (ii). 次のように置く。
操作後は次のようになる:
あとはケース (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 パズル(およびその拡張)を解く」」を見られたい。