1. 程式人生 > >LeetCode - Permutations(全排列、康託展開 Cantor expansion)- 題目 31、46、47、60、77

LeetCode - Permutations(全排列、康託展開 Cantor expansion)- 題目 31、46、47、60、77

31. Next Permutation

46. Permutations

47. Permutations II

60. Permutation Sequence

77. Combinations

這五道題是LeetCode上關於數列的全排列的題目,為了學習和總結,先記錄一下康託展開(Cantor expansion)和逆運算(也有叫康託編碼與解碼的,但我感覺其實就是全排列 = =)。

首先,全排列就是指1到n這n個數,按照各種順序排列,顯而易見,有n的階乘種不同的排列方式。

比如說 1, 2, 3 三個數,有123、132、213、231、312、321 共 6(3的階乘) 種不同的排列方式,就好像三個不同的球,放三個不同的盒子,做排列組合的題目。

在LeetCode中,這些排列是有順序的,如果是數字,那就按照數字的大小順序,如果是字母,那就按照字母的ascII碼順序,也是從左往右,優先順序越來越低。比如先是123,然後是132,先是abc,然後是acb。

一、康拓展開(給一個排列,問是在全排列中的第幾個

    比如說,對於1,2,3三個數,給出一個排列是 213,問它是1,2,3的全排列中的第幾個。由於需要將213展開,一項一項計算,所以就叫展開。

    康託展開是一個全排列到一個自然數雙射Bijection ),康拓展開其實是一種特殊的雜湊函式,常用於構建雜湊表

時的空間壓縮。把一個整數X展開成如下形式(其中 ! 表示階乘,a為整數,且0 <= a < i, i =1,2,..,n): 

                                             X = a[n] * n! + a[n - 1] * (n - 1)! + ... + a[2] * 2! + a[1] * 1!

    上述公式中的 a[n] 用例子說明更容易理解。 比如在(1,2,3,4,5)五個數全排列中,我們想知道 34152 的康託展開值,也就是它在全排列中是第幾個。步驟如下(演算法順序是從右往左):

    1、首位是3,比3小的數有兩個,1和2,所以a[5] = 2,可以得出,首位小於3,也就是首位是1或者2的排列組合共有 a[5] * (5 - 1)! = 2 * 24 = 48 種;

    2、第二位是4,比4小的數有兩個,1和2(因為3已經出現過了,這點很重要),所以 a[4] = 2;

    3、第三位是1,小於1的數為0個,所以 a[3] = 0;

    4、第四位是5,小於5且沒有出現過的數有1個,就是2,所以 a[2] = 1;

    5、最後一位不用計算,因為是全排列,沒有重複的,最後只剩一個沒有出現過的。

    那麼,根據上述公式, X = 2 * 4! + 2 * 3! + 0 * 2! + 1 * 1! + 0 * 0! = 61,所以比34152小的排列組合有61種,那麼34152在全排列中,就是第62個。如果理解了上述過程,程式碼就很好寫了。

一、康拓展開逆過程(求出1到n,n個數全排列中第 x 個排列

    既然康託展開是一個雙射,那麼一定是可逆的,可以通過康託展開值求出原排列,即可以求出n的全排列中第x大排列。

    比如說n = 5,x = 62,也就是 1,2,3,4,5 五個數的排列組合中第62個是什麼樣。演算法步驟如下:

    1、首先 62 - 1 = 61(因為從0開始);

    2、用 61 / 4! = 2 餘 13,那麼 a[5] = 2,第一位就是3(因為比首位數小的有兩個);

    3、用 13 / 3! = 2 餘 1 , 那麼 a[4] = 2,第二位就是4(因為比第二位小的有兩個,3又用過了,也就是找出第 i + 1 小的沒有用過的數);

    4、用  1  / 2! = 0 餘 1  ,那麼 a[3] =  0,第三位是1;

    5、用  1  / 1! = 1 餘 0  ,那麼第四位是在12345五個數中沒有用過的裡邊第 2 小的(1、3、4用過了,剩 2 和 5),所以是 5;

    6、最後一位不用計算,因為只剩一個沒有用過的數了。

從上邊的演算法步驟可以看出兩點:

    1)每次計算除法後的結果,加一,比如等於 m,那這一位就是1到n這些數中,沒有用過的裡邊第 i 小的,除法的餘數是下一次的被除數,除數從 n - 1開始,每次減一;

    2)需要用一個數組或者容器,儲存著n個數是否被用過。

    理解了上面的Cantor展開,就容易做出最上邊的五道題目了。

