一.引出康托展开
动态规划题有一类分支叫状压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;
}
}
}
}
理论就是这些~
如果有写的不对或者不全面的地方 可通过主页的联系方式进行指正,谢谢