组合算法的个人总结

组合算法

〇、题外总结

n个元素的全组合是一个比较特殊的存在,虽然也叫“组合”却有本质上的区别,但本文主要总结的是 *Cnm*类型的组合。 全组合的列举可以直接按(二)进制位来对应每个元素,那么n元素的全组合就一一对应于0~2n所表示的二进制位。

一、组合描述

从n个元素中任选m个元素进行组合,组合结果数量为(特注:符号 C 的上标要始终不大于下标才有意义,后续不再重复声明类似问题)
C n m = C n n − m = C n − 1 m − 1 + C n − 1 m ( 恒 等 式 ) C^{m}_{n} = C^{n-m}_n = C^{m-1}_{n-1} + C^{m}_{n-1} (恒等式) Cnm=Cnnm=Cn1m1+Cn1m

  • 基础理论回顾:
    A n n = n ! ( 基 础 的 全 排 列 , 从 1 − n 每 个 位 置 的 可 选 元 素 为 n − 1 , 即 n ∗ ( n − 1 ) ∗ . . . ∗ 3 ∗ 2 ∗ 1 ) A^n_n = n! (基础的全排列,从1-n每个位置的可选元素为n-1,即n*(n-1)*...*3*2*1) Ann=n!1nn1n(n1)...321
    A n m = A n n A n − m n − m ( 在 全 排 列 的 基 础 上 , 只 取 前 m 个 元 素 , 则 每 个 都 有 后 n − m 个 元 素 在 重 复 排 列 ) A_n^m = \frac{A_n^n}{A_{n-m}^{n-m}} (在全排列的基础上,只取前m个元素,则每个都有后n-m个元素在重复排列) Anm=AnmnmAnnmnm
    C n m = A n m A m m ( 在 一 般 排 列 基 础 上 , 剔 除 排 列 顺 序 因 素 , 则 每 个 组 合 都 有 m 个 元 素 在 重 复 排 列 ) C_n^m = \frac{A_n^m}{A_m^m} (在一般排列基础上,剔除排列顺序因素,则每个组合都有m个元素在重复排列) Cnm=AmmAnmm
