60. 排列序列

60. 排列序列

题目描述

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

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

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

给定 nk,返回第 k 个排列。

示例 1:

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

示例 2:

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

示例 3:

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

提示:

  • 1 ≤ n ≤ 9 1 \le n \le 9 1n9
  • 1 ≤ k ≤ n ! 1 \le k \le n! 1kn!

题解:

法一:

参考 下一个排列 ,直接求出第 1~k 个排列,返回结果。

时间复杂度: O ( n ∗ k ) O(n*k) O(nk)

额外空间复杂度: O ( n ) O(n) O(n)

法一代码:

class Solution {
    
    
public:
    string getPermutation(int n, int k) {
    
    
        string ret = "";
        for ( int i = 1; i <= n; ++i )
            ret.push_back( i + '0' );
        int t, j;
        while ( --k ) {
    
    
            t = n - 1;
            while ( t > 0 && ret[t - 1] > ret[t] ) --t;
            if ( --t < 0 ) break;
            j = n - 1;
            while ( j > t && ret[j] < ret[t] ) --j;
            swap( ret[t], ret[j] );
            reverse( ret.begin() + t + 1, ret.end() );
        }
        return ret;
    }
};
/*
时间:140ms,击败:18.61%
内存:5.8MB,击败:96.84%
*/

速度不敢恭维。。。

法二:

模拟。

其实没有必要把第 1~k 的每个排列都求出来,只要能从左往右依次确认第 k 个排列的每一位元素即可。

举个例子,假设 n = 4, k = 8,列出以 1,2,3,4 开头的排列:

  1. 1 + [2, 3, 4的排列],(4 - 1)! = 6种
  2. 2 + [1, 3, 4的排列],(4 - 1)! = 6种
  3. 3 + [1, 2, 4的排列],(4 - 1)! = 6种
  4. 4 + [1, 2, 3的排列],(4 - 1)! = 6种

可以确定 k = 8 在第二组中,第一位可以确定是 2

在排除掉第一组的 6 个排列后,k 变为 k' = k - 6 = 2,对剩下的元素继续分组:

  1. 21 + [3, 4的排列],(3 - 1)! = 2种
  2. 23 + [1, 4的排列],(3 - 1)! = 2种
  3. 24 + [1, 3的排列],(3 - 1)! = 2种

由于 k' = 2 在第一组,可以确定第二位为 1

由于在第一组,前面排除掉 0 个元素,k' 变为 k'' = k' - 0 = 2,对剩下的元素继续分组:

  1. 213 + [4],(2 - 1)! = 1种
  2. 214 + [3],(2 - 1)! = 1种

可以确定 k'' = 2 在第二组中,所以第三位可以确定为 4

