60-第k个排列

60-第k个排列

力扣60

https://leetcode-cn.com/problems/permutation-sequence/

这题虽然是一道回溯题,但如果只是仅仅简单的回溯一下,会超时,经过剪枝和优化可以到13%。此题可以数学优化,还是有点挑战性的。再说了,我相信很多人和我一样看到题目第一个想到的肯定是先计算出开头数字,但是写起来还是回溯…


1. 暴力回溯+剪枝

先来看看最low的回溯,这份提交是会超时的,而且还做了剪枝(到k个后面的就不继续了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution {
public String getPermutation(int n, int k) {
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = i + 1;
}
backtrace(nums, new StringBuilder(), k);
return res.get(res.size() - 1);
}

private List<String> res = new ArrayList<>();
private boolean max = false;

public void backtrace(int[] nums, StringBuilder track, int k) {
//结束条件
if (track.length() == nums.length) {
res.add(track.toString());
if (res.size() == k) max = true;
return;
}
for (int i = 0; i < nums.length; i++) {
//剪枝
if (max) return ;
if (track.indexOf(String.valueOf(nums[i])) != -1) continue;
//做选择
track.append(nums[i]);
//递归
backtrace(nums, track, k);
//撤销
track.deleteCharAt(track.length() - 1);
}
}
}


2. 提前结束条件优化

可以看到,剪枝也超时,分析一下,可以看出track.indexOf这句可能会影响性能,我们修改为标记数组,查找的时候O(1),并且做了空间上的优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public String getPermutation(int n, int k) {
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = i + 1;
}
backtrace2(nums, new boolean[n], new StringBuilder(), k);
return res;
}

private String res = ""; //优化一下空间,结果只存一个
private int num = 0;
public void backtrace2(int[] nums, boolean[] flag, StringBuilder track, int k){
//结束条件
if (track.length() == nums.length){
res = track.toString();
num++;
return ;
}
for (int i = 0; i < nums.length; i++) {
//剪枝
if (k == num) return ;
if (flag[i]) continue;
//做选择
track.append(nums[i]);
flag[i] = true;
//递归
backtrace2(nums, flag, track, k);
//撤销
track.deleteCharAt(track.length() - 1);
flag[i] = false;
}
}
}


3. 数学优化

事实上写起来并不难,而且我相信大多数人第一眼看到题目想到的应该是这个,不过写的还是回溯(因为笔试需要时间,一般不敢冒险,选择了自己熟悉的方法…)

3.1 总体思路和过程

以正常人的思维,手写可能的排列的时候一定是这样的顺序:

先固定第一位是1,后三位按顺序排列

234

243

324

342

423

432

也就是说1开头,那么剩下3位进行排列,共需A(3, 3) = 3! = 6

那么第一位想要为2必须先经历完1开头的,且2开头的也有6条数据

同样的,第一位想要是3,那么需经历开头1和2,那么也就是说,如果n位,那么第一位需要变成a1,那么前面有(n - 1)! * (a1 - 1)


反过来,已知k = 9,那么k / 6 = 1余3,说明第一位肯定已经超过了1的那6条,因此第一位是2

接下来确定第二位,那么变成了子问题:此时可供选择的选择列表把2去除了变为134,k为余数3,说人话就是:第一位的2已经确定了,在剩下的134中找到第3条数据。

当然,程序的话就不用再写了,这是子问题,之前已经写好了,重新遍历上面的即可


3.2 java实现步骤

下面是java代码,我先贴完整代码(可以不看先,先看讲解),再一步步讲解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class six_zero_k个排列_other {

@Test
public void test(){
String res = getPermutation(4, 9);
System.out.println(res);
}

public String getPermutation(int n, int k) {
//生成排列数列表 paiLie[0] = 0! paiLie[1] = 1! 由排列公式可得n! = n * (n - 1)!
int[] paiLie = new int[n + 1];
List<Integer> nums = new LinkedList<>(); //选择列表,也就是1234,每次被选择一位后这里就会减少一位
paiLie[0] = 1;
for (int i = 1; i <= n; i++) {
paiLie[i] = paiLie[i - 1] * i; //n! = n * (n - 1)!
nums.add(i);
}
//准备完毕,开始从左到右找到每一位
String res = "";
k--; //这个可以回过头来看,因为下面k的计算是跟跳过的个数onceSkip相关的,onceSkip是下标,因此要先-1成下标
while (nums.size() != 0){
int onceSkip = paiLie[(nums.size() - 1)]; //也就是1开头的有6条,这就是这个6,当然第二位的时候就是(n - 1)!了
int index = k / onceSkip;
res += nums.get(index);
//后面重复,nums变为少了当前加的这个值,k也会减去跳过的个数
nums.remove(index);
k -= index * onceSkip; //总共跳过的条数
}
return res;
}
}

别急,一步步来做:

  1. paiLie[]:先要生成上面说的排列数列表,因为每次都是n! = (n - 1)! * n,需要前一个阶乘作为基础,所以一开始生成比较好,而不是临时算。
  2. nums:同时还要生成上面说的选择列表,也就是一开始为1234,第一轮被选出2之后剩下134的这个东西
1
2
3
4
5
6
7
8
9
10
11
public String getPermutation(int n, int k) {
//生成排列数列表 paiLie[0] = 0! paiLie[1] = 1! 由排列公式可得n! = n * (n - 1)!
int[] paiLie = new int[n + 1];
List<Integer> nums = new LinkedList<>(); //选择列表,第一轮为1234
paiLie[0] = 1;
for (int i = 1; i <= n; i++) {
paiLie[i] = paiLie[i - 1] * i;
nums.add(i);
}
return "";
}

打个断点看看效果

完美(笑)


准备工作完成,下面看我们要完成的计算公式

  1. 总跳过数:之前说了第一位需要变成a1,那么前面有(n - 1)! * (a1 - 1),(n - 1)!也就是开头为1的这一组会有多少条,起名onceSkip,对应的代码就是onceSkip = paiLie[(nums.size() - 1)]没错吧,至于a1 - 1,也就是跳过了多少组,起名index,也就是index = k / onceSkip

打个断点看看:res已经将第一位2加进来了

需要注意的是题目给的k是现实的k,但是我们k计算相关的是nums.get(index),用的是下标,因此一开始做了一次k - 1,自己试下就知道了。


  1. 本轮结束第一位已加入res,开始确定第二位,子问题,进入前修改一下选择列表nums:去除已加入的2,修改k:k代表的是剩余几条数据,因此要减去已经跳过的index * onceSkip

看代码

就完成了,代码其实非常简单,就是计算别搞错了。

力扣2ms,官方的代码1ms,但是思路一样的


其他回溯系列题目可以点击这里

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×