60. Permutation Sequence
Leetcode第60道题,原题链接在这里:Permutation Sequence
题目描述非常简单:
对于n个数字1, 2, 3, ..., n,可以组成n!种不同的排列,我们可以对这n!个排列依次排序,例如,对于n=3时,排完序之后的这些组合依次为:
"123"
"132"
"213"
"231"
"312"
"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 2 3
1 3 2
2 1 3
2 3 1
3 2 1
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最快的男人!🤭