1. 程式人生 > >暴力求解-列舉排列(二)

暴力求解-列舉排列(二)

通過上一篇的內容,我們可以得到一顆遞迴解答樹

假設n=4.序列為{1,2,3,4},可以用一顆樹顯示出了。這顆樹和前面介紹的二叉樹不同。第0層(根)結點有n個子結點,第1層結點各有n-1個子結點,第2層結點各有n-2個子結點,第3層結點各有n-3個子結點,....,第n層結點都沒有子結點(即都是葉子),而每個葉子對應於一個排列,共有n!個葉子。由於這棵樹展示的是從什麼都沒做逐步生成完整解的過程,因此將其稱為解答樹。

如果某問題的解可以由多個步驟得到,而每個步驟都有若干種選擇(這些候選方案集可能會依賴於先前做出的選擇),且可以用遞迴列舉法實現,則它的工作方式可以用解答樹來描述。

這顆解答樹一共有多少個結點呢?可以逐層檢視,第0層有1個結點,第1層n個,第2層有n*(n-1)個結點(因為第1層的每個結點都有n-1結點),第3層有n(n-1)*(n-2)個(因為第2層的每個結點都有n-2個結點),....,第n層有n*(n-1)*(n-2)*.....*2*1 = n!個。

下面把它們加起來,為了推導方便,把n*(n-1)*(n-2)*...*(n-k)寫成n! / (n-k-1)!,則所有結點之和為:

T(n) = \sum_{k=0}^{n-1} \frac{n!}{(n-k-1)!} = n!\sum_{k=0}^{n-1}\frac{1}{(n-k-1)!} = n!\sum_{k=0}^{n-1}\frac{1}{k!}

根據高等數學中的泰勒展開公式,\lim_{n->\infty }\sum_{k=0}^{n-1}\frac{1}{k!} = e,因此T(n) < n! e = O(n!) 。由於葉子有n!個,倒數第二層也有n!個結點,因此上米娜的各層全部加起來也不到n!,這是一個很重要的結論:在多數情況下,解答數上的結點集合全部來源於最後一兩層。和它們相比,上米娜的結點數可以忽略不計。

不熟悉泰勒展開公式也沒有關係,可以寫一個程式,輸出\sum_{k=0}^{n-1}\frac{1}{k!}隨著n增大時的變化,並發現它能很快收斂。這就是計算機的優點之一-可以通過模擬避開數學推導。即使無法嚴密而精確的求解,也可以找到令人信服的實驗資料。

下一個排列

列舉所有排列的另一個方法是慧聰字典序最小排列開始,不停呼叫 求下一個排列的過程。如何求下一個排列呢?C++的STL中提供了一個庫函式 next_permutation。

執行結果:

void STL_print_repeat_permutation(int n,int *P)
{
   //從字典序的最小排列開始,不停呼叫求下一個排列的過程
   do{
      for(int i=0;i<n;i++) //輸出排列P
        printf("%d ",P[i]);
      printf("\n");
   }while(next_permutation(P,P+n)); //求下一個排列
}

需啊喲注意的是,上述程式碼同樣使用於可重集。記得在此之前對P陣列進行排序。