深入探讨:如何实现排列组合

一、引言

当你点开了这篇博客,希望你能站在跟我一起探讨的角度上来思考这个问题,那么也许你能获得更多的启示 ^_^。

最近在做 LeetCode 的时候,有一道题让我想到了另一个问题:

如何编程实现排列组合算法?
也就是说,输入 M 个元素的整型数组,输出取 N 个数的排列组合结果,并将结果打印出来。

这个问题乍一听,好像并不复杂,但是仔细一想,又好像无从下手。毕竟是 M 中取 N 个元素,如果仅仅是 M 中取 1 个元素的话,我们只需要遍历一次即可;取 2 个元素的话,我们大不了遍历两次吧;但是,取 N 个元素呢?你怎么控制遍历 N 次呢?

很明显,这个问题不能使用循环来做。

那么,我们该如何看待这个问题呢?看来还是举个例子说比较好:

Input: M = [1, 2, 3, 4] N = 3

这里我们得到了输入数组 1, 2, 3, 4 ,并且要从中取出 3 个数,最后输出所有的排列的结果。

我们可以这么思考这个问题:

行为等价行为
4 个中取 3 个3 个中取 2 个 * 4
3 个中取 2 个2 个中取 1 个 * 3

这里,你可以试想,当我们要从 4 个中取出来 3 个,那么我们可以随意先取 1 个,那么取这 1 个的可能性就是 4 种,再乘以 3 个中取 2 个的所有结果,就得到了 4 个中取 3 个的结果。之后再依次类推即可。

最后我们发现:

我们要求 M 中取 N 个数的结果,只要求到 M - 1 中取 N - 1 个数的结果即可
同样的,我们要求 M - 1 中取 N - 1 个数的结果,只需要求 M - 2 中取 N - 2 个数的结果即可

直到,我们只需要求到 M - N + 1 中取 1 个数的结果,就可以依次算出 M 中取 N 个数的结果.

怎么样,分治的方法思路非常清晰吧。

只要这一点领会了之后,再编写程序也就有了设计思路了,接下来让我们来编写这个程序吧。

二、实现:排列

引言里已经说出了排列实现的思路,编写这份代码其实是比较痛苦的。如果你对递归稍微有点不熟悉的话,可能就会花费很多时间。不过还好,最后还是写出来了 :)

// test for M get N permutation
class Solution0 {
public:
    vector<vector<int>>  getPermutation(vector<int> m, int n) {
        vector<vector<int>> result;
        vector<int> tempVec;
        if (n == 1) {
            for (auto i : m) {
                tempVec.clear();
                tempVec.push_back(i);
                result.push_back(tempVec);
            }
            return result;
        }
        vector<vector<int>> tempResult;
        for (int i = 0; i < m.size(); ++i) {
            tempVec = m;
            tempVec.erase(tempVec.begin() + i);
            tempResult = getPermutation(tempVec, n - 1);
            for (auto vec : tempResult) {
                vec.push_back(m[i]);
                result.push_back(vec);
            }
        }
        return result;
    }
};

有什么比直接讲解代码能更加体现思路的呢,就让我简要的讲解下这段代码吧:

  1. 首先,递归都是有一个逼近条件的。这里的逼近条件是什么呢?就是这个 N 值,我们每次取掉一个数,待取值范围就会少一个数,也就是每次递归传入的 m 数组就会少一个数。当传入的 n 值等于了 1,也就是在待取范围里取 1 个数的可能结果,这个是非常好求的,所以这里直接返回了

  2. 然后,我们拿到了取 1 的结果了,我们得到取 2 结果,只需要往取 1 的结果里面加入一个值即可(这个值会有不同的取值可能,也就生成了不同的取 2 的排列可能);这里的递归调用,将取出的数(遍历取值)取出之后,将减少了一个数的数组传入了递归函数中,目的是为了获取下一级获取 n - 1 的数值

  3. 最后,我们依次拿到了取 1 的结果、取 2 的结果,直到我们拿到了最后的取 n 的结果

在代码里,使用了 std::vector 来存储一组数值,使用了 std::vector<vector<int>> 来存储输出数组,使用了 std::vector::erase 方法来删除指定的元素(模拟取数过程)。这些都是基础的部分,也就不再赘述了。

