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%的用户