我们知道,组合数在组合数学中非常有用,组合数是一个十分常用的数字。
比如,书架上有4本书ABCD,我们想拿2本读,那么有C(4,2)种方法。
这个数字的求法是:4!/2!(4-2)!,其中n!=1*2*3...n,是阶乘数。
所以按定义求其实也十分“容易”,这里是指容易写罢了。
先求一个阶乘:
def fac(n):
if n<=1:
return 1
s=1
while n>1:
s*=n
n-=1
return s
然后求组合数Cnm:
def Cnm(n,m):
if n<0 or m <0 or n<m:
raise ValueError
return fac(n)//(fac(m)*fac(n-m))
真的是非常容易理解了。
数字小,还好说。但如果你不用python,你的阶乘算法很可能溢出,(或者一些新手用递归写阶乘,python是不能高阶递归的,大概990+次递归),那么这个就很麻烦了。
实际上高中老师教的计算方法是,C(n,k)=n*(n-1)...(n-k)/k*(k-1)...1;
原则上是简化了“手算”。
但是不能根本上保证阶乘溢出问题。
苦苦思索,高中真的白读了吗?
其实真的不是,有一种结合了高中一道经典组合题产生的动态规划法可以相对较快的求出组合数,只是多花了不少空间。
考虑:
一个4X5的方格组,也就是一行5个格子,有4行,最开始你站在最左上角,你可以向下或向右一步,问有几种方法来到达最右下角。
高中数学解法是这样的,你一共需要走7步,但是你需要选择3步向下,4步向右,但是选择在什么时候来走下,其余走右,
这就是C(7,3)的数字。
哇,这东西出来组合数了诶。
但是我们考虑一个表格,每个格对应方格祖的一个,这个格子记录你到达这里有几种方法。
动态规划的思想是:你为了到达A[i][j],你可以从这个格上面的格子向下走,也可以选择从左边的格子向右走。
这是一个‘或’事件思想。也就是A[i][j]=A[i-1][j]+A[i][j-1]
为此我们为了初始化一个可以求解的表格,在最外圈加一层都是‘1’的格子,其实很好理解,到达A【0】【k】只有一种方法,那就是一直向右走,纵列同理。
所以我们最后的结果是A[n][n].
def Cnm(n,k):
#为了初始化简单,直接全部设为1
dp=[[1 for x in range(k+1)] for y in range(n-k+1)]
for i in range(1,n-k+1):
for j in range(1,k+1):
dp[i][j]=dp[i][j-1]+dp[i-1][j]
return dp[-1][-1]
其中返回的是要求的数字,但是实际上我们算出了所有组合,而且斜对角看是一个杨辉三角。
这用了O(nk)的时间与空间,但是换来了更快的、不溢出的算式。