31、Next Permutation

Implement next permutation, which rearranges numbers into the lexicographically next greater permutation of numbers.

If such arrangement is not possible, it must rearrange it as the lowest possible order (ie, sorted in ascending order).

The replacement must be in-place and use only constant extra memory.

Here are some examples. Inputs are in the left-hand column and its corresponding outputs are in the right-hand column.

1,2,3 → 1,3,2
3,2,1 → 1,2,3
1,1,5 → 1,5,1

解:

    題目意思就是給你一個排列 P,返回它的下一個排列,也就是比 P 大的裡邊最小的排列(P中數字可能有重複的)。

    這道題的思路如下:

    1、從右往左,找到第一個不滿足上升趨勢的數,記錄它的位置比如叫 i,比如說 123542,那麼 “3” 這個位置就是第一個不滿足的(如果是 54321 這樣遍歷完了都沒有找到的,一定是最大的排列了, 這時按題目要求返回這些數的升序排列即可);

    2、再從右往左,找到第一個比 i 位置的數大的數,記錄位置比如叫 j,比如 123542中,“4”的位置就是第一個大於 3 的;

    3、交換 ij 位置上的數,123542 變成 124532;

    4、從 i + 1 位置(包括i+1)開始,到陣列最後,進行升序排序,123542 變成 124235,返回結果。

分析上邊的步驟就能理解,相當於

    1、先找到第一個可以變大的,變大後影響最小的位置

    2、然後從這個位置後邊的所有數中,找到比這個數大的裡邊最小的一個

    3、然後交換這兩個數,這樣從左往右,直到第 i 個位置,就一定是變大的排列裡邊,最小的情況

    4、最後,i + 1 到最後,直接升序排序即可。

void nextPermutation(vector<int>& nums)
{
    int n = nums.size();
    for (int i = n - 1; i >= 0; i--)
    {
        if(nums[i] < nums[i + 1])
        {
            for (int j = n - 1; j > i; --j)
            {
                if (nums[j] > nums[i])
                {
                    swap(nums[i], nums[j]);
                    sort(nums.begin() + i + 1, nums.end());
                    return;
                }
            }
        }
    }
    sort(nums.begin(), nums.end());
}

    其實在C++ STL中,<algorithm>標頭檔案中,有一個函式就是做這個的,就叫 next_permutation。這道題下邊的一行程式碼也可以搞定。

next_permutation(begin(nums), end(nums));

 

46、Permutations

Given a collection of distinct integers, return all possible permutations.

Input: [1,2,3]       Output: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]

解:

    就是給出一組數,返回所有它的全排列。有了上邊的那道題目我們直接利用上邊的函式,或者直接利用STL的方法,從第一個變 n! - 1 次到最後一個,就能得到全排列了。beat 99.93% of cpp submissions。

vector<vector<int>> permute(vector<int>& nums)
{
    sort(nums.begin(), nums.end());
    vector<vector<int> > res;
    res.push_back(nums);
    int n = 1, s = nums.size();
    while(s != 1)
        n *= s--;
    while(next_permutation(nums.begin(), nums.end()) && --n)
        res.push_back(nums);

    return res;
}

47、Permutations II

Given a collection of numbers that might contain duplicates, return all possible unique permutations.

Input: [1,1,2]             Output: [ [1,1,2], [1,2,1], [2,1,1] ]

解:

    這道題其實可以用上邊一樣的方式完成(一模一樣的程式碼= =)。