二、01转换法
  1. 算法描述
    该算法的原理就是利用了开篇中恒等式的逆向推导过程。具体规则如下:

    • 定义一个一维数组,长度为n,其中前m个元素为1(代表选中),后n-m个元素为0(代表未选中)
    • 初始数组即表示第一个选中的组合,此后开始遍历数组:
      • 从前往后开始,找到数组中首次出现1 0两个连续元素的位置,将此时的1 0互换(相当于1向后移1位)
      • 上一步完成后,将上一步中的1前边的所有1都移到数组的最前端
      • 完成之后,此时数组即表示新的一种组合
      • 继续重复上述循环遍历,直到所有的1都被移到数组最后端

    那么为什么01转换法可以正确地遍历出所有的组合情况呢?

  2. 先分析恒等式
    C n m = C n n − m = C n − 1 m − 1 + C n − 1 m ( 证 明 过 程 自 行 查 找 ) C^{m}_{n} = C^{n-m}_n = C^{m-1}_{n-1} + C^{m}_{n-1} (证明过程自行查找) Cnm=Cnnm=Cn1m1+Cn1m
    该等式可以将 Cnm 问题逐步简化,例如:
    C n m = C n − 1 m − 1 + C n − 1 m = C n − 1 m − 1 + C n − 2 m − 1 + C n − 2 m − 1 = . . . = C n − 1 m − 1 + C n − 2 m − 1 + C n − 3 m − 1 + . . . + C m + 1 m − 1 + C m m − 1 + C m m C_n^m = C^{m-1}_{n-1} + C^{m}_{n-1} = C^{m-1}_{n-1} + C^{m-1}_{n-2} + C_{n-2}^{m-1} =...=C^{m-1}_{n-1} + C^{m-1}_{n-2} +C^{m-1}_{n-3} +...+ C^{m-1}_{m+1} + C^{m-1}_{m} + C^{m}_{m} Cnm=Cn1m1+Cn1m=Cn1m1+Cn2m1+Cn2m1=...=Cn1m1+Cn2m1+Cn3m1+...+Cm+1m1+Cmm1+Cmm
    C 6 4 = C 6 2 = C 5 1 + C 5 2 = C 5 1 + C 4 1 + C 4 2 = . . . = C 5 1 + C 4 1 + C 3 1 + C 2 1 + C 2 2 = 15 C_6^4 = C_6^2 = C_5^1 + C_5^2 = C_5^1 + C_4^1 + C_4^2 =...= C_5^1 + C_4^1 + C_3^1 + C_2^1 + C_2^2 = 15 C64=C62=C51+C52=C51+C41+C42=...=C51+C41+C31+C21+C22=15

  3. 分析算法
    该算法就是利用了上述恒等式的逆向推导过程。

    • 先看算法第一步,数组中前m个为1表示选中,可以看为仅前m个元素参与了组合,后n-m个暂时闲置,即 Cmm
    • 之后的第一次“移位”操作,相当于是将参与组合的m个元素再加一个,并且选中新加进入的元素(不选则会跟上一步重复),而在下一个新元素“加入”前,组合的数量相当于Cmm-1。另外此时的前m个元素依然可能发生“移位”操作,这可以看作为 Cmm-1 的递归子问题,子问题的结果即是 Cmm-1
    • 上述两点完成后,先看结果:Cmm-1 + Cmm,此时的所有1都是连在一起的,下一步将会把最后的一位1继续向后“移位”,从而在当前组合元素池中再加一个新元素,新的组合结果为 Cm+1m-1 ,总的组合结果为 Cm+1m-1 + Cmm-1 + Cmm
    • 到这里该算法的逻辑原理已经很清晰了,随着上述过程继续进行,直到结束,最终可以得到完整的 Cnm 个组合结果。
    • 最后一点,也是该算法最后缺少的一块,就是虽然最后得到了Cnm 个组合结果,但为什么不可能有重复的组合出现?这个问题很简单,算法之所以限制从前往后单向操作,就是为了去重。因为在上述描述中每次“新加入”的元素都是必然被选中的,而这一点也保证了新的组合一定不会与之前的组合重复。解决了这个问题,那么对01转换法的理解才真正完美。
  4. 快速实现版(刚开始用python简单实现的算法版本)

srclist = [854, 631, 546, 682, 670, 493]

def combination(srclist, num) :
    c = [0] * len(srclist)
    c[:num] = [1] * num
    print(list(zip(c, srclist)))
    while shift10(c):
        print(list(zip(c, srclist)))

def shift10(clist):
    """[summary]

    Args:
        clist ([type]): [description]

    Returns:
        [type]: [description]
    """    
    i = 0
    max = len(clist) - 1
    while i < max:
        if clist[i] == 1:
            if clist[i + 1] == 0:
                clist[i] = 0
                clist[i + 1] = 1
                return True
            zero = 0
            while zero < i:
                if clist[zero] == 0:
                    clist[zero] = 1
                    clist[i] = 0
                    break
                zero += 1
        i += 1
    return False

combination(srclist, 3)
[(1, 854), (1, 631), (1, 546), (0, 682), (0, 670), (0, 493)]
[(1, 854), (1, 631), (0, 546), (1, 682), (0, 670), (0, 493)]
[(1, 854), (0, 631), (1, 546), (1, 682), (0, 670), (0, 493)]
[(0, 854), (1, 631), (1, 546), (1, 682), (0, 670), (0, 493)]
[(1, 854), (1, 631), (0, 546), (0, 682), (1, 670), (0, 493)]
[(1, 854), (0, 631), (1, 546), (0, 682), (1, 670), (0, 493)]
[(0, 854), (1, 631), (1, 546), (0, 682), (1, 670), (0, 493)]
[(1, 854), (0, 631), (0, 546), (1, 682), (1, 670), (0, 493)]
[(0, 854), (1, 631), (0, 546), (1, 682), (1, 670), (0, 493)]
[(0, 854), (0, 631), (1, 546), (1, 682), (1, 670), (0, 493)]
[(1, 854), (1, 631), (0, 546), (0, 682), (0, 670), (1, 493)]
[(1, 854), (0, 631), (1, 546), (0, 682), (0, 670), (1, 493)]
[(0, 854), (1, 631), (1, 546), (0, 682), (0, 670), (1, 493)]
[(1, 854), (0, 631), (0, 546), (1, 682), (0, 670), (1, 493)]
[(0, 854), (1, 631), (0, 546), (1, 682), (0, 670), (1, 493)]
[(0, 854), (0, 631), (1, 546), (1, 682), (0, 670), (1, 493)]
[(1, 854), (0, 631), (0, 546), (0, 682), (1, 670), (1, 493)]
[(0, 854), (1, 631), (0, 546), (0, 682), (1, 670), (1, 493)]
[(0, 854), (0, 631), (1, 546), (0, 682), (1, 670), (1, 493)]
[(0, 854), (0, 631), (0, 546), (1, 682), (1, 670), (1, 493)]
  1. 优化版
