一、引言
当你点开了这篇博客,希望你能站在跟我一起探讨的角度上来思考这个问题,那么也许你能获得更多的启示 ^_^。
最近在做 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;
}
};
有什么比直接讲解代码能更加体现思路的呢,就让我简要的讲解下这段代码吧:
首先,递归都是有一个逼近条件的。这里的逼近条件是什么呢?就是这个 N 值,我们每次取掉一个数,待取值范围就会少一个数,也就是每次递归传入的 m 数组就会少一个数。当传入的 n 值等于了 1,也就是在待取范围里取 1 个数的可能结果,这个是非常好求的,所以这里直接返回了
然后,我们拿到了取 1 的结果了,我们得到取 2 结果,只需要往取 1 的结果里面加入一个值即可(这个值会有不同的取值可能,也就生成了不同的取 2 的排列可能);这里的递归调用,将取出的数(遍历取值)取出之后,将减少了一个数的数组传入了递归函数中,目的是为了获取下一级获取 n - 1 的数值
最后,我们依次拿到了取 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!