ホームページへ戻る 開発ノートメニューへ戻る 11パズルの最長手数解へ戻る

最小完全ハッシュ関数の作り方


■ 順列型の最小完全ハッシュ関数

 0から4までの5個の数字が下のように並んでいる場合を例にして説明します。
 5個の数字の並べ方は5!通りありますので5!(=120)通りの並べ方の総てに対して0から119までの数値を一意に割り付けることが目的となります。

  34102

 ここでは左側から順に数字を見ていくことにします。最初の数字は3で残りの数字の個数は4個ですね。
 この残れさた数字の個数分の総順列数は4!ですが、この数量を基数と言います。
 つまり左端の数字が何であるかを完全に識別する為に最低限必要な基本となる重みのことです。
 従って先ず最初の数字3に基数である4!を掛け算してはじき出します。

  []4102 → 3*4!

 次に左から2番目の数字ですが、ここから先はとても注意が必要です。
 2番目の数字は4で残りの数字の個数は3個です。残りの数字の個数が3個なので基数は3!になります。つまり基数が変化します。この基数の変換が無駄なハッシュ値領域を作らない最小完全ハッシュ値を得る為の重要なポイントです。
 そしてさらに重要なポイントは、先ほどの様にして4*3!をはじき出すのは間違いだということです。
 実際の数字そのものに基数の3!を掛けるのではなくて[4102]の4個の数字の中で4という数字が何番目に小さな数字かという順位に着目しなければならないのです。
 0位から数えて数字4の順位は3位であるという考え方をして3*3!をはじき出すのが正解なのです。
 そうしてやらないと現在以降のはじき出す数値が前回の基数であった4!に収まらなくなるのです。

  [34]102 → 3*4!+3*3!

 以下同様にして、

  [341]02 → 3*4!+3*3!+1*2!
  [3410] → 3*4!+3*3!+1*2!+0*1!
  [34102] → 3*4!+3*3!+1*2!+0*1!+0*0!

 この計算結果は92でこれが34102の最小完全ハッシュ値となります。

 では次に、この考え方に基づいた最小完全ハッシュ関数の具体的な作り方に移ります。
 階乗計算を毎回実行するのは無駄があるので、先ず階乗値テーブルを作っておきます。
/* 階乗値テーブル */
int FACTOR[5] = {
    24, 6, 2, 1, 1
};
 面倒なのは「何番目に小さな数字かという順位」を求める部分です。しかし、これにはとてもうまい方法が考案されています。
 「着目している数字よりも大きな数字が、まだ残っている数字群の中にあればその値を1つ減ずる」という処理をして行くと実際の数字がおのずと順位の値に変化してくれるというのです。

  []34102
  []3102 ←2番目の数字4は3より大きいので1つ減ずる。
  [33]102
  [331]01 ←5番目の数字2は1より大きいので1つ減ずる。
  [3310] ←5番目の数字1は0より大きいので1つ減ずる。
  [33100]

 あとは単純に3*4!+3*3!+1*2!+0*1!+0*0!=92の計算をすることになります。
 最後の項の値は必ずゼロになることを考慮してこのようなプログラムが書けます。
/* 順列型の最小完全ハッシュ関数 */
int ChangeNumber(int table[])
{
    int  i, j, hash = 0, work[SIZE-1];

    for (i=0; i < SIZE-1; i++)
        work[i] = table[i];
    for (i=0; i < SIZE-1; i++) {
        hash += work[i] * FACTOR[i];
        for (j=i+1; j < SIZE-1; j++)
            if (work[i] < work[j]) work[j]--;
    }
    return hash;
}
 参考文献:「M.Hiroi's Home Page」のPuzzle DE Programming(幅優先探索の高速化(1) -- 8パズルを解く --)
 尚、広井さんは 奥村晴彦「C言語による最新アルゴリズム事典」技術評論社 を参考文献として紹介しています。

■ 組合せ型の最小完全ハッシュ関数

 5個の中から2個を選ぶという設定で数字が下のように並んでいる場合を例にして説明します。
 5個の中から2個を選ぶ組合せは52通りありますので52(=10)通りの総ての選び方に対して0から9までの数値を一意に割り付けることが目的となります。

  10100

 先ほどと同様にして左側から順に数字を見ていくことにします。
 先ず最初の位置の基数を考えます。ここがゼロであったとすれば残りの組合せは42となります。
 つまり、これだけの数値の幅を確保しておく必要があるということなのでこれが最初の位置の基数値になります。
 そしてここが1なのでここで42をはじき出します。

  []0100 → 42

 2番目の数字は0なので考える必要はありません。3番目の数字は1なのでここでの基数を考えます。
 この位置で確保しておく必要がある数値の幅は21なのでその基数である21をはじき出します。

  [101]00 → 4221

 残りは総て0なのでここで計算は終りとなります。
 この計算結果は8でこれが10100の最小完全ハッシュ値となります。

 順列型よりも基数が動的に変化するのでちょっと理解しにくいかも知れませんが手続きは順列型よりずっと簡単です。
 プログラムもnrの値を格納したテーブルPASCAL[n][r]を用意しておくとこのようになります。
/* 組合せ型の最小完全ハッシュ関数 */
int ChangeNumber(int table[])
{
    int  i, n, r, hash = 0;

    n = SIZE - 1;
    r = 2;
    for (i=0; n >= r && r > 0; i++,n--)
        if (table[i]) hash += PASCAL[n][r--];
    retuen hash;
}
 ここでfor文の終了判定式ですが、n < r は残りが総て1で、r == 0 は残りが総て0である場合を意味します。
 つまり選択の余地が無くなった時点で計算は終了します。判りにくければこのfor文を、
    for (i=0; i < SIZE; i++,n--)
 としても同じ結果になります。
 ところで、nrの値を格納したテーブル名が何故PASCALなのかと言うとこのテーブルの中身はパスカルの三角形(2項定理の係数)そのものになっているからです。ちなみに、このテーブルは足し算だけで作ることが出来ます。
/* nCr値テーブル */
int PASCAL[5][3] = {
    1, 0, 0,
    1, 1, 0,
    1, 2, 1,
    1, 3, 3,
    1, 4, 6
};
 参考文献:「M.Hiroi's Home Page」のGuest Bookにおける鎌田さんの書込み。(火付け役はdeepgreenさん)
 鎌田さん、ありがとうございます。


ホームページへ戻る 開発ノートメニューへ戻る 11パズルの最長手数解へ戻る