leetcode 60. 第k个排列

问题描述

给出集合 [1,2,3,…,n],其所有元素共有 n! 种排列。
按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:
"123"
"132"
"213"
"231"
"312"
"321"
给定 n 和 k,返回第 k 个排列。
说明:
给定 n 的范围是 [1, 9]。
给定 k 的范围是[1,  n!]。
示例 1:
输入: n = 3, k = 3
输出: "213"
示例 2:
输入: n = 4, k = 9
输出: "2314"

问题分析

第一种方法类似于全排列,但是该方法时间超时,需要剪枝。

class Solution {
public:
    string getPermutation(int n, int k) {
        vector<string> ans;
        string path;
        vector<bool> flag(n,0);
        backtrack(ans,path,n,0,flag);
        return ans[k-1];
    }
    void backtrack(vector<string>&ans,string&path,int n,int level,vector<bool>&flag)
    {
        if(level == n){
            ans.push_back(path);
            return;
        }
        for(int i = 0; i < n; i++)
        {
            if(!flag[i])
            {
                path += i+'1';
                flag[i] = 1;
                backtrack(ans,path,n,level+1,flag);
                path.pop_back();
                flag[i] = 0;
            }
        }
    }
};

因为我们只要第k个元素,因此不必全部回溯,因此可以进行剪枝:

class Solution {
public:
    string getPermutation(int n, int k) {
        string path,str;
        vector<bool> flag(n,0);
        int num = 0;
        backtrack(path,n,0,flag,k,num,str);
        return str;
    }
    void backtrack(string&path,int n,int level,vector<bool>&flag,int k,int& num,string& str)
    {
        if(level == n){
            ++num;
            if(num == k){
                str = path;
                return;
            }
            return;
        }
        for(int i = 0; i < n; i++)
        {
            if(!flag[i])
            {
                path += i+'1';
                flag[i] = 1;
                backtrack(path,n,level+1,flag,k,num,str);
                if (!str.empty())
                    return;
                path.pop_back();
                flag[i] = 0;
            }
        }
    }
};

但是效率依然感人:

执行用时 :1940 ms, 在所有 C++ 提交中击败了5.15%的用户
内存消耗 :8.2 MB, 在所有 C++ 提交中击败了56.94%的用户

第三种方法是康拓展开法,参考自博客,思路如下:

首先我们要知道当n = 3时,其排列组合共有3! = 6种,当n = 4时,其排列组合共有4! = 24种,我们就以n = 4, k = 17的情况来分析,所有排列组合情况如下:

1234
1243
1324
1342
1423
1432
2134
2143
2314 
2341
2413
2431
3124
3142
3214
3241
3412 <--- k = 17
3421
4123
4132
4213
4231
4312
4321

我们可以发现,每一位上1,2,3,4分别都出现了6次,当第一位上的数字确定了,后面三位上每个数字都出现了2次,当第二位也确定了,后面的数字都只出现了1次,当第三位确定了,那么第四位上的数字也只能出现一次,那么下面我们来看k = 17这种情况的每位数字如何确定,由于k = 17是转化为数组下标为16:

最高位可取1,2,3,4中的一个,每个数字出现3!= 6次,所以k = 16的第一位数字的下标为16 / 6 = 2,即3被取出
第二位此时从1,2,4中取一个,k = 16时,k' = 16 % (3!) = 4,而剩下的每个数字出现2!= 2次,所以第二数字的下标为4 / 2 = 2,即4被取出
第三位此时从1,2中去一个,k' = 4时,k'' = 4 % (2!) = 0,而剩下的每个数字出现1!= 1次,所以第三个数字的下标为 0 / 1 = 0,即1被取出
第四位是从2中取一个,k'' = 0时,k''' = 0 % (1!) = 0,而剩下的每个数字出现0!= 1次,所以第四个数字的下标为0 / 1= 0,即2被取出

那么我们就可以找出规律了
a1 = k / (n - 1)!
k1 = k % (n - 1)!

a2 = k1 / (n - 2)!
k2 = k1 % (n - 2)!
...

an-1 = kn-2 / 1!
kn-1 = kn-2 % 1!

an = kn-1 / 0!
kn = kn-1 % 0!

因此代码可以写为:

class Solution {
public:
    string getPermutation(int n, int k) {
        string str;
        int i;
        string num = "123456789";
        vector<int> factor(n, 1);
        for (i = 1; i < n; ++i) factor[i] = factor[i - 1] * i;
        --k;
        for(i = n; i > 0; i--)
        {
            int j = k / factor[i-1];
            str += num[j];
            k %= factor[i-1];   
            num.erase(j,1);   
        }
        return str;
    }
};

erase函数的原型如下:
(1)string& erase ( size_t pos = 0, size_t n = npos );
(2)iterator erase ( iterator position );
(3)iterator erase ( iterator first, iterator last );
也就是说有三种用法:
(1)erase(pos,n); 删除从pos开始的n个字符,比如erase(0,1)就是删除第一个字符
(2)erase(position);删除position处的一个字符(position是个string类型的迭代器)
(3)erase(first,last);删除从first到last之间的字符(first和last都是迭代器)
结果如下:

执行用时 :0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗 :8.1 MB, 在所有 C++ 提交中击败了87.18%的用户
posted @ 2020-02-14 11:11  曲径通霄  阅读(157)  评论(0编辑  收藏  举报