解释
康托展开是一种特殊的哈希函数。康托展开可以完成了如下图所示的工作
状态 | 012345678 | 012345687 | 012345768 | 012345786 | … | 876543210 |
---|---|---|---|---|---|---|
Cantor | 0 | 1 | 2 | 3 | … | 362880 - 1 |
第一行是 0 ~ 8 这 9 个数字的全排列,共 9! = 362880 个,按从小到大排序。第二行是每个排列对应的位置,例如最小的 {012345678} 在第 0 个位置, 最大的 {876543210} 在最后的 362880 - 1 位置。
- 函数Cantor()实现的功能是:输入一个排列,即第一行的某个排列,计算出它的Cantor值,即第二行对应的数值
Cantor()的复杂度为O(n2),n 是集合中元素的个数。
原理
例:判断 2143 是 {1, 2, 3, 4} 的全排列中第几大的数。
计算排在 2143 前面的排列数目,可以将问题转化为以下排列的和。
- (1)首位小于 2 的所有排列。比 2 小的只有 1 一个数,后面 3 个数的排列有 3 * 2 * 1 = 3! 个,写成 1 * 3! = 6
- (2)首位为 2、第 2 位小于 1 的所有排列。无,写成 0 * 2! = 0。
- (3)前两位为 21、第 3 位小于 4 的所有排列。只有 3 一个数(2134),写成 1 * 1! = 1。
- (4)前 3 位为 214、第四位小于 3 的所有排列。无,写成 0 * 0! = 0。
求和:1 * 3! + 0 * 2! + 1 * 1! + 0 * 0! = 7,所以 2143 是第八大的数。如果用 int visited[24]数组记录各排列的位置,{2143} 就是 visited[7]; 第一次访问这个排列时,置visited[7] = 1; 当再次访问这个排列的时候发现 visited[7] = 1,说明已经处理过,判重。
根据上面的例子得到康托展开公式。
把一个集合产生的全排列按字典序排列,第 X 个元素的计算公式如下
- X = a[n] * (n - 1)! + a[n - 1] * (n - 2)! + … + a[i] * (i - 1)! + … + a[2] * 1! + a[1] * 0!
其中,a[i] 表示原数的第 i 位在当前未出现元素中排在第几个(从 0 开始),并且有 0 <= a[i] < i(1 <= i < n)
- 上述过程的反过程是康托逆展开:某个集合的全排列,输入一个数字 k,返回第 k 大的排列
处理问题
八数码问题
- BFS + Cantor
在一个 3 * 3 的棋盘上放置编号为 1 ~ 8 的 8 个方块,每个占一格,另外还有一个空格。与空格相邻的数字方块可以移动到空格里。任务一:指定初始棋局和目标棋局,计算出最少的移动步数;任务二:输出数码的移动序列。
//输入:1 2 3 0 8 4 7 6 5
1 0 3 8 2 4 7 6 5
//输出:2
#include <bits/stdc++.h>
const int LEN = 362880;//共 9! = 362880 种
using namespace std;
struct node{
int state[9];//记录一个八数码的排列,即一个状态
int dis;//记录到起点的距离
};
int dir[4][2] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};//左、上、右、下,顺时针,左上为(0, 0);
int visited[LEN] = {0};//与每个状态对应的记录,cantor()函数对它置数,并判重
int start[9];//开始状态
int goal[9];//目标状态
long int factory[] = {1, 1, 2, 6, 24, 720, 5040, 40320, 362880};
bool Cantor(int str[], int n){
long result = 0;
for(int i = 0; i < n; i ++){
int counted = 0;
for(int j = i + 1; j < n; j ++){
if(str[i] > str[j])
++ counted;
}
result += counted * factory[n - i - 1];
}
if(!visited[result]){
visited[result] = 1;
return 1;
}
else
return 0;
}
int bfs(){
node head;
memcpy(head.state, start, sizeof(head.state));
head.dis = 0;
queue<node>q;
Cantor(head.state, 9);
q.push(head);
while(!q.empty()){
head = q.front();
if(memcmp(head.state, goal, sizeof(goal)) == 0)
return head.dis;
q.pop();
int z;
for(z = 0; z < 9; z ++)//找这个状态种元素 0 的位置
if(head.state[z] == 0)//找到了
break;
int x = z%3;//横坐标
int y = z/3;//纵坐标
for(int i = 0; i < 4; i ++){//左、上、右、下,四种变化
int newx = x + dir[i][0];//元素 0 转移后的新坐标
int newy = y + dir[i][1];
int nz = newx + 3 * newy;//转化为一维
if(newx >= 0 && newx < 3 && newy >= 0 && newy < 3){//未越界
node newnode;
memcpy(&newnode, &head, sizeof(struct node));//复制这新的状态
swap(newnode.state[z], newnode.state[nz]);//把 0 移到新的位置
newnode.dis ++;
if(Cantor(newnode.state, 9))//用康托展开判重
q.push(newnode);//把新的状态放进队列
}
}
}
return -1;//没找到
}
int main(){
for(int i = 0; i < 9; i ++) cin >> start[i];//初始状态
for(int i = 0; i < 9; i ++) cin >> goal[i];//目标状态
int num = bfs();
if(num != -1) cout << num << endl;
else cout << "Impossible" << endl;
return 0;
}
//1 2 3 0 8 4 7 6 5 1 0 3 8 2 4 7 6 5