一,加法原理與乘法原理
加法原理與乘法原理是排列與組合的基礎。加法原理本質上是分類,乘法原理本質上是分步。
分類,就是把一個集合(某事物)分成互不相交的若干獨立的部分。比如,概率論中的全概率公式就將事件分成”全划分“
分類思想可以簡化程序的時間復雜度。比如:最短路徑算法-Dijkstra算法的應用之單詞轉換(詞梯問題)
分步,就是第一步干嘛,第二步再干嘛……比如A地到D地,第一步:先到達B地;第二步,再到達C地
二,排列
P(n,r)表示從n個數中選擇r個數的一個全排列
公式:P(n,r)=P(n-1,r)+p(n-1,r-1)*r
上面公式用到了分類思想:對於某個元素而言,要么選,要么不選。如果不選它,則在剩下的n-1個元素中選r個進行全排列;如果選了它,則在剩下的n-1個元素中只需要選r-1個元素進行全排列了。但是選了的那個元素,一共有r種位置存放(因為這是排列,同一個元素放在不同的位置 屬於不同的排列)故p(n-1,r-1)*r。
排列一共有三種:①線排列,就是通常的普通排列。
公式:P(n,r)=n!/(n-r)!
②圓排列,相當於排序的數圍成一個圓。比如循環隊列的那種實現方式。舉個圓排列的例子如下:
a b c d四個字母的線排列共有4!=24種。對於其中的一種排列: a b c d ,它對應着四種等價圓排列:
1) a b c d 2) b c d a 3)c d a b 4)d a b c
因此,對於給定n個數的所有圓排列,一共有 p(n,r)/r種。 因為,一個圓排列可以產生r個線排列。
③重排列
對於線排列而言,某個元素選了之后,就不能再選了(一個元素只能選一次)
對於重排列而言,一個元素可以選多次。
集合{∞·b1,∞·b2,....,∞·bn}的一個r排列個數為:nr
即從b1,b2...bn,共n個元素中選出r個,每個元素可選任意多次,一共有種nr排列
還有一種重排列是:每個元素最多只能選K次(某個固定的次數)
{K1·b1,K2·b2,....,Kn·bn}( 元素b1最多只能選K1次)的一個r排列個數為:(K1+K2+....+Kn)!/(K1!K2!...Kn!)
上面表示從b1,b2...bn,共n個元素中選出r個進行排列,但是 bj最多只能選Kj次,一共有(K1+K2+....+Kn)!/(K1!K2!...Kn!)次排列方式。
三,組合
C(n,r)表示從n個數中選擇r個數的一個全組合。
公式:C(n, r)=[n!/(r!)(n-r)!]=P(n,r)/r!
公式:C(n,r)=C(n-1,r)+C(n-1,r-1) 這個公式用到了分類思想。對於n個元素的某個元素,要么選,要么不選。
如果選了,只需在 剩下的n-1 個元素中選 r-1個;如果不選,則在剩下的n-1個元素中選r個(因為一共要選r個啊)。
這個公式非常有用,這是楊輝三解公式:其中基准條件C(n,0)=1,C(i,i)=1
C(0,0)
C(1,0) C(1,1)
C(2,0) C(2,1) C(2,2)
C(3,0) C(3,1) C(3,2) C(3,3)
..... ..... .....
如果要求解C(n,r),可以先求解出C(n-1,r) 和 C(n-1,r-1);再運用公式相加即可。很明顯,這是一個與Fib數列類似的遞歸計算。只不過求Fib(n)時,只有一個參數,而這里有二個參數而已。當然,上面的程序效率是非常低的,因為它重復計算了很多子問題。代碼實現如下:
1 public class YanHuiTriangle { 2 //compute C(n,r) 3 public static long c(int n, int r){ 4 if(n == 0 || r == 0) 5 return 1;//base condition 6 if(n == r) 7 return 1;//base condition 8 else 9 return c(n-1, r-1) + c(n-1, r); 10 } 11 12 //compute c(n,r) 13 public static long c_dp(int n ,int r){ 14 long[][] dp = new long[n+1][r+1]; 15 16 //init 17 for(int i = 0; i <= n; i++) 18 dp[i][0] = 1;//if(n==0) 19 for(int i = 0; i <= r; i++) 20 dp[0][i] = 1;//if(r==0) 21 for(int i = 0; i <= r; i++)//Assume r < n 22 dp[i][i] = 1;//if(n==r) 23 24 for(int i = 1; i <= n; i++) 25 { 26 for(int j = 1; j <= r; j++) 27 { 28 if(j < i) 29 dp[i][j] = dp[i-1][j] + dp[i-1][j-1]; 30 else if(j > i)//從 i 個元素中選取出 j個元素(j>i 沒有意義) 31 dp[i][j] = 0; 32 } 33 } 34 return dp[n][r]; 35 } 36 37 public static void main(String[] args) { 38 39 long start_dp = System.currentTimeMillis(); 40 System.out.println(c_dp(50, 10)); 41 long end_dp = System.currentTimeMillis(); 42 System.out.println("dp use time: " + (end_dp - start_dp) + "ms"); 43 44 long start = System.currentTimeMillis(); 45 System.out.println(c(50,10)); 46 long end = System.currentTimeMillis(); 47 System.out.println("non dp use time: " + (end - start) + "ms"); 48 } 49 }
這個示例完美地證明了DP(動態規划)實現和遞歸實現的差距。是學習DP的一個好示例。如何將一個程序從遞歸改成DP?看上面的示例就會有啟示了。
其次,這也是0-1背包問題分解的思路,對於某件物品,要么拿走,要么不拿走。因此,這個組合公式在DP問題的分析中經常用到。
重組合公式:從集合{∞·b1,∞·b2,....,∞·bn}中選取r個元素,(不考慮次序),一共有多少種組合?
答案是:F(n,r) = C(n+r-1, r)
四,二項式定理
(X+Y)n = C(n,0)X0 Yn + C(n,1)X1 Yn-1 +.....+ C(n,n)Xn Y0
“二項式”定理嘛,就是有兩個項相加求n次冪。。。。。