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”
思路:深搜+剪枝
由于是输入有序数据:[1..n]
,那么我们深搜计算[1..n]
的全部全排列,会发现是按照字典序排列的,则第k
个叶子节点即为所求。
单纯的深搜做了许多无用功,搜索了许多无用节点。可以利用排列的数学性质进行剪枝,以n = 4为例:
定义一个函数f(n,i) = (n-i)!
表示[1..n]
的序列中,前i
个位置固定的情况下存在的排列数,也就是上图搜索树中每个根节点对应的叶子节点数。
实际上f(n,i)就是(n-i)!,编码时只需预计算1-n的阶乘即可。
如确定第一位为1时,有f(4,1) = 6
种排列:234,243,324,342,423,432
。
如确定第一位为2时,有f(4,1) = 6
种排列:134,143,314,341,413,431
。
如确定第一位为3时,有f(4,1) = 6
种排列:124,142,214,241,412,421
。
如确定第一位为4时,有f(4,1) = 6
种排列:123,132,213,231,312,321
。
由于第一位的可选元素有4个,所以有4棵子树。(第i位有n-i+1个可选元素)
…
我们的目标是找到k
个叶子节点,根据f(n,i)
我们确定了当前根节点下的叶子节点数,那么根据f(n,i)
与k
的关系就能确定k
所在的子树,从而避免搜索无用子树。找到了目标子树也就确定了第i位元素。
如果
f
(
n
,
i
)
>
k
f(n,i)>k
f(n,i)>k,说明k
在第一棵子树中;
如果
f
(
n
,
i
)
<
k
<
2
f
(
n
,
i
)
f(n,i)<k<2f(n,i)
f(n,i)<k<2f(n,i),说明k
不在第一棵子树,而在第二棵子树的第
k
−
f
(
n
,
i
)
k-f(n,i)
k−f(n,i)个叶子节点;
…;
如果
(
n
−
i
)
∗
f
(
n
,
i
)
<
k
<
(
n
−
i
+
1
)
∗
f
(
n
,
i
)
(n-i)*f(n,i)<k<(n-i+1)*f(n,i)
(n−i)∗f(n,i)<k<(n−i+1)∗f(n,i),说明k
在第n-i+1
棵子树中的第
k
−
(
n
−
i
)
∗
f
(
n
,
i
)
k - (n-i)*f(n,i)
k−(n−i)∗f(n,i)个叶子节点;
如果
k
>
(
n
−
i
+
1
)
∗
f
(
n
,
i
)
k>(n-i+1)*f(n,i)
k>(n−i+1)∗f(n,i)说明不存在第k
个叶子节点。
实际编码中,我们使用辗转相减法更新k的值,若k<f(n,i)
,则k -= f(n,i)
。
确定了是第
m
(
1
<
m
≤
n
−
i
+
1
)
m(1<m\leq n-i+1)
m(1<m≤n−i+1)棵子树,也就确定了第i
位元素是可选元素中字典序为m
的元素,那么接下来就是深搜确定第i+1
位的元素。如此递归直到找到叶子节点,即为所求。
如何快速找到可选元素中字典序为
m
的元素?
如果我们在确定当前元素后,都能保证可选元素保持字典序,那么可选元素中字典序为m的元素就是
nums[i+m]
。
办法就是使用依次交换的方式确定当前位置的元素。例如确定第一位元素:
[1,2,3,4]
,如果1不是目标元素,就交换1,2:[2,1,3,4]
如果2不是目标元素,就交换2,3:[3,1,2,4]
如果3不是目标元素,就交换3,4:[4,1,2,3]
可以发现在确定第一位元素之后,后面3位元素仍然保持字典序。
public class Solution {
private List<List<Integer>> permutations = new ArrayList<>();
private void swap(int[] nums, int i, int j) {
nums[i] = nums[i] ^ nums[j] ^ (nums[j] = nums[i]);
}
private int[] factorial = new int[11];
private void calculateFactorial(int n) {
factorial[1] = 1;
for (int i = 2; i <= n; i++) {
factorial[i] = factorial[i - 1] * i;
}
}
/**
* 判断是否有第k个排列,若有返回true,否则返回false
*/
private boolean kthPermutation(int[] nums, int lo, int hi, int k) {
if (k == 1) { return true;}
boolean flag = false;
for (int i = lo; i < hi; i++) { // 当前有hi - lo棵子树
int res = hi - lo - 1; // 当前子树有factorial[res]个叶子节点
if (factorial[res] >= k) // 第k个叶子节点在当前子树中
flag = kthPermutation(nums, lo + 1, hi, k);
else if (factorial[res] < k) {// 如果factorial[res] < k,说明k不在当前子树
if (i + 1 == hi) break; // k不在最后一棵子树中
swap(nums, lo, i + 1); // 交换nums[lo]与字典序为i+1的可选元素
k -= factorial[res]; // 来到下一棵子树。
}
if (flag) return true;
}
return false;
}
public String getPermutation(int n, int k) {
int[] nums = new int[]{1,1,2,2};
// for (int i = 0; i < n; i++) {nums[i] = i + 1;}
calculateFactorial(n);
System.out.println(kthPermutation(nums, 0, n, k));
StringBuilder ans = new StringBuilder();
for (int i = 0; i < n; i++) { ans.append(nums[i]); }
return ans.toString();
}
}