排列组合生成问题的讨论(二)

67 篇文章 0 订阅
58 篇文章 0 订阅

简介

    在前面的文章里我们讨论了排列,全排列和可重复排列的几种实现。这里我们接着讨论组合的几种情况以及对应的实现。这些问题衍生出来的其他问题非常多,不过都有一定的套路可以遵循。这里针对这些情况一一讨论。

 

问题分析

    具体组合的情况也有不同种。比如说我们最常见的,针对不重复的n个元素的集合a,从其中取出k个元素来(k <= n), 所有取出这些元素和位置无关,所以它们构成一个组合C(n, k)。还有一些是针对可以重复的集合取出k个数的情况。另外,针对纯组合问题,我们可能会考虑一些针对集合元素里所有可能的组合,包括的元素个数从1个到n个。还有一些若干个元素之和等于某些元素的问题,也可以从这里找到相关的思路。

 

不可重复取组合

    针对这种情况,我们先来看一个示例,假设我们有数据集合{1, 2, 3},那么如果从里面取两个元素的组合则有(12), (13), (23)。其中每个元素只能在被选择的集合里出现一次。这里是假设我们原有的数据集合里也没有重复的元素。

    现在,我们来考虑一种实现组合的思路。以前面的数据集合{1, 2, 3}为例。首先,我们在目的数据集里取第一个元素1, 那么后面一个位置可以取的元素就是从{2, 3}里挑了。原来选定的1不能取。在前面的一个元素选择了之后,我们后面要选择或者能选择的范围应该被前面的给限定了。所以,如果我们用递归的函数来描述的话,必然有一个类似与槛一样的参数,它指定后面的元素只能从某个范围开始来取。以数列{1, 2, 3, 4, 5}为例来看。首先第一个位置取1, 然后第二个取2, 于是得到(12)。如果有第三个项的话,需要取的是3。后面可以取的是(124),(125)。所以,从这里可以看到,每个位置所取的元素是从前面指定位置后的所有可以取的位置。

    我们再考虑一下递归结束的条件。当递归的层次到给定取的元素个数时,才能输出给定的组合,并退出。

     于是,按照前面的这个思路,我们可以得到一个大致的伪码实现:

void combine(源数据集合a, 目的数据集合b, 当前迭代起始点begin, 当前目标数据集合位置cur, int n) {
    if(cur == n)
       //输出数组b
    for(集合a中间从begin到end的元素i) {
        b[cur] = a[i];
        combine(a, b, i + 1, cur + 1, n);
    }
}

   从这部分伪码来进一步细化一下代码实现。这里传递的begin其实就是从源数组里开始搜索的位置。于是,详细的实现如下:

public static void newCombine(int[] a, int[] b, int begin, int cur, int n) {
        if(cur == n) {
            printList(b);
            return;
        }
        for(int i = begin; i < a.length; i++) {
            b[cur] = a[i];
            newCombine(a, b, i + 1, cur + 1, n);
        }
    }

    我们需要注意的就是在实现里,if(cur == n)里设定了return语句。因为当这部分输出数组的工作结束就该返回了。我们也可以通过将后面的for循环包含在else语句中来省略掉return语句。调用这个函数的示例代码如下:

public static void main(String[] args) {
        int[] a = {1, 2, 3, 4};
        int n = 2;
        int[] b = new int[n];
        newCombine(a, b, 0, 0, n);
    }

 

可重复取组合

    假设源数组里有数字{1, 2, 3, 4},要求取两个数字的组合。那么可重复取的组合可以有(11), (12), (22)等。和前面取组合元素里一个最大的差异就是,前面的取法里要求后面能取的元素只能是前面给定的开始范围。而因为这里可以最小取一个和前面给定元素相同的。比如说当前元素所在的索引为i,那么后面需要取的元素只要是大于或者等于b[i - 1]的。这样,这种取法的一个特点就是不需要根据当前取到的元素去给后面的元素指定开始取的位置了。

    在原来的基础上,我们修改后的代码实现如下:

public static void repCombine(int[] a, int[] b, int cur, int n) {
        if(cur == n) {
            printList(b);
            return;
        }
        for(int i = 0; i < a.length; i++) {
            if(cur == 0 || a[i] >= b[cur - 1]) {
                b[cur] = a[i];
                repCombine(a, b, cur + 1, n);
            }
        }
    }

    这里的要点也在于这个if(cur == 0 || a[i] > b[cur - 1])这个判断。这里根据是否为开始的索引0或者判断取到的元素是否比前面已经取的元素大来确定下一个元素。相对来说,调用方法参数更少一些。调用的方法代码如下:

public static void main(String[] args) {
        int[] a = {1, 2, 3, 4};
        int n = 2;
        int[] b = new int[2];
        repCombine(a, b, 0, n);
    }

 

总结

    这里结合不能重复取和可以重复取的两种情形进行了分析。重点在于组合的选取里,我们首先保证源数据是有序的,这样每次取的元素可以根据顺序来判断获取。而后面取的元素基于一个递归迭代的关系,要么根据当前的位置来指定下一次搜索源数据的位置,要么按照一定的条件从源数据和当前目标数据列里进行判断筛选。思路比较有意思,值得好好体会。我们以前讨论过的部分排列其实用到了组合和排列技术的结合。它无非就是首先取得一个组合,然后再对组合的元素进行全排列。另外,本文前面提到的一些其他的应用也会在后续的文章里讨论。

 

参考材料

http://blog.csdn.net/zmazon/article/details/8315418

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值