当前位置: 首页 > article >正文

LeetCode 60. 排列序列【数学,逆康托展开】困难

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

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

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

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

给定 n 和 k,返回第 k 个排列。

示例 1:

输入:n = 3, k = 3
输出:"213"

示例 2:

输入:n = 4, k = 9
输出:"2314"

示例 3:

输入:n = 3, k = 1
输出:"123"

提示:

  • 1 <= n <= 9
  • 1 <= k <= n!

解法 逆康托展开

要想解决本题,首先需要了解一个简单的结论:对于 n n n 个不同的元素(例如数 1 , 2 , ⋯   , n 1, 2, \cdots, n 1,2,,n ),它们可以组成的排列总数目为 n ! n! n!

对于给定的 n n n k k k ,不妨从左往右确定第 k k k 个排列中的每一个位置上的元素到底是什么

我们首先确定排列中的首个元素 a 1 a_1 a1 。根据上述的结论,我们可以知道:

  • 1 1 1 a 1 a_1 a1 的排列一共有 ( n − 1 ) ! (n-1)! (n1)! 个;
  • 2 2 2 a 1 a_1 a1 的排列一共有 ( n − 1 ) ! (n-1)! (n1)! 个;
  • ⋯ \cdots
  • n n n a 1 a_1 a1 的排列一共有 ( n − 1 ) ! (n-1)! (n1)! 个。

由于我们需要求出从小到大的第 k k k 个排列,因此:

  • 如果 k ≤ ( n − 1 ) ! k \leq (n-1)! k(n1)! ,我们就可以确定排列的首个元素为 1 1 1
  • 如果 ( n − 1 ) ! < k ≤ 2 ⋅ ( n − 1 ) ! (n-1)! < k \leq 2 \cdot (n-1)! (n1)!<k2(n1)! ,我们就可以确定排列的首个元素为 2 2 2
  • ⋯ \cdots
  • 如果 ( n − 1 ) ⋅ ( n − 1 ) ! < k ≤ n ⋅ ( n − 1 ) ! (n-1) \cdot (n-1)! < k \leq n \cdot (n-1)! (n1)(n1)!<kn(n1)! ,我们就可以确定排列的首个元素为 n n n

因此,第 k k k 个排列的首个元素就是:
a 1 = ⌊ k − 1 ( n − 1 ) ! ⌋ + 1 a_1 = \lfloor \frac{k-1}{(n-1)!} \rfloor + 1 a1=(n1)!k1+1
其中 ⌊ x ⌋ \lfloor x \rfloor x 表示将 x x x 向下取整。

当我们确定了 a 1 a_1 a1 后,如何使用相似的思路,确定下一个元素 a 2 a_2 a2 呢?实际上,我们考虑以 a 1 a_1 a1 为首个元素的所有排列:

  • [ 1 , n ] \ a 1 [1,n] \backslash a_1 [1,n]\a1 最小的元素为 a 2 a_2 a2 的排列一共有 ( n − 2 ) ! (n−2)! (n2)! 个;
  • [ 1 , n ] \ a 1 [1,n] \backslash a_1 [1,n]\a1 次小的元素为 a 2 a_2 a2 的排列一共有 ( n − 2 ) ! (n-2)! (n2)! 个;
  • ⋯ \cdots
  • [ 1 , n ] \ a 1 [1,n] \backslash a_1 [1,n]\a1 最大的元素为 a 2 a_2 a2 的排列一共有 ( n − 2 ) ! (n−2)! (n2)! 个;

其中 [ 1 , n ] \ a 1 [1,n] \backslash a_1 [1,n]\a1 表示包含 1 , 2 , ⋯ n 1, 2, \cdots n 1,2,n 中除去 a 1 a_1 a1 以外元素的集合。这些排列从编号 ( a 1 − 1 ) ⋅ ( n − 1 ) ! (a_1-1) \cdot (n-1)! (a11)(n1)! 开始,到 a 1 ⋅ ( n − 1 ) ! a_1 \cdot (n-1)! a1(n1)! 结束,总计 ( n − 1 ) ! (n-1)! (n1)! 个,因此 k k k 个排列实际上就对应着这其中的第
k ′ = ( k − 1 )   m o d   ( n − 1 ) ! + 1 k' = (k-1) \bmod (n-1)! + 1 k=(k1)mod(n1)!+1
个排列
。这样一来,我们就把原问题转化成了一个完全相同但规模减少 1 1 1 的子问题:求 [ 1 , n ] \ a 1 [1, n] \backslash a_1 [1,n]\a1 n − 1 n-1 n1 个元素组成的排列中,第 k ′ k' k 小的排列。

