(數論十一)康託展開與逆康託展開
一.引出康託展開
動態規劃題有一類分支叫狀壓DP,意思就是把狀態壓縮為一個二進位制陣列,然後轉為十進位制數儲存。一般n的大小不會超過20,因為20個狀態的組合就有2^20,也就是1e6種可能。
對於一些題目,緊緊利用狀態壓縮,會發現狀態的組合數遠遠超過1e6的範圍,那時候我們沒有辦法在1s內遍歷出來,或者大到根本連陣列都開不出來的時候,一般情況下就需要用到康託展開
例如:對於一個組合 1 2 3 4 5 6 7 8,A操作可以讓其轉變為8 7 6 5 4 3 2 1,B操作可以讓其轉變為4 1 2 3 6 7 8 5,C操作可以讓其轉變為1 7 2 4 5 3 6 8
給出一個初始組合和目標組合,問由初始到目標最少的變換步驟,若多種則選字典樹最小的那種?
對於這種題,如果我們把1~8看作0~7,拿這8個數的當作一個狀態來儲存,需76543210種狀態(且裡邊有些狀態根本就不可能出現,如11111111),這樣肯定是不可行的。
如果我們利用狀態壓縮把它轉為2進位制,0為000,1為001,2為010….7為111,那麼8個數連在一起共有24位,也就是需要2^24 = 16777216個狀態進行儲存,然後縮小了7倍,但是陣列依舊太大了
這時候,我們需要考慮康託展開對狀態進行定義
二.關於康託展開
和狀壓陣列不同,康託展開陣列a[i]代表的是該序列從第i位開始到最後一位,第i位的數排第幾(排名和i都是從0開始)
舉個例子:3,5,4,1,2中:a[0] = 2,a[1] = 3, a[2] = 2,a[3] = 0, a[4] = 0
那麼3 5 4 1 2的狀態值 = a[0]✖️4! + a[1]✖️3! + a[2]✖️2! + a[3]✖️1! + a[4]✖️0! = 70
也就是說,康託展開能夠把狀態壓縮到極致(即像上邊那種沒有用過的諸如11111111等都被拋棄掉,只剩有用的狀態存在),即節省了空間也節省了時間。
三.關於康託逆展開
我們在二中得到的70可以通過康託逆展開重新得到3,5,4,1,2,方法如下:
70 / 4! = 2餘22,因此a[0] = 2;
22 / 3! = 3餘4,因此a[1] = 3;
4 / 2! = 2餘0,因此a[2] = 2;
0 / 1! = 0餘0,因此a[3] = 0;
0 / 0! = 0餘0,因此a[4] = 0;
在1, 2 , 3, 4, 5中,第2大(從0開始算)的數是3
在1, 2 , 4, 5中,第3大(從0開始算)的數是5
在1, 2 , 4中,第2大(從0開始算)的數是4
在1, 2中,第0大(從0開始算)的數是1,最後一個數就是2
因此就能得到序列3,5,4,1,2
以上就是康託逆展開
四.程式碼實現:
(1)康託展開實現程式碼:
fact[10]; //fact[i]儲存i的階乘的值
//把陣列s合併為一個狀態num, k代表陣列長度
void cantor (int s[], ll &num, int k) {
num = 0;
for (int i = 0; i < k; i ++) {
int cnt = 0;
for (int j = i + 1; j < k; j++) {
if (s[i] > s[j]) cnt++;
}
num += fact[k - i - 1] * cnt;
}
}
(2)康託逆展開程式碼:
fact[10]; //fact[i]儲存i的階乘的值
//把狀態值num轉回陣列s
bool book[10]; //判斷序列中下角標為i的數是否已經標記
void inv_cantor (int s[], ll num, int k) {
memset (book, 0, sizeof(book));
for (int i = 0; i < k; i++) {
int p = num / fact[k - i - 1];
num %= fact[k - i - 1];
int tot = 0;
for (int j = 0; j < k; j++) {
if (!book[j]) tot ++;
if (tot == p) {
book[j] = 1;
s[i] = j + 1;
break;
}
}
}
}
理論就是這些~
如果有寫的不對或者不全面的地方 可通過主頁的聯絡方式進行指正,謝謝