60. Permutation Sequence

Leetcode第60道题,原题链接在这里:Permutation Sequence

题目描述非常简单:
对于n个数字1, 2, 3, ..., n,可以组成n!种不同的排列,我们可以对这n!个排列依次排序,例如,对于n=3时,排完序之后的这些组合依次为:

  1. "123"
  2. "132"
  3. "213"
  4. "231"
  5. "312"
  6. "321"

现在问题是:
给你一个n和k,返回这n!个有序排列项的第k项。

例如,对于n=3和k=2,你应该返回132

在讲这道题之前,先复习一下给定一个正整数n,如何求1~n共n个数字组成的所有排列。
这个显然应该用递归去做的,代码如下:

class Solution{
public:
    vector<vector<int>> permutation(int n){
        vector<int> nums;
        vector<vector<int>> res;
        for (int i = 1; i <= n; i++)
            nums.push_back(i);

        helper(res, nums, 0);
        return res;
    }

    void helper(vector<vector<int>> &res, vector<int> &nums, int start){
        if (start == nums.size()){
            res.push_back(nums);
        }

        for (int i = start; i < nums.size(); i++){
            swap(nums[i], nums[start]);
            helper(res, nums, start + 1);
            swap(nums[i], nums[start]);
        }
    }

    void swap(int &a, int &b){
        int tmp = a;
        a = b;
        b = tmp;
    }
};

算法的时间复杂度显然就是O(n!)

现在回到我们今天这道题目,我一开始想的方法是,能不能在上面这个找所有排列的基础上进行改造?

毕竟上面的算法是产生所有排列,然后我在产生的递归过程种记录一个cnt值就行了,等cnt数到k的时候我就return,避免接下来没必要的递归过程。

这种算法能不能产生正确解呢,答案为否,我们先运行一下上面那个算法,看看我们得到的所有排列:

  1. 1 2 3
  2. 1 3 2
  3. 2 1 3
  4. 2 3 1
  5. 3 2 1
  6. 3 1 2

有没有发现问题,问题就是,我们上面那种算法产生的所有排列根本就没有排好序,具体为什么会这样你可以自己调试看看。

为了解决这个问题,我对原来的算法做了一点修改,然后就得到下面的算法:

class Solution {
public:
    string getPermutation(int n, int k) {
        string str = "", res = "";
        int cnt = 0;
        bool tag = false;

        for (int i = 1; i <= n; i++) str += ('0' + i);
        perm(str, 0, cnt, k, res, tag);
        return res;
    }

    //A是要排列的数组,start、end表示对A[start]与A[end]之间的元素进行全排列
    void perm(string A, int start, int& cnt, int k, string& res, bool& tag)
    {
        if (tag) return;

        if (start == A.size() - 1) {
            cnt++;
            if (cnt == k) {
                res = A;
                tag = true;
            }
        }
        else {
            for (int i = start; i <= A.size() - 1; i++) {
                swap(A[i], A[start]);
                //A[start+1 ... end]部分排序
                sort_part(A, start+1);
                perm(A, start + 1, cnt, k, res, tag);
                swap(A[i], A[start]);
            }
        }
    }

    void sort_part(string& str, int start) {
        string left = str.substr(0, start);
        string right = str.substr(start, str.size() - left.size());
        sort(right.begin(), right.end());
        str = left + right;
    }
};

现在这个算法能不能得到正确结果呢?答案是可以的,只不过。。。

额,看来这种方法还是做了很多不必要的计算,导致计算时间非常长,当n值比较小的时候你可能还感觉不到,可是当n=9,k=296662时,这个运行时间可就非常长了:

看到没,运行时间4秒多,这不TLE才怪呢。

于是,我又进行更深刻的思考,我们上面算法到底哪里做了不必要的计算,于是我就想,如果让我去手动计算这个值,我会怎么算?

我恍然大悟,我们肯定不是一个一个列出来,然后列到296662项的时候才停下来,我们分明可以根据k=296662这个数去依次确定这个排列前面的数字是多少。

例如,当我们n=4,k=9时,我们知道,4!可以写成如下形式:
4! = 4 * 3!
于是,我们可以把 4! 个项分成4组,每组有 3! 个项

其中第0组的开头都是数字1,第1组的开头都是数字2...依次类推。
例如,第二组为:

2134
2143
2314
2341
2413
2431

然后我们看看第9是在哪一组,没错就是用(9-1)/(3!) = 1注意下标从0开始
既然是第1组,那第9项排列的第一个数字自然就是2呀,所有其他数字开头的排列我们都不需要考虑了。这样,我们只需要在第1组中去找我们需要的那一项了。

现在问题来了,第9项是第1组的第几项,由于所有的排列项都是排好序的,于是我们自然就知道,它是在第二组的第(9-1)%(3!) = 2项。注意下标从0开始

此时,问题规模由原来的 4! 降低为了 3!。

接下来如何思考?这里才是问题的关键,很多人可能觉得,问题由n!降低成(n-1)!就可以了,接下来暴力搜索就行了,如果你只理解到这一层,那么你并没有彻底理解这个问题。

如果你再深入思考一下,你会发现,我们现在遇到的问题其实和原问题是同一个问题,只不过n由原来的4变成了3,k由原来的9变成了2。

那么你自然会发现,这是一个递归问题,我们接下来需要往下递归,直到问题规模变成1为止。

如果你想到了这里,那么恭喜你,这道问题的解法应该就是这样(我觉得这应该是复杂度最低的解法)。

这个复杂度是多少呢,你会发现,每递归一层,我们就能确定当前位置的一个数字,我们最后要确定n个数字,那么就应该要递归n次,所以复杂度应该是O(n),而且我们发现,这个复杂度竟然跟k无关!

想一想我们之前那种很暴力的做法,是不是k越大,算法所花时间越多?比方说k=1肯定不会超时,而k=296662时就超时了。

最后看一下我写的代码:

class Solution {
public:
    string getPermutation(int n, int k) {
        string res;
        vector<int> nums;
        for (int i = 1; i <= n; i++) nums.push_back(i);
        helper(nums, n, k-1, res);//下标从0开始
        return res;
    }

    void helper(vector<int>& nums, int n, int k, string& res) {
        if (res.size() == nums.size()) return;

        int fac = factorial(n - 1);
        int idx = k / fac, cnt = -1;
        for (int i = 0; i < nums.size(); i++) {
            if (nums[i] == -1) continue;
            cnt++;
            if (cnt == idx) {
                res.push_back('0' + nums[i]);
                nums[i] = -1;
                break;
            }
        }

        k = k % fac;
        helper(nums, n - 1, k, res);
    }

    int factorial(int n) {//计算n的阶乘
        int res = 1;
        for (int i = 1; i <= n; i++) res *= i;
        return res;
    }
};

提交一下,AC通过,爽的一批!

在本地vs也跑了一下,运行时间0秒

芜湖,爽,今天我就是leetcode最快的男人!🤭

posted @ 2020-12-26 20:44  nullxjx  阅读(113)  评论(0编辑  收藏  举报