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;
}

 

 

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LeetCode-Editor是一种在线编码工具,它提供了一个用户友好的界面编写和运行代码。在使用LeetCode-Editor时,有时候会出现乱码的问题。 乱码的原因可能是由于编码格式不兼容或者编码错误导致的。在这种情况下,我们可以尝试以下几种解决方法: 1. 检查文件编码格式:首先,我们可以检查所编辑的文件的编码格式。通常来说,常用的编码格式有UTF-8和ASCII等。我们可以将编码格式更改为正确的格式。在LeetCode-Editor中,可以通过界面设置或编辑器设置来更改编码格式。 2. 使用正确的字符集:如果乱码是由于使用了不同的字符集导致的,我们可以尝试更改使用正确的字符集。常见的字符集如Unicode或者UTF-8等。在LeetCode-Editor中,可以在编辑器中选择正确的字符集。 3. 使用合适的编辑器:有时候,乱码问题可能与LeetCode-Editor自身相关。我们可以尝试使用其他编码工具,如Text Editor、Sublime Text或者IDE,看是否能够解决乱码问题。 4. 查找特殊字符:如果乱码问题只出现在某些特殊字符上,我们可以尝试找到并替换这些字符。通过仔细检查代码,我们可以找到导致乱码的特定字符,并进行修正或替换。 总之,解决LeetCode-Editor乱码问题的方法有很多。根据具体情况,我们可以尝试更改文件编码格式、使用正确的字符集、更换编辑器或者查找并替换特殊字符等方法来解决这个问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值