说实话,这段代码没什么可读性。或者说复杂的递归函数本来就没有什么可读性可言。不过能够完成目标也算一种安慰吧。

三、实现:组合

那么,接下来让我们看看组合该如何实现呢?

其实组合与排列不一样的地方是,需要手动去剔除那些元素相同但是排列不同的集合,比如:

{1, 2, 3}
{2, 1, 3}
{3, 2, 1}
{1, 3, 2}
{2, 3, 1}
{3, 1, 2}

以上这六种排列,均只对应了一个组合。为了达到剔除的目的,我专门写了一个判断函数,只有没有出现过的组合才能加入到结果输出中去。

代码如下:

// test for M get N combination
class Solution1 {
public:
    vector<vector<int>> getCombination(vector<int> m, int n) {
        vector<vector<int>> result;
        vector<int> tempVec;
        if (n == 1) {
            for (auto i : m) {
                tempVec.clear();
                tempVec.push_back(i);
                result.push_back(tempVec);
            }
            return result;
        }
        vector<vector<int>> tempResult;
        for (int i = 0; i < m.size(); ++i) {
            tempVec = m;
            tempVec.erase(tempVec.begin() + i);
            tempResult = getCombination(tempVec, n - 1);
            for (auto vec : tempResult) {
                vec.push_back(m[i]);
                if (!existEqualCombination(result, vec)) {
                    result.push_back(tempVec);
                }
            }
        }
        return result;
    }

protected:
    bool existEqualCombination(vector<vector<int>> result, vector<int> temp) {
        sort(temp.begin(), temp.end());
        for (auto vec : result) {
            sort(vec.begin(), vec.end());
            if (vec == temp) return true;
        }
        return false;
    }
};

这段代码中,大部分逻辑与求排列的逻辑是一样的,所不同的是在加入结果输出的时候,加了一个判断函数,此函数根据 std::vector::operator== 操作符的性质进行判断,在判断之前必须对比较双方先进行排序(按照字段序比较)。

因为思路大体一样,这里也就不再赘述了。

四、呵呵:std::next_permutation

有没有简单的方法:

当然有啦 ~~~

我们通过查找资料可以得知 STL 中有这么一个函数 C++在线参考手册之std::next_permutation,想要详细了解这个函数的同学可以点击这里。

这个函数大概就是可以输出指定容器的排列结果。通过使用这个函数,我写出了更加简单的求排列结果的方法:

// test for std::next_permutation 
class Solution2 {
public:
    vector<vector<int>> getPermutation(vector<int> m, int n) {
        vector<vector<int>> result;
        if (n == m.size()) {
            while (next_permutation(m.begin(), m.end())) {
                result.push_back(m);
            }
            result.push_back(m);
        } else {
            for (int i = 0; i < m.size(); ++i) {
                vector<int> temp = m;
                temp.erase(temp.begin() + i);
                result = getPermutation(temp, n);
            }
        }
        return result;
    }
};

首先,我们要明确一点,我们的 std::next_permutation 函数是能够输出指定容器的排列结果。那么 M 中取 N 个数的结果也就演变成了,如何制造出 N 个元素的 M 数组的子集。

这个问题其实也非常好解决,我们只需要遍历每次取掉一个数,然后递归调用本函数,传入少了一个数的数组即可。当前仅当 n 与当前的传入数组相等的时候,我们就可以直接输出结果了。

这里值得注意的是,通过自测,发现std::next_permutation 函数并不会输出 m 本身的排列结果,所以最后还要加上 m 本身。

至此,我们关于编程实现排列组合算法的探讨算是告一段落了。

真是花了不少的精力呢 T_T

五、总结

其实这是一个看起来比较简单的问题,但是真的做起来却着实让我花了不少的时间(加上调试的时间应该也有四五个小时吧)。

不过这一次的探索,让我对分治法有了初步的认识。真正引领我做出来的思路是这么一句话:

分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。

试想这里,同样的,我们要求到 M 中取 N 的排列,就只需要将问题简化为规模较小的 M - 1 中取 N - 1 的排列即可,那么依次类推,直到我们取 1 的结果直接能够计算出来,那么通过递归就可以反推到 M 中取 N 的结果。

不得不说,这是一次非常有趣的尝试。

To be Stronger!

  • 10
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值