题目描述
在一个3×3的网格中,1~8这8个数字和一个“X”恰好不重不漏地分布在这3×3的网格中。
例如:
1 2 3
X 4 6
7 5 8
在游戏过程中,可以把“X”与其上、下、左、右四个方向之一的数字交换(如果存在)。
我们的目的是通过交换,使得网格变为如下排列(称为正确排列):
1 2 3
4 5 6
7 8 X
例如,示例中图形就可以通过让“X”先后与右、下、右三个方向的数字交换成功得到正确排列。
交换过程如下:
1 2 3 1 2 3 1 2 3 1 2 3
X 4 6 -> 4 X 6 -> 4 5 6 -> 4 5 6
7 5 8 7 5 8 7 X 8 7 8 X
现在,给你一个初始网格,请你求出得到正确排列至少需要进行多少次交换。
输入样例:
2 3 4 1 5 x 7 6 8
输出样例:
19
分析
本题求最少步数,所以应当用bfs来做
首先定义一个能表示矩阵状态的结构体,每次把由当前状态更新的合法的新状态压入队列
如果状态为目标状态,那么返回步数,如果更新不到目标状态,返回-1
我们可以想到,这个3*3的矩阵可以表示为一个长度为9的字符串
但是我们知道,bfs需要把遍历过的状态标记,以防止死循环
那么,如何开辟一个数组
使得这个数组中的元素,能够和矩阵的所有状态(长度为9的字符串的全排列)一一对应
这才是难点
全排列哈希
-
我们熟知的数一般都是常进制数,所谓常进制数就是该数的每一位都是常数进制的
$k$进制数上的每一位都逢$k$进一,第$i$位的位权是$k^i$
-
这里要介绍一种变进制数,用来表示字符串的排列状态
这种数的第$i$位逢$i$进一,第$i$位的位权是$i!$
用$d[i]$来表示一个变进制数第$i$位上的数字
一个$n$位变进制数的值就为$\sum_{i=0}^{n-1}$ $d[i]×i!$
这是一个最大的9位变进制数
876543210
它对应的十进制数为
8 × 8! + 7 × 7! + 6 × 6! + …… + 1 × 1! + 0 × 0! = 9! - 1 = 362879
我们可以找到一个9位变进制数,与一个9位无重复串的某种排列一一对应
用$d[i]$表示字符串中的第$i$位与其前面的字符组成的逆序对个数
字符串的一种排列对应的变进制数的值为$\sum_{i=0}^{n-1}$ $d[i]×i!$
这是字符串123x46758
的与$d[i]$的对应关系
i 0 1 2 3 4 5 6 7 8
s[i] 1 2 3 x 4 6 7 5 8
d[i] 0 0 0 0 1 1 1 3 1
它对应的变进制数的值为
1 × 4! + 1 × 5! + 1 × 6! + 3 × 7! + 1 × 8! = 56304
因此可以用以下函数求字符串的一种排列对应的哈希值
int permutation_hash(char s[], int n) //求长度为n的字符串某种排列的哈希值
{
int ans = 0;
for(int i = 0; i < n; i ++)
{
int d = 0;
for(int j = 0; j < i; j ++)
if(s[j] > s[i]) d ++; //求s[i]与其前面的字符组成的逆序对个数
ans += d * fact[i];
}
return ans;
}
- n不能太大,通常不超过12,否则会溢出
- 时间复杂度为O(n²)
全排列哈希 + BFS
C++ 代码
#include<cstring>
#include<iostream>
#include<queue>
using namespace std;
int fact[9];
bool vis[362880];
int permutation_hash(char s[]) //求长度为9的字符串某种排列的哈希值
{
int ans = 0;
for(int i = 0; i < 9; i ++)
{
int d = 0;
for(int j = 0; j < i; j ++)
if(s[j] > s[i]) d ++; //求s[i]与其前面的字符组成的逆序对个数
ans += d * fact[i];
}
return ans;
}
typedef struct{
char s[10];
int step;
int k; //'x'在第k位
}Point;
int dx[4] = {-1, 0, 1, 0};
int dy[4] = { 0,-1, 0, 1};
int bfs(Point p)
{
vis[permutation_hash(p.s)] = true;
queue<Point> q;
q.push(p);
while(!q.empty())
{
p = q.front();
q.pop();
/*
printf("%d ",p.step); //print调试法
puts(p.s);
*/
if(!strcmp(p.s , "12345678x")) return p.step;
int x = p.k / 3; //'x'的行数
int y = p.k % 3; //'x'的列数
Point next;
next.step = p.step + 1;
for(int i = 0; i < 4; i ++)
{
int nx = x + dx[i];
int ny = y + dy[i];
if(nx >= 0 && nx <= 2 && ny >= 0 && ny <= 2)
{
next.k = nx * 3 + ny; //求出'x'在字符串中的的新位置
strcpy(next.s, p.s);
next.s[9] = 0;
next.s[p.k] = p.s[next.k]; //先用即将和'x'交换的字符覆盖'x'之前的位置
next.s[next.k] = 'x'; //再给'x'的新位置赋值'x'
int hash = permutation_hash(next.s);
if(!vis[hash])
{
vis[hash] = true;
q.push(next);
}
}
}
}
return -1;
}
int main()
{
fact[0] = 1;
for(int i = 1; i < 9; i ++) fact[i] = fact[i - 1] * i; //预处理fact[i] = i!
char c[2],str[10];
Point start;
for(int i = 0; i < 9; i ++)
{
scanf("%s",&c);
if(c[0] == 'x') start.k = i;
start.s[i] = c[0];
}
start.s[9] = 0;
start.step = 0;
printf("%d",bfs(start));
return 0;
}
orz
好强!orz
好牛逼
orz
康托展开在这道题唯一的用处就仅仅是$O(N^2)$做了个字符串哈希呗,感觉康托展开有更高级的用法?能给定reference吗?比如说有哪些还能这么做或者能用这个方法优化?
好像没什么题目,leetcode上60题纯粹模板题之外,就只有八数码了,因为好像阶乘基本处理不了很多的数据,估计应用方面比较少,优化的话参见博客中的(实现的算法复杂度为O(n^2),实际当n很大时,内层循环计算在当前位之后小于当前位的个数可以用 线段树来处理计算,而不用每次都遍历,这样复杂度可以降为O(nlogn)),
一篇很棒的博客
https://blog.csdn.net/wbin233/article/details/72998375?spm=1001.2101.3001.6650.9&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-9-72998375-blog-87696372.pc_relevant_paycolumn_v3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-9-72998375-blog-87696372.pc_relevant_paycolumn_v3&utm_relevant_index=12
康托展开
Orz
orz
orz
Orz
为什么这里d[i]要选用逆序对的个数呀?
康托展开就是这么定义的,这种方法可以实现1对1的映射
orz
orz
请问这个全排列哈希适合那种类型问题, 数据量多大?
只适合很短的排列与int的一个双射,因为每次映射都要进行$O(len²)$次计算
orz
orz
orz
如此清晰 %%%
牛啊大佬
转化为二维空间的行和列那块, int x = p.k / 3; //’x’的行数
int y = p.k % 3; //’x’的列数。谁能说一下,谢谢了
行的下标是从0开始的 所以 /3 表示 每行3个 有多少行, %3 表示 每行三个的情况下,当前位置该在那个列的位置。 建议手动模拟一下,更加形象。
为什么要让start.s[9]和next.s[9]= 0呢?
全排列哈希的第5句“用d[i]来表示一个变进制数第i位上的数字”,是不是应该改为“用s[i]来表示一个变进制数第i位上的数字”,我看半天。。。。
是代码里的变量 $d$