洗牌算法

54張有序的撲克牌,設計一種算法,實現洗牌操作:
方法一:
1。隨機產生一個1-n的數x,做爲第一張牌。
2。隨機產生一個1-(n-1)的數y,如果y<x,則將y作爲第二張牌,否則將y+1作爲第二張牌。
3。隨機產生一個1-(n-i)的數z,取第z個沒有被抽出來的作爲第i張牌。(i=3,4,5...54)
這種算法的複雜度爲O(N^2),因爲計算每個隨機數的牌號平均要執行(N/2)次比較。
對應於現實中的撲克牌,這種算法等於每次從牌堆中隨機抽一張,放到另一堆上,直到抽完爲止,這裏新的一堆就是洗完的牌序。

方法二:
1。隨機產生一個1-n的數x,然後讓第x張牌和第1張牌互相調換。
2。隨機產生一個1-n的數y,然後讓第y張牌和第2張牌互相調換。
3。隨機產生一個1-n的數z,然後讓第z張牌和第i張牌互相調換。(i=3,4,5...54)
這種算法的複雜度爲O(N)。

方法三:
因爲一共有N!種洗牌結果,所以可以等概率地產生一個1-N!之間的隨機數x,然後用康託展開的方法,根據x生成對應的排列,即爲洗牌結果。
關於康託展開,參考http://baike.baidu.com/view/437641.htm

方法一的改進:
方法一由於計算隨機數對應的牌號平均要執行(N/2)次比較,所以複雜度爲O(N^2),N*N的第一個N是牌的數目,無法優化,但是第二個N是確定牌的序號,這個是否有辦法優化呢?
第一種優化就是每次抽出一張牌後,就將之後的所有牌向前移動一個單位,下次產生的隨機數就是目標牌的下標。
如第一次隨機數爲3,抽出3,撲克牌數組變爲1 2 4 5 6 7...
第二次抽出6,直接取數組的第六個元素即7,然後把7之後的牌前移一個單位。這樣牌的定位複雜度就是1了,但是移動的複雜度仍爲N/2,所以這種算法並沒有起到效率優化的目標。
第二種優化是爲樹狀數組。用一個長爲54的樹狀數組 used 存儲 1-i 張牌中被投抽出來的牌的張數。這樣 i-used[i] 就是牌號爲 i 的撲克的序號。然後用2分查找的方式,就可以快速確定隨機數對應的牌號,樹狀樹的複雜度爲logn,這樣總的算法複雜度爲O(NlogN)。
但是考慮到一種54張牌,樹狀數組優化帶來的性能提升很小,比起程序代碼帶來的可維護性損失,頗不值得。

方法二的修正:
方法二乍看好像不錯,網上也有很多文章使用這種算法,但是其實這是一種錯誤的方法,因爲方法二的所有可能性爲N^N,而洗好的牌一種有N!種可能,又因爲N^N % N! !=0,所以每種結果的概率是不相同的。
那麼如何修正這個問題呢?仿照第一種方法,第i次洗牌不是產生一個1-n的隨機數,而是產生一個i-n的隨機數,這樣可能性結果的可能性就是N!了。就有可能概率相等了。
證明在<<計算機程序設計藝術>>上。

方法一代碼:
#include <stdio.h>
#include <stdlib.h>
#define N 54

int poker[N+1];
void shuffle(){
    int used[N+1];
    int i,j,k;
    for(i=1;i<=N;i++)
        used[i]=0;
    for(i=1;i<=N;i++){//定位
        k = rand()%(N+1-i)+1;
        for(j=1;k!=0;j++){
            if(0==used[j])//只計算未抽出的牌
                k--;
        }
        poker[i]=j-1;
        used[j-1]=1;
    }
}
int main(){
    int i;
    srand(time(NULL));
    shuffle();
    for(i=1;i<=N;i++)
        printf("%d ", poker[i]);
    printf("\n");
}
修正後的方法二代碼:
#include <stdio.h>
#include <stdlib.h>
#define swap(a,b) {int t=(a);(a)=(b);(b)=(t);}
#define N 54

int poker[N+1];
void shuffle(){
    int i,k;
    for(i=1;i<=N;i++)//初始化
        poker[i]=i;
    for(i=1;i<=N;i++){
        k = rand()%(N+1-i)+i;
        swap(poker[i],poker[k]);   //交換
    }  
}
int main(){
    int i;
    srand(time(NULL));
    shuffle();
    for(i=1;i<=N;i++)
        printf("%d ", poker[i]);
    printf("\n");
}

康託展開源代碼(計算排列的編號):洗牌算法要反過來,即根據排列的編號計算排列的內容。
#include <stdio.h>

int fac[] = {1,1,2,6,24,120,720,5040,40320,362880};
unsigned long cantor(int s[], int n) {
    int i,j,temp,num=0;
    for(i=1;i<n;i++){
        temp = 0;
        for(j=i+1;j<=n;j++){
            if(s[j]<s[i])
                temp++;
        }  
        num+=fac[n-i]*temp;
    }  
    return num+1;
}

int main(){
    int x[]={0,1,2,3,5,4};
    printf("%ld\n", cantor(x,5));
    int y[]={0,5,4,3,2,1};
    printf("%ld\n", cantor(y,5));

}


原文鏈接,感謝原作者

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章