算法:

  1. 设第 k k k 个排列为 a 1 , a 2 , ⋯   , a n a_1, a_2, \cdots,a_n a1,a2,,an ,我们从左往右地确定每一个元素 a i a_i ai​ 。我们用数组 valid \textit{valid} valid 记录每一个元素是否被使用过。
  2. 我们从小到大枚举 i i i
    1. 我们已经使用过了 i − 1 i−1 i1 个元素,剩余 n − i + 1 n-i+1 ni+1 个元素未使用过,每一个元素作为 a i a_i ai 都对应着 ( n − i ) ! (n-i)! (ni)! 个排列,总计 ( n − i + 1 ) ! (n-i+1)! (ni+1)! 个排列;
    2. 因此在第 k k k 个排列中, a i a_i ai 即为剩余未使用过的元素中第 ⌊ k − 1 ( n − i ) ! ⌋ + 1 \lfloor \frac{k-1}{(n-i)!} \rfloor + 1 (ni)!k1+1 小的元素;
    3. 在确定了 a i a_i ai 后,这 n − i + 1 n-i+1 ni+1 个元素的第 k k k 个排列,就等于 a i a_i ai 之后跟着剩余 n − i n-i ni 个元素的第 ( k − 1 )   m o d   ( n − i ) ! + 1 (k-1) \bmod (n-i)! + 1 (k1)mod(ni)!+1 个排列。

在实际的代码中,我们可以首先将 k k k 的值减少 1 1 1,这样可以减少运算,降低代码出错的概率。对应上述的后两步,即为:

  • 因此在第 k k k 个排列中, a i a_i ai​ 即为剩余未使用过的元素中第 ⌊ k ( n − i ) ! ⌋ + 1 \lfloor \frac{k}{(n-i)!} \rfloor + 1 (ni)!k+1 小的元素;
  • 在确定了 a i a_i ai 后,这 n − i + 1 n-i+1 ni+1 个元素的第 k k k 个排列,就等于 a i a_i ai 之后跟着剩余 n − i n-i ni 个元素的第 k   m o d   ( n − i ) ! k \bmod (n-i)! kmod(ni)! 个排列。

实际上,这相当于我们将所有的排列从 0 0 0 开始进行编号。

class Solution {
public:
    string getPermutation(int n, int k) {
        --k;
        int factory[10] = {1};
        for (int i = 1; i < n; ++i) factory[i] = factory[i - 1] * i;
        bool vis[10] = {false};

        string ans;
        for (int i = n - 1; i >= 0; --i) {
            int num = k / factory[i];
            k = k % factory[i];
            for (int j = 1; j <= n; ++j) {
                if (!vis[j]) {
                    if (num-- == 0) {
                        ans += '0' + j;
                        vis[j] = true;
                        break;
                    }
                }
            }
        }
        return ans;
    }
};

复杂度分析:

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( n ) O(n) O(n)

http://www.kler.cn/a/150117.html

相关文章:

  • 什么是Java的线程(Thread)?
  • 使用GetX实现GetPage中间件
  • 02多线程基础知识
  • 2024年9月电子学会青少年软件编程Python等级考试(四级)真题试卷
  • Centos7.6离线安装软件
  • 适用于 c++ 的 wxWidgets框架源码编译SDK-windows篇
  • ⑤【Sorted Set】Redis常用数据类型: ZSet [使用手册]
  • WordPress更改文章分类插件
  • CH01_适应设计模式
  • 网络安全如何自学?
  • 深圳市东星制冷机电受邀莅临2024国际生物发酵展,济南与您相约
  • Spring的依赖注入,依赖注入的基本原则,依赖注入的优势
  • 【Vulnhub 靶场】【Coffee Addicts: 1】【简单-中等】【20210520】
  • 01_原理-事件循环
  • docker (简介、dcoker详细安装步骤、容器常用命令)一站打包- day01
  • [密码学]DES
  • 二维数值型数组例题2
  • k8s中批量处理Pod应用的Job和CronJob控制器介绍
  • 【机器学习 | 可视化系列】可视化系列 之 决策树可视化
  • filebeat日志收集工具
  • shiro整合redis
  • 匿名内部类(内部类) - Java
  • git-4
  • 前五年—中国十大科技进展新闻(2012年—2017年)
  • leetcode面试经典150题——30 长度最小的子数组
  • Leetcode—15.三数之和【中等】