1. 程式人生 > >深入探討:如何實現排列組合

深入探討:如何實現排列組合

一、引言

當你點開了這篇部落格,希望你能站在跟我一起探討的角度上來思考這個問題,那麼也許你能獲得更多的啟示 ^_^。

最近在做 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

有沒有簡單的方法:

當然有啦 ~~~

這個函式大概就是可以輸出指定容器的排列結果。通過使用這個函式,我寫出了更加簡單的求排列結果的方法:

// 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!