第Q個全排序(逆康拓展開)

  • 題面描述
    現有一個長度爲n的集合S,集合S裏的元素爲 [1,2,3……n]。按照高中數學的知識,我們顯然可以知道,集合裏的元素的排列共有 n! 種。現在給你集合長度n以及一個正整數 Q,請你找出集合元素組成的第Q個的排列。

  • 輸入
    輸入數據由多組測試樣例組成,每組測試樣例第一行分別輸入兩個正整數n ( 1 <= n <= 9 ),Q( 1 <= Q <= n! )

  • 輸出
    輸出第Q個的排列,元素之間沒有空格

  • 樣例輸入

2 2
5 10

  • 樣例輸出

21
13452

第一發超時代碼是暴力dfs找第Q個排列, 毫無疑問的超時了,這時我想起 next_permutation() 函數,於是有了以下代碼:

#include <bits/stdc++.h>

using namespace std;
int k[15];

int main()
{
	int n, m;
	while(~scanf("%d%d", &n, &m))
	{
		for(int i = 0; i < n; i++)
		{
			k[i] = i + 1;
		}
		int t = 1;
		do
		{
			if(t == m)
        	{
        		for(int i = 0; i < n; i++)
        			printf("%d", k[i]);
        		printf("\n");
        		break;
			}
			t++;
		}while(next_permutation(k, k + n));
	}
	return 0;
}

沒想到出題人早就料到了這種情況,又超時了,無奈下上網查到了康拓展開/逆康拓展開。

簡述

康託展開是一個全排列到一個自然數的雙射,常用於構建hash表時的空間壓縮。設有n個數(1,2,3,4,…,n),可以有組成不同(n!種)的排列組合,康託展開表示的就是是當前排列組合在n個不同元素的全排列中的名次。

原理

X=a[n] * (n-1)!+a[n-1] * (n-2)!+…+a[i] * (i-1)!+…+a[1]*0!

排列組合 名次 康託展開
123 1 0 * 2! + 0 * 1! + 0 * 0!
132 2 0 * 2! + 1 * 1! + 0 * 0!
213 3 1 * 2! + 0 * 1! + 0 * 0!
231 4 1 * 2! + 1 * 1! + 0 * 0!
312 5 2 * 2! + 0 * 1! + 0 * 0!
321 6 2 * 2! + 1 * 1! + 0 * 0!

舉個例子
在(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。

具體代碼實現如下:(假設排列數小於10個)

int k[10] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};  //階乘
int cantor(int *a, int q)
{
	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 += k[n - i - 1] * smaller;  // 康託展開累加
	}
	return x;  // 康託展開值
}

逆康託展開

康託展開是一個全排列到一個自然數的雙射,因此是可逆的。即對於上述例子,在(1,2,3,4,5)給出61可以算出起排列組合爲 34152。由上述的計算過程可以容易的逆推回來,具體過程如下:

  • 用 61 / 4! = 2餘13,說明a[5]=2,說明比首位小的數有2個,所以首位爲3。
  • 用 13 / 3! = 2餘1,說明a[4]=2,說明在第二位之後小於第二位的數有2個,所以第二位爲4。
  • 用 1 / 2! = 0餘1,說明a[3]=0,說明在第三位之後沒有小於第三位的數,所以第三位爲1。
  • 用 1 / 1! = 1餘0,說明a[2]=1,說明在第二位之後小於第四位的數有1個,所以第四位爲5。
  • 最後一位自然就是剩下的數2。
  • 通過以上分析,所求排列組合爲 34152。

具體代碼實現如下:(假設排列數小於10個)

int k[15] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};  // 階乘

//康託展開逆運算
void decantor(int x, int n)
{
	vector <int> ve;  // 存放當前可選數
	vector <int> a;  // 所求排列組合
	for(int i = 1; i <= n; i++)
		ve.push_back(i);
	for(int i = n; i >= 1; i--)
    {
        int r = x % k[i - 1];
        int t = x / k[i - 1];
        x = r;
        sort(ve.begin(), ve.end());  // 從小到大排序 
        a.push_back(ve[t]);  // 剩餘數裏第t+1個數爲當前位
        ve.erase(ve.begin() + t);  // 移除選做當前位的數
    }
}

那麼看懂了康拓/逆康拓展開,也就能輕(shi)輕(fen)鬆(fei)鬆(li)的解出這道題,以下爲ac代碼:

#include <bits/stdc++.h>

using namespace std;
int k[15] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};
void decantor(int x, int n)
{
	vector <int> ve;
	vector <int> a;
	for(int i = 1; i <= n; i++)
		ve.push_back(i);
	for(int i = n; i >= 1; i--)
    {
        int r = x % k[i - 1];
        int t = x / k[i - 1];
        x = r;
        sort(ve.begin(), ve.end());
        a.push_back(ve[t]);
        ve.erase(ve.begin() + t);
    }
    for(int i = 0; i < n; i++)
    	printf("%d", a[i]);
    printf("\n");
}

int main()
{
	int n, m;
	while(~scanf("%d%d", &n, &m))
	{
		decantor(m - 1, n);  // 注意這裏m-1纔是康拓值
	}
	return 0;
}

應用

  • 給定一個自然數集合組合一個全排列,所其中的一個排列組合在全排列中從小到大排第幾位。
    在上述例子中,在(1,2,3,4,5)的全排列中,34152的排列組合排在第62位。
  • 反過來,就是逆康託展開,求在一個全排列中,從小到大的第n個全排列是多少。
    比如求在(1,2,3,4,5)的全排列中,第62個排列組合是34152。[注意具體計算中,要先 -1 纔是其康託展開的值。
  • 另外康託展開也是一個數組到一個數的映射,因此也是可用於hash,用於空間壓縮。比如在保存一個序列,我們可能需要開一個數組,如果能夠把它映射成一個自然數, 則只需要保存一個整數,大大壓縮空間。比如八數碼問題。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章