class Combination:

    def __init__(self, srclist) -> None:
        if not isinstance(srclist, list):
            raise TypeError("the argument 'srclist' needs a <list> type, but %s provided" % type(srclist).__name__)
        self._srclist = srclist

    def combine(self, count) -> Generator:
        if count <= 0:
            return []
        srclen = len(self._srclist)
        if count >= srclen:
            return list(self._srclist)
        slist = [0] * srclen
        for _ in range(count):
            slist[_] = 1
        yield [item for item, flag in zip(self._srclist, slist) if flag]
        while self.__shift10_(slist):
            yield [item for item, flag in zip(self._srclist, slist) if flag]

    def __shift10_(self, slist) -> bool:
        i = 0
        max = len(slist) - 1
        zero = 0    # 记录可能为0的位置
        while i < max:
            if slist[i] == 1:
                if slist[i + 1] == 0:   # 遇到第一个10
                    slist[i] = 0
                    slist[i + 1] = 1
                    return True
                if zero == i:   # 没出现过0,找下一位
                    zero += 1
                else:    # zero记录有首位0的位置,把1移到最左端
                    slist[zero] = 1
                    slist[i] = 0
                    zero += 1   # 指向下一个0
            i += 1
        return False


c = Combination(srclist)
r = c.combine(4)
for i in r:
    print(i)

输出:

[851, 631, 546, 682]
[851, 631, 546, 670]
[851, 631, 682, 670]
[851, 546, 682, 670]
[631, 546, 682, 670]
[851, 631, 546, 522]
[851, 631, 682, 522]
[851, 546, 682, 522]
[631, 546, 682, 522]
[851, 631, 670, 522]
[851, 546, 670, 522]
[631, 546, 670, 522]
[851, 682, 670, 522]
[631, 682, 670, 522]
[546, 682, 670, 522]

在该优化版的实现算法中,算法的时间复杂度可以近似为
O ( n C n m ) O(nC_n^m ) O(nCnm)

三、直观递归

