排列组合的写法_茴香豆:组合序列生成的四种写法

偶尔无聊,生搬硬凑下组合序列生成的四种写法。

方法零 状态/序列生成法

组合,combination,或者说n choose k的问题。一般在命令式编程语言中,采用生成器的方式来完成。大体就是保存状态,按序列生成。这种方式的标准方式,参考C++的algorithm的库。在Python中,使用itertools生成出来。原理都是相似的。

在函数式编程语言中,则有着不同的思路。函数式编程语言中,不提倡使用状态这种方式。或者说通过递归调用+延迟计算的方式,综合起来的特性,得到数学结构上更为优美的解法。

我们约定组合序列生成的函数为comb :: Int -> [a] -> a, comb k xs,其中k为选择个数,xs为待选取列表。生成结果为一个列表的列表。在数学表达式中,我们按照中国约定俗称的C(n,k),表示从n个中选取k个。注意下面的代码示例,可以左右拨动。

我们知道关于组合有一些基本的公式。根据这些公式,其实组合问题,实在逐步递降的(待组合的列表更短,或者选择个数k变小)。而以下的种种写法,其实都是由背后的公式推演出来的。

方法一 C(n+1,k) = C(n,k-1) + C(n,k)

比如我们有公式C(n+1,k) = C(n,k-1) + C(n,k)

4f95a0d32fa3adcb7970959197e46556.png

这个公式的含义为从n+1个中选取k个,等价于固定选择女朋友,从剩余n中选择k-1个;或者直接选择不带女朋友,从剩余n中选择k个。
也就是组合分成两种情况,选择了第一个,或者丢弃第一个的。所有结果,等于这二者之和。这种方式,两个分支中,都缩减了问题的规模。故而函数递归是可以穷尽的。

这个公式理解最为普遍。大名鼎鼎的杨辉三角,就是按照这个公式来进行推演的。

5a0ff8eb04a1acecb7af9768ae05d5ea.png

按照这种理解,我们有Haskell代码:

comb k (x:xs) = map (x:) (comb (k - 1) xs))     ++ (comb k xs)

(x:xs)为模式匹配,匹配第一个元素为x,剩余列表为xs。结果为两者之和,++,表示两部分结果合并。前半部分,给comb (k-1) xs的结果,每一个都添加了x作为头。后半部分,则在xs中选择k个。这行代码与上面的公式完美映射。

方法二 C(n+1,k+1) = C(n,k) + C(n-1,k) + ... + C(k,k)

这个公式的含义为在n+1个中选择k+1个,等于以下情况之和:

  • 按序最先选择第一个为火车头,剩下的选择k个

  • 按序最先选择第二个为火车头,剩下的选择k个

  • 。。。

  • 按序最先选择第k+1个为火车头,剩下的选择k个(注意,这个时候只剩下k个,所以就是这k个)

fc31eb51811ed19d3dd90221547cc72d.png

按照这个公式,我们利用haskell中的tails函数。这个函数生成序列的每个后缀。

tails [0..3][[0,1,2,3],[1,2,3],[2,3],[3],[]]

我们对每个后缀序列,都固定选择其头部的元素,剩余的选择k个。有haskell代码如下。f函数,就是固定选择头t,剩下的在递归到组合函数comb。这样得到的结果,再全部累积一起,通过concat函数。

comb k xs = concatMap f (tails xs)    where f [] = []          f (t:ts) = map (t:) (comb (k - 1) ts)

方法三 C(m+n,k) = SUM[C(n,i)C(m,k-i)] 0<=i<=k

这个公式的意思为将待选列表分为两部分m和n,然后等于下列情况之和

  • 在m个男生中选择0个,在n个女生中选择k个

  • 在m个男生中选择1个,在n个女生中选择k-1个

  • 。。。

  • 在m个男生中选择k个,在个女生n中选择0个

57af05234bf8e60d6266a58b4d4df373.png

这种方式的最大的好处是,直接将m+n的问题量级降低到其一半水平。用这种方式,理论上将问题从O(N)的方式,可以转换为O(logN)。因为我们生成的是全序列,这种差异体现不出来。

这里我们要注意列表相乘运算,也就是m中选择的i中情况,与n中选择k-i中情况,是可以交叉配对的。也就是第一个结果中的任一一个列表,可以和后一个结果中的任一个列表构成一组新的结果。更形象一点,就是我的选择i种情况出来的A结果,与你的选择k-i种情况出来的B结果,都可以组合在一起,然后形成选择为k个对象的结果。这个结果的个数为A*B=AB。

这等价于排列组合的乘法原理,也等价于笛卡尔积的过程。按照haskell中的实现,我们直接利用liftM2,这个monad辅助函数实现了。

prod :: a -> a -> aprod xs ys = liftM2 (++) xs ys

我们有最终的代码:

comb k xs = concatMap               (\i -> prod (comb (k-i) hd) (comb i tl))              [0..k]    where (hd, tl) = halve xs

从hd中选择k-i个,出来的结果,与tl中选择i个,乘在一起,最终相concat,构成新的结果。

完整代码如图,或者参考原文链接。

226ac8a0a110ac638427206ea3a537ce.png

综上,我们可以看到在函数式语言中,可以按照数学公式写出非常优雅的代码。而且因为其中的逻辑清晰,不需要关心更多细节的状态问题,相对更容易做到bug-free。这种书写代码的方式,目前也在和其他编程范式在相互融合。了解下,就当一起玩下吧。

鸣谢:文中的很多图片来自于浣熊数学

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值