剩下的 k''' = k'' - 1 = 1 ,只剩下元素 3 了,没必要继续划分。所以,第 k = 8 个排列就是 2 1 4 3

总结一下规律,假设当前推测的为第 i 个元素:

所以第 k = 8 个排列就是:[2, 1, 4, 3]

所以可以这样去做:

设第 k 个排列为 a − 1 , a 2 , . . . , a n a-1, a_2, ..., a_n a1,a2,...,an,从左往右依次确定每个元素 a i a_i ai,同时需要记录哪些元素已经被使用,从小到大枚举 i

  • 前面已经使用了 i - 1 个元素,还剩下 n - i + 1 个元素未使用,每个元素作为当前的 a i a_i ai 都有 (n - i)! 个排列
  • 在第 k 个排列中, a i a_i ai 为剩余未使用元素中的第 k − 1 ( n − i ) ! + 1 \frac{k-1}{(n - i)!} + 1 (ni)!k1+1 小元素
  • 确定了 a i a_i ai 后,当前 n - i + 1 个元素的第 k 个排列变成剩下的 n - i 个元素的第 $ (k - 1) % (n - i)! + 1$ 个排列

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

额外空间复杂度: O ( n ) O(n) O(n)

class Solution {
    
    
public:
    string getPermutation(int n, int k) {
    
    
        vector<int> fac( n + 1 );
        fac[0] = fac[1] = 1;
        for ( int i = 2; i <= n; ++i )
            fac[i] = fac[i - 1] * i;
        vector<bool> vis( n + 1, true );
        string ret = "";
        for ( int i = 1; i <= n; ++i ) {
    
    
            int t = (k - 1) / fac[n - i] + 1;
            for ( int j = 1; j <= n; ++j ) {
    
    
                t -= vis[j];
                if ( !t ) {
    
    
                    vis[j] = false;
                    ret.push_back( j + '0' );
                    break;
                }
            }
            k = (k - 1) % fac[n - i] + 1;
        }
        return ret;
    }
};
/*
时间:0ms,击败:100.00%
内存:5.7MB,击败:99.09%
*/
法三:

逆康托展开。

首先介绍一下 康托展开

康托展开是一个全排列到一个自然数的双射,常用于构建hash表时的空间压缩。设有 n个数1,2,3,4,…,n,可以组成不同 n! 种排列组合,康托展开表示的就是在 n 个不同元素的全排列中, 比当前排列组合小的个数,那么也可以表示当前排列组合在 n 个不同元素的全排列中的名次(当前的名次 = 比当前排列组合小的个数 + 1)。

其原理就是 x = a[n]*(n - 1)! + a[n - 1]*(n - 2)! + ... + a[i]*(i - 1)! + ... + a[1] * 0!

其中, a[i] 表示 i+1~n 中,比 nums[i] 小的元素个数,所以 0 <= a[i] < n ,康托展开公式就是这玩意。

例如有3个数(1,2,3),则其排列组合及其相应的康托展开值如下:

排列组合 名次 康托展开
123 1 0 * 2! + 0 * 1! + 0 * 0! 0
132 2 0 * 2! + 1 * 1! + 0 * 0! 1
213 3 1 * 2! + 0 * 1! + 0 * 0! 2
231 4 1 * 2! + 1 * 1! + 0 * 0! 3
312 5 2 * 2! + 0 * 1! + 0 * 0! 4
321 6 2 * 2! + 1 * 1! + 0 * 0! 5

比如其中的 231

  • 想要计算排在它前面的排列组合数目 123,132,213,则可以转化为计算比首位小即小于2的所有排列「1 * 2!」,首位相等为2并且第二位小于3的所有排列「1 * 1!」,前两位相等为 23 并且第三位小于1的所有排列(0 * 0!)的和即可,康托展开为:1 * 2!+ 1 * 1! + 0 * 0! = 3
  • 所以小于 231 的组合有 3 个,所以 231 的名次是4。

再举个例子说明,在1,2,3,4,5 5个数的排列组合中,计算 34152 的康托展开值:

  • 首位是3,则小于3的数有两个,为1和2,a[5]=2,则首位小于3的所有排列组合为 a[5]*(5-1)!
  • 第二位是4,则小于4的数有两个,为1和2,注意这里3并不能算,因为3已经在第一位,所以其实计算的是在第二位之后小于4的个数。因此a[4]=2
  • 第三位是1,则在其之后小于1的数有0个,所以a[3]=0
  • 第四位是5,则在其之后小于5的数有1个,为2,所以a[2]=1
  • 最后一位就不用计算啦,因为在它之后已经没有数了,所以a[1]固定为0
  • 根据公式:
    X = 2 * 4! + 2 * 3! + 0 * 2! + 1 * 1! + 0 * 0! = 2 * 24 + 2 * 6 + 1 = 61

所以比 34152 小的组合有61个,即 34152 是排第62。

具体代码如下:

static const int FAC[] = {
    
    1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};	// 阶乘
int cantor(int *a, int n)
{
    
    
	int x = 0;
	for (int i = 0; i < n; ++i) {
    
    
		int smaller = 0;  // 在当前位之后小于其的个数
		for (int j = i + 1; j < n; ++j) {
    
    
			if (a[j] < a[i])
				smaller++;
		}
		x += FAC[n - i - 1] * smaller; // 康托展开累加
	}
	return x;  // 康托展开值
}

注意:这里实现的算法复杂度为 O ( n 2 ) O(n^2) O(n2),实际当n很大时,内层循环计算在当前位之后小于当前位的个数可以用 线段树来处理计算,这样复杂度降为 O ( n l o g n ) O(nlogn) O(nlogn)

下面来介绍 逆康拓展开

因为 康托展开 是全排列到自然数的 双射 ,因此两者是可逆的。还以上述的例子为例,给出 61 ,求出排列,逆推回去就行:

  • 61 / 4! = 2…13,所以 a[5] = 2 ,说明比首位小的元素有两个,所以首位为 3
  • 13 / 3! = 2…1 ,所以 a[4] = 2,说明在第二位之后有两个元素小于第二位的元素,所以第二位为 4
  • 1 / 2! = 0…1,所以 a[3] = 0,说明在第三位之后没有元素比第三位小,所以第三位为 1
  • 1 / 1! = 1…0 ,所以 a[4] = 1,说明在第四位之后有一个元素比它小,所以第四位为 5
  • 最后剩下的第五位就是 2

故所求的排列为 34152

逆康拓展开代码如下:

static const int FAC[] = {
    
    1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};	// 阶乘

//康托展开逆运算
void decantor(int x, int n)
{
    
    
    vector<int> rest;  // 存放当前可选数,保证有序
    for(int i=1;i<=n;i++)
        v.push_back(i);
        
    vector<int> ans;  // 所求排列组合
    for(int i=n;i>=1;i--)
    {
    
    
        int r = x % FAC[i-1];
        int t = x / FAC[i-1];
        x = r;
        a.push_back(v[t]);      // 剩余数里第t+1个数为当前位
        v.erase(v.begin()+t);   // 移除选做当前位的数
    }
}

那么 逆康托展开 用到本题上就是:

class Solution {
    
    
public:
    string getPermutation(int n, int k) {
    
    
        vector<int> fac( n + 1 );
        fac[0] = fac[1] = 1;
        for ( int i = 2; i <= n; ++i )
            fac[i] = fac[i - 1] * i;
        vector<bool> vis( n + 1, true );
        int d;
        string ret = "";
        --k;
        for ( int i = n; i >= 1; --i ) {
    
    
            d = k / fac[i - 1];
            for ( int j = 1; j <= n; ++j ) {
    
    
                d -= vis[j];
                if ( d == -1 ) {
    
    
                    ret += j + '0';
                    vis[j] = false;
                    break;
                }
            }
            k %= fac[i - 1];
        }
        return ret;
    }
};
/*
时间:0ms,击败:100.00%
内存:5.8MB,击败:97.69%
*/

好像跟 方法二 差不多,都是从前往后依次确定每一位的值。

猜你喜欢

转载自blog.csdn.net/MIC10086/article/details/113395574
今日推荐