6.3 二项组合树

n选k组合问题

  高中时,我们就学习过排列组合,从n个中选k个是 ( k n ) (^n_k) (kn),但是如何用代码生成却是一个问题。一个传统的方法是字典序循环法。比如在1~5中选3个,代码就是这样:

def select_3(arr):
    results = []
    n = len(arr)
    for i in range(n):
        for j in range(i + 1, n):
            for k in range(j + 1, n):
                results.append([arr[i], arr[j], arr[k]])
    return results


if __name__ == '__main__':
    print(select_3([1, 2, 3, 4, 5]))

  输出为:

[[1, 2, 3], [1, 2, 4], [1, 2, 5], [1, 3, 4], [1, 3, 5], [1, 4, 5], [2, 3, 4], [2, 3, 5], [2, 4, 5], [3, 4, 5]]

  这个算法有非常明显的缺点,选k个就要k层循环,代码扩展性差。

递归字典序法

  为了摆脱这个缺点。我们知道,对于任何循环层数不固定的代码,都有两种解决方案:一是递归,第二种其实还是递归不过使用了BFS或DFS优化递归。下面就用递归优化上述代码。
  比如对于5选3,其实是选好一个后,再4选2,这样逐步递归。但是这种递归方式不是字典序的。如果严格依据上一步的算法改成递归,应该是先选一个,再从数组索引之后的子数组里选k-1个元素。以下是使用递归的代码:

def select(arr, n):
    if n == 1:
        return [[x] for x in arr]
    results = []
    for i, e in enumerate(arr):
        subs = select(arr[i + 1:], n - 1)
        for sub in subs:
            results.append([e] + sub)
    return results


if __name__ == '__main__':
    print(select([1, 2, 3, 4, 5], 3))

  结果都是一样的,我就不输出结果了。对于这种递归,分析递归的过程,就可以优化为树状结构,然后再使用DFS或BFS代替递归了。

二项组合树介绍

  学过二项堆的都知道二项树。二项树的特点呢,就是长子节点数刚好是总结点数的一半,次子呢,节点数是长子的一半。这个数据结构十分优雅。
  说了这么多,不如贴一张图,以三元素的所有组合为例子,从图里可以看出所有的组合构成的就是二项树。
三元素的二项组合树

  二项组合树有以下几个特点:
  1. 根节点是空集。以0开始,第N层是N个元素的组合;
  2. 亲兄弟之间以在原集合内的顺序排序;
  3. 儿子比父亲多一个元素,并且只能取原集合中父亲之后的元素。
  二项组合树用来求全排列性能并不高,因为二进制算法性能更好,但是用来求C(M,N)的所有组合是非常快的,因为只要用BFS搜索取那一层的就行了。因为我们只取那一层,所以可以使用剪枝策略,下一层就不用取了。因为计算父亲所在的索引性能比较差,所以在节点中存储自己最后一位元素的索引可以提高性能。最终我用十几行行python代码,写出了二项组合树。

python代码实现

def combinations(input, n):
    # 队列元素是集合加上索引的元组
    queue = [([], 0)]
    result = []
    while len(queue) > 0:
        e = queue.pop(0)
        level = len(e[0])
        if level == n:
            # 剪枝策略
            result.append(e[0])
        else:
            # 计算children
            subarray = input[e[1]:]
            for i, child in enumerate(subarray):
                x = (e[0] + [child], e[1] + i + 1)
                queue.append(x)
    return result


if __name__ == '__main__':
    print(combinations([1, 2, 3, 4, 5], 3))

  测试结果

[[1, 2], [1, 3], [1, 4], [1, 5], [2, 3], [2, 4], [2, 5], [3, 4], [3, 5], [4, 5]]

java代码实现

public class CombinationUtils {
    private static class Node {
        Object[] combination;
        int index;

        public Node(Object[] combination, int index) {
            this.combination = combination;
            this.index = index;
        }
    }

    public static Object[][] getCombinations(Object[] input, int n) {
        // 长度可以通过组合公式计算出来C(N,K)=N!/K!(N-K)!,这里就不计算了,直接用ArrayList实现
        final ArrayList<Object[]> combinations = new ArrayList<>();
        // 使用二项组合树算法
        final ArrayDeque<Node> deque = new ArrayDeque<>();
        deque.add(new Node(new Object[]{}, 0));
        while (!deque.isEmpty()) {
            final Node node = deque.pop();
            // 剪枝策略
            if (node.combination.length == n) {
                combinations.add(node.combination);
            } else {
                // 放入children
                for (int i = node.index; i < input.length; i++) {
                    final Object[] child = new Object[node.combination.length + 1];
                    System.arraycopy(node.combination, 0, child, 0, node.combination.length);
                    child[node.combination.length] = input[i];
                    deque.add(new Node(child, i + 1));
                }
            }
        }
        return combinations.toArray(new Object[0][]);
    }
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

醒过来摸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值