60、Permutation Sequence

    這道題就是完全的康託展開,給一個數 n,返回1到n這n個數全排列的第 k 個。按照上邊的思路,我的程式碼如下:

string getPermutation(int n, int k)
{
    string res;
    vector<bool> used(10, false); // 0-9 這10個數是否被用過,0其實只用來佔位
    k -= 1;
    if(n <= 1)
        return "1";
    int facn = 1, tmpn = n - 1;
    while(tmpn != 1)
        facn *= tmpn--;
    tmpn = n;
    while(n-- != 1)
    {
        int quotient = k / facn, remainder = k % facn;   // 商 和 餘數
        int cnt = 0, i = 1;
        while(i <= 9)
        {
            if(used[i] == false)
                ++cnt;
            if(cnt == quotient + 1)
                break;         
            ++i;
        }
        res += char(i + '0');
        used[i] = true;
        facn /= n;
        k = remainder;
    }
    for(int i = 1; i <= tmpn; i++)
        if(used[i] == false)
        {
            res += char(i + '0');
            break;
        }           
    return res;
}

    不過由於每一位上其實只有1到9這9中可能,有很多東西是可以預先設好的,就像下邊的程式碼。但是演算法的思路是一樣的:

string getPermutation(int n, int k)
{
    const int fact[10] = { 1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880 };
    char seq[] = "123456789";
    char res[] = "000000000";

    for (int i = 1; i <= n; ++i)
    {
        auto idx = (k - 1) / fact[n - i];   // find index and copy the corresponding digit to the result
        res[i - 1] = seq[idx];

        for (int j = idx; j < n; ++j)      // erase seq[idx] by writing over it
            seq[j] = seq[j + 1];

        k -= idx * fact[n - i];            // reduce k
    }
    return string(res, res + n);
}

77、Combinations

Given two integers n and k, return all possible combinations of k numbers out of 1 ... n.

Input: n = 4, k = 2                  Output: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]

解:

    給出兩個數 n 和 k,返回在 1 到 n 這n個數中任意 k 個數組成的所有排列組合。

    我的方式是深度優先遍歷,比如n=5,k=3,那麼就是從123遍歷到345,vector長度達到 k 就push到結果vector中。

void dfs(vector<vector<int> >& res, const int n, const int k, int start, vector<int>& comb)
{
    if(k == 0)
    {
        res.push_back(comb);
        return;
    }
    for(int i = start; i <= n; i++) // 如果從i=1開始,321這樣的不是遞增的排列也會遍歷到
    {
        comb.push_back(i);
        dfs(res, n, k - 1, i + 1, comb);
        comb.pop_back();
    } 
}

vector<vector<int>> combine(int n, int k)
{
    vector<vector<int> > res;
    vector<int> comb;
    int start = 1;
    dfs(res, n, k, 1, comb);
    return res;
}

    還有一種不適用遞迴的方法,比如n=5,k=3,也是從123遍歷到345,但是是一點一點加的,先是到了123,push,然後124,push,125,push,到了126,發現6這個數 > n - k + 1 + i,第三位置上的數已經全遍歷完了,也就是12開頭的都已經push到結果中了,那麼就開始遍歷13開頭的(所有這種方式是會遍歷126這種不可能出現的數的,上邊的遞迴的演算法不會,但是遞迴還是比這種慢)。cur[i] > n - k + 1 + i 這個判斷理解了,就很好理解了。(比如n=5,k=3,那麼所有結果中,最座標最大就是3,第二位最大就是4,第三位最大就是5,所有數中最大的數一定是345)。

vector<vector<int>> combine(int n, int k)
{
    vector<vector<int>> res;
    vector<int> cur(k, 0);
    int i = 0;
    while (i >= 0)
    {
        cur[i]++;
        if (cur[i] > n - k + 1 + i) // n - k + 1 是最左邊位置,也就是第0位能取到的最大值
            --i;
        else if (i == k - 1)       // 最後一位
            res.push_back(cur);
        else
        {
            i++;
            cur[i] = cur[i - 1];
        }
    }
    return res;
}