传送门

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本书是《组合数学》第3版的修订版,全书共分8章,分别是:排列与组合、递推关系与母函数、容斥原理与鸽巢原理、burnside引理与polya定理、区组设计、线性规划、编码简介、组合算法简介。丰富的实例及理论和实际相结合是本书一大特点,有利于对问题的深入理解。. 本书是计算机系本科生和研究生的教学用书,也可作为数学专业师生的教学参考书。 目录回到顶部↑ 第1章 排列与组合. 1.1 加法法则与乘法法则 1.2 一一对应 1.3 排列与组合 1.3.1 排列与组合的模型 1.3.2 排列与组合问题的举伊 1.4 圆周排列 1.5 排列的生成算法 1.5.1 序数法 1.5.2 字典序法 1.5.3 换位法 1.6 允许重复的组合与不相邻的组合 1.6.1 允许重复的组合 1.6.2 不相邻的组合 1.6.3 线性方程的整数解的个数问题 1.6.4 组合的生成 1.7 组合意义的解释 1.8 应用举例 1.9 stirling公式 1.9.1 wallis公式 .1.9.2 stirling公式的证明 习题 第2章 递推关系与母函数 2.1 递推关系 2.2 母函数 2.3 fibonacci序列 2.3.1 fibonacci序列的递推关系 2.3.2 若干等式 2.4 优选法与fibonacci序列的应用 2.4.1 优选法 2.4.2 优选法的步骤 2.4.3 fibonacci的应用 2.5 母函数的性质 2.6 线性常系数齐次递推关系 2.7 关于线性常系数非齐次递推关系 2.8 整数的拆分 2.9 ferrers图像 2.10 拆分数估计 2.11 指数型母函数 2.11.1 问题的提出 2.11.2 指数型母函数的定义 2.12 广义二项式定理 2.13 应用举例 2.14 非线性递推关系举例 2.14.1 stirling数 2.14.2 catalan数 2.14.3 举例 2.15 递推关系解法的补充 习题 第3章 容斥原理与鸽巢原理 3.1 demorgan定理 3.2 容斥定理 3.3 容斥原理举例 3.4 棋盘多项式与有限制条件的排列 3.5 有禁区的排列 3.6 广义的容斥原理 3.6.1 容斥原理的推广 3.6.2 一般公式 3.7 广义容斥原理的应用 3.8 第二类stirling数的展开式 3.9 欧拉函数φ(n) 3.10 n对夫妻问题 3.11 mobius反演定理 3.12 鸽巢原理 3.13 鸽巢原理举例 3.14 鸽巢原理的推广 3.14.1 推广形式之一 3.14.2 应用举例 3.14.3 推广形式之二 3.15 ramsey数 3.15.1 ramsey问题 3.15.2 ramsey数 习题 第4章 burnside引理与polya定理 4.1 群的概念 4.1.1 定义 4.1.2 群的基本性质 4.2 置换群 4.3 循环、奇循环与偶循环 4.4 burnside引理 4.4.1 若干概念 4.4.2 重要定理 4.4.3 举例说明.. 4.5 polya定理 4.6 举例 4.7 母函数形式的polya定理 4.8 图的计数 4.9 polya定理的若干推广 习题 第5章 区组设计 5.1 问题的提出 5.2 拉丁方与正交的拉丁方 5.2.1 问题的引入 5.2.2 正交拉丁方及其性质 5.3 域的概念 5.4 galois域gf(pm) 5.5 正交拉丁方的构造 5.6 正交拉丁方的应用举例 5.7 均衡不完全的区组设计 5.7.1 基本概念 5.7.2 (b,u,r,k,λ)-设计 5.8 区组设计的构成方法 5.9 steiner三元素 5.10 kirkman女生问题 习题 第6章 线性规划 6.1 问题的提出 6.2 线性规划的问题 6.3 凸集 6.4 线性规划的几何意义 6.5 单纯形法的理论基础 6.5.1 松弛变量 6.5.2 解的充要条件 6.6 单纯形法与单纯形表格 6.7 改善的单纯形法 6.8 对偶概念 6.9 对偶单纯形法 习题 第7章 编码简介 7.1 基本概念 7.2 对称二元信道 7.3 纠错码 7.3.1 最近邻法则 7.3.2 hamming不等式 7.4 若干简单的编码 7.4.1 重复码 7.4.2 奇偶校验码 7.5 线性码 7.5.1 生成矩阵与校验矩阵 7.5.2 关于生成矩阵和校验矩阵的定理 7.5.3 译码步骤 7.6 hamming码 7.7 bch码 习题 第8章 组合算法简介 8.1 归并排序 8.1.1 算法 8.1.2 举例 8.1.3 复杂性分析 8.2 快速排序 8.2.1 算法的描述 8.2.2 复杂性分析 8.3 ford-johnson排序法 8.4 排序的复杂性下界 8.5 求第是个元素 8.6 排序网络 8.6.1 0-1原理 8.6.2 bn网络 8.6.3 复杂性分析 8.6.4 batcher奇偶归并网络 8.7 快速傅里叶变换 8.7.1 问题的提出 8.7.2 预备定理 8.7.3 快速算法 8.7.4 复杂性分析 8.8 dfs算法 8.9 bfs算法 8.10 αβ剪技术 8.11 状态与图 8.12 分支定界法 8.12.1 tsm问题 8.12.2 任务安排问题 8.13 最短树与kruskal算法 8.14 huffman树 8.15 多段判决 8.15.1 问题的提出 8.15.2 最佳原理 8.15.3 矩阵链积问题 8.15.4 图的两点间最短路径

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值