深入探討:如何實現排列組合
一、引言
當你點開了這篇部落格,希望你能站在跟我一起探討的角度上來思考這個問題,那麼也許你能獲得更多的啟示 ^_^。
最近在做 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
有沒有簡單的方法:
當然有啦 ~~~
這個函式大概就是可以輸出指定容器的排列結果。通過使用這個函式,我寫出了更加簡單的求排列結果的方法:
// 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!