一種變進制數及其應用(全排列之Hash實現)

我們經常使用的數的進製爲“常數進制”,即始終逢p進1。例如,p進制數K可表示爲

K = a0*p^0 + a1*p^1 + a2*p^2 + ... + an*p^n (其中0 <= ai <= p-1),

它可以表示任何一個自然數。

對於這種常數進製表示法,以及各種進制之間的轉換大家應該是很熟悉的了,但大家可能很少聽說變進制數。這裏我要介紹一種特殊的變進制數,它能夠被用來實現全排列的Hash函數,並且該Hash函數能夠實現完美的防碰撞和空間利用(不會發生碰撞,且所有空間被完全使用,不多不少)。這種全排列Hash函數也被稱爲全排列數化技術。下面,我們就來看看這種變進制數。
我們考查這樣一種變進制數:第1位逢2進1,第2位逢3進1,……,第n位逢n+1進1。它的表示形式爲
 K = a1*1! + a2*2! + a3*3! + ... + an*n! (其中0 <= ai <= i),
也可以擴展爲如下形式(因爲按定義a0始終爲0),以與p進製表示相對應:
 K = a0*0! + a1*1! + a2*2! + a3*3! + ... + an*n! (其中0 <= ai <= i)。
(後面的變進制數均指這種變進制數,且採用前一種表示法)
先讓我們來考查一下該變進制數的進位是否正確。假設變進制數K的第i位ai爲i+1,需要進位,而ai*i!=(i+1)*i!=1*(i+1)!,即正確的向高位進1。這說明該變進制數能夠正確進位,從而是一種合法的計數方式。
接下來我們考查n位變進制數K的性質:
(1)當所有位ai均爲i時,此時K有最大值
    MAX[K] = 1*1! + 2*2! + 3*3! + ... + n*n!
           = 1! + 1*1! + 2*2! + 3*3! + ... + n*n! - 1
           = (1+1)*1! + 2*2! + 3*3! + ... + n*n! - 1
           = 2! + 2*2! + 3*3! + ... + n*n! - 1
           = ...
           = (n+1)!-1
    因此,n位K進制數的最大值爲(n+1)!-1。
(2)當所有位ai均爲0時,此時K有最小值0。
因此,n位變進制數能夠表示0到(n+1)!-1的範圍內的所有自然數,共(n+1)!個。

在一些狀態空間搜索算法中,我們需要快速判斷某個狀態是否已經出現,此時常常使用Hash函數來實現。其中,有一類特殊的狀態空間,它們是由全排列產生的,比如N數碼問題。對於n個元素的全排列,共產生n!個不同的排列或狀態。下面將討論如何使用這裏的變進制數來實現一個針對全排列的Hash函數。

從數的角度來看,全排列和變進制數都用到了階乘。如果我們能夠用0到n!-1這n!個連續的變進制數來表示n個元素的所有排列,那麼就能夠把全排列完全地數化,建立起全排列和自然數之間一一對應的關係,也就實現了一個完美的Hash函數。那麼,我們的想法能否實現呢?答案是肯定的,下面將進行討論。

假設我們有b0,b1,b2,b3,...,bn共n+1個不同的元素,並假設各元素之間有一種次序關係 b0<b1<b2<...<bn。對它們進行全排列,共產生(n+1)!種不同的排列。對於產生的任一排列 c0,c1,c2,..,cn,其中第i個元素ci(1 <= i <= n)與它前面的i個元素構成的逆序對的個數爲di(0 <= di <= i),那麼我們得到一個逆序數序列d1,d2,...,dn(0 <= di <= i)。這不就是前面的n位變進制數的各個位麼?於是,我們用n位變進制數M來表示該排列:
   M = d1*1! + d2*2! + ... + dn*n!
因此,每個排列都可以按這種方式表示成一個n位變進制數。下面,我們來考查n位變進制數能否與n+1個元素的全排列建立起一一對應的關係。

由於n位變進制數能表示(n+1)!個不同的數,而n+1個元素的全排列剛好有(n+1)!個不同的排列,且每一個排列都已經能表示成一個n位變進制數。如果我們能夠證明任意兩個不同的排列產生兩個不同的變進制數,那麼我們就可以得出結論:
★ 定理1 n+1個元素的全排列的每一個排列對應着一個不同的n位變進制數。

對於全排列的任意兩個不同的排列p0,p1,p2,...,pn(排列P)和q0,q1,q2,...,qn(排列Q),從後往前查找第一個不相同的元素,分別記爲pi和qi(0 < i <= n)。
(1)如果qi > pi,那麼,
如果在排列Q中qi之前的元素x與qi構成逆序對,即有x > qi,則在排列P中pi之前也有相同元素x > pi(因爲x > qi且qi > pi),即在排列P中pi之前的元素x也與pi構成逆序對,所以pi的逆序數大於等於qi的逆序數。又qi與pi在排列P中構成pi的逆序對,所以pi的逆序數大於qi的逆序數。
(2)同理,如果pi > qi,那麼qi的逆序數大於pi的逆序數。
因此,由(1)和(2)知,排列P和排列Q對應的變進制數至少有第i位不相同,即全排列的任意兩個不同的排列具有不同的變進制數。至此,定理1得證。

計算n個元素的一個排列的變進制數的算法大致如下(時間複雜度爲O(n^2)):
template <typename T>
size_t PermutationToNumber(const T permutation[], int n)
{
    // n不能太大,否則會溢出(如果size_t爲32位,則n <= 12)
    size_t result = 0;
    for (int j = 1; j < n; ++j) {
        int count = 0;
        for (int k = 0; k < j; ++k) {
            if (permutation[k] > permutation[j])
                ++count;
        }
        // factorials[j]保存着j!
        result += count * factorials[j];
    }

    return result;
}

說明:
(1)由於n!是一個很大的數,因此一般只能用於較小的n。
(2)有了計算排列的變進制數的算法,我們就可以使用一個大小爲n!的數組來保存每一個排列的狀態,使用排列的變進制數作爲數組下標,從而實現狀態的快速檢索。如果只是標記狀態是否出現,則可以用一位來標記狀態。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章