leetcode解题报告——60. 第k个排列

60. 第k个排列【中等】

给出集合 [1,2,3,…,n],其所有元素共有 n!种排列。

按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:

  1. “123”
  2. “132”
  3. “213”
  4. “231”
  5. “312”
  6. “321”

给定 nk,返回第 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) kf(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) (ni)f(n,i)<k<(ni+1)f(n,i),说明k在第n-i+1棵子树中的第 k − ( n − i ) ∗ f ( n , i ) k - (n-i)*f(n,i) k(ni)f(n,i)个叶子节点;

如果 k > ( n − i + 1 ) ∗ f ( n , i ) k>(n-i+1)*f(n,i) k>(ni+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<mni+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();
        }
    }



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值