A_Star 康托展开 八数码问题

A_Star算法是一个主要用bfs实现的寻路算法,当数据量庞大的时候往往有更高的时间效率,可以看成是Dijkstra算法的升级版。

“盲目搜索会浪费很多时间和空间, 所以我们在路径搜索时, 会首先选择最有希望的节点, 这种搜索称之为 "启发式搜索"”(本段话来源于

特点:时间效率高,但是空间会呈指数增长

A_Star算法时间效率的提升主要为每个数据增加上了优先级,每次更新距离选取优先级最高的点来进行更新(也就是“最有希望的节点”)优先级由这个函数来决定:                               ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​                     ​​​​​​​        ​​​​​​​        ​​​​​​​          f(n) = g(n) + h(n)

g(n)是从起点到n点的距离

h(n)是从n点到终点的预期距离,被称为启发函数(顾名思义就是通过预期来给定最有希望的花费)

A_Star最核心就在于如何去选择启发函数:

1.如果网格图中只能上下左右四个方向移动时:曼哈顿距离

        用公式来说就是:A(a,b)  B(c,d)  曼哈顿距离dis = abs(a - c) + abs(b - d)

2.如果能向周围上下左右斜上歇下八个方向移动时:对角距离

           代码摘自此处

function heuristic(node)
    local dx = abs(node.x-goal.x)
    local dy = abs(node.y-goal.y)
    return D * (dx + dy) + (D2 - 2 * D) * min(dx,dy)
end

3.如果能向任意方向移动时:欧几里得距离

        换句话说就是两点之间的直线距离

其次要注意的是,关于启发函数的选取必须要保证f(n) + g(n) <= f(end),即从起点到点n的最短距离加上点n到终点的预期距离必须小于等于从起点到终点的最短距离。其次,如果启发函数h(n)的值一直为0,那么就会退化成Dijkstra算法,甚至从时间空间上来说不如Dijkstra算法

关于A_Star算法有个非常经典的八数码问题,类似于数字华容道:题目链接

题目首先给出一个字符串,例如:

//2 3 4 1 5 x 7 6 8 

我们要做的本质上就是怎么移动x把它变成:

//1 2 3 4 5 6 7 8 x

首先说结论:如果给定的字符串是有解的,那么这个字符串的逆序对数量一定是一个偶数

理由如下:

        首先我们最后要让字符串变成"12345678x",这串数字的逆序对数量毫无疑问为0,那也就是说我们的最终答案要让字符串的逆序对数量变成0.

        其次我们通过观察可以发现:

/*
2 3 4        2 3 x   
1 5 x   -->  1 5 4
7 6 8        7 6 8

2 3 4 1 5 x 7 6 8
       |
2 3 x 1 5 4 7 6 8
*/

       不论当我们怎样移动x的位置,移动之后的结果在一个字符串上得到的结果都是将x的位置往前或者往后移动两位,也就是说移动之后的结果只会将移动前的字符串逆序对数量+2或者-2或者不变,对逆序对的奇偶性没有产生任何影响。这也就说明了如果逆序对数量为奇数的话,不论我们怎么移动,都不会将逆序对数量变成0,也就无解

        知道了这些条件,我们用A_Star算法就可以解决这题:

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
#include <unordered_map>
#define x first
#define y second
using namespace std ;

typedef pair<int, string> PIS ;         //预期值(启发函数)   字符串

int dx[4] = {0, 0, 1, -1} ;     //上下左右四个方向
int dy[4] = {1, -1, 0, 0} ;
char op[] = "rldu" ;            //操作字符,根据上面的方向来写的

int h(string s) {       //用来计算启发函数,也就是曼哈顿距离  f(n)
	int res = 0 ;
	for (int i = 0 ; i < s.size() ; i ++) {
		if (s[i] != 'x') {
			int t = s[i] - '1' ;
			res += abs(t / 3 - i / 3) + abs(t % 3 - i % 3) ;  //曼哈顿距离
		}
	}
	return res ;
}

string bfs(string start) {
	string end = "12345678x" ;      //最终状态
	unordered_map<string, int>dis ;  //dis表示从起点到n点的最短距离
	unordered_map<string, pair<char, string>> prev;  //prev用来记录路径
	priority_queue<PIS, vector<PIS>, greater<PIS>> heap ;  //heap是优先队列,根据第一个键值(启发函数)从小到大排序
	dis[start] = 0 ;
	heap.push({h(start), start}) ;
	while (heap.size()) {
		auto t = heap.top() ;
		heap.pop() ;
		string tmp = t.y ;
		if (tmp == end)  //如果达到了最终状态直接break
			break ;
		int x, y ;
		for (int i = 0 ; i < 9 ; i ++) {        //获取'x'的坐标
			if (tmp[i] == 'x') {
				x = i / 3, y = i % 3 ;
				break ;
			}
		}
		string backup = tmp ;
		for (int i = 0 ; i < 4 ; i ++) {
			int tx = x + dx[i], ty = y + dy[i] ;
			if (tx < 0 || tx >= 3 || ty < 0 || ty >= 3)
				continue ;
			tmp = backup ;
			swap(tmp[tx * 3 + ty], tmp[x * 3 + y]) ;
			if (!dis.count(tmp) || dis[tmp] > dis[backup] + 1) { //如果dis没有出现过tmp,或者dis[tmp]有更优解
				dis[tmp] = dis[backup] + 1 ;
				prev[tmp] = {op[i], backup} ;
				heap.push({dis[tmp] + h(tmp), tmp}) ;
			}
		}
	}

	string res ;
	while (end != start) {      //回溯返回答案
		res += prev[end].x ;
		end = prev[end].y ;
	}
	reverse(res.begin(), res.end()) ;
	return res ;
}

int main() {
	char c ;
	string str, seq ;  //seq用来计算逆序对的数量
	while (cin >> c) {
		if (c != 'x')
			seq += c ;
		str += c ;
	}
	int cnt = 0 ;
	for (int i = 0 ; i < 8 ; i ++) {  //计算逆序对的数量
		for (int j = i ; j < 8 ; j ++) {
			if (seq[j] > seq[i])
				cnt ++ ;
		}
	}
	if (cnt & 1)  //如果是奇数直接unsolvable
		puts("unsolvable") ;
	else
		cout << bfs(str) << '\n' ;

	return 0 ;
}

用这串代码是可以过掉AcWing上面的八数码题,但是HDU上面的八数码是过不了了

HDU-1043(Vjudge)

Eight(HDU-1043)

前面我们说过A_Star算法的一个弊端就是空间会呈指数增长,如果数据量太庞大,我们内存可能会超限。所以说我们需要用别的方法来优化空间复杂度。

这就用到了一个知识叫做"康托展开":

康托展开是一个全排列到一个自然数双射,常用于构建哈希表时的空间压缩。 (百度)

康托展开运算:X= a_{n}(n-1)!+a_{n-1}(n-2)!+...+a_{1}0!

a表示的是当前数字往右有多少比它大的数,例如:

//4    5    3    1    2

1.第一位是4,往右边数总共有三个数比它小,所以就是3×4!

2.第二位数字是5,往右边数总共有三个数比它小,所以就是3×3!

3.第三位数字是3,往右边数总共有两个数比它小,所以就是2×2!

4.第四位是1,往右边数没有数字比它小,所以就是0×1!

5.第五位数字是2,往右边数没有数字比它小,所以就是0×0!

最终X=3×4!+3×3!+2×2!+0×1!+0×0!=94

这样就得到了45312这个全排列到自然数的一个映射

同时康托也能逆展开:

1.94 / 4!  = 3 ······ 22  三个数比它小:4

2.22 / 3!  = 3 ······ 4    三个数比它小:5

3.4 / 2! =2 两个数比它小:3

··················

通过这样的思路我们就能将一个康托展开映射成的数,反过来推出原来的排列结果:45312

通过康托展开我们就可以构建哈希表来对空间复杂度进行压缩,学习这块的知识时我看了很多人的代码,其中我觉得这个大佬的代码写的最为好看:大佬链接

其中他的手写类好看的让我欲罢不能,所以我大体也是照着他的模样来学习的,我改动了字符串处理代码和函数的传参类型,我觉得会更好看一点,代码如下:

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std ;
const int n = 9 ;

const int fac[n] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320} ;  //预处理阶乘

const int dx[4] = {0, 0, 1, -1} ;

const int dy[4] = {1, -1, 0, 0} ;
char op[] = "rldu" ;		//路径
const int N = 362880 ;//最大的hash值+1,也就是排列876543210的hash值+1
const int aim = 46233;  //123456780的hash值
char step[N] ;  //路径
bool vis[N] ;		//记录有没有被hash过
int mp[N] ;			//实际路径
int parent[N] ;

int goal_state[9][2] = {		//预处理坐标
	{0, 0}, {0, 1}, {0, 2},
	{1, 0}, {1, 1}, {1, 2},
	{2, 0}, {2, 1}, {2, 2}
};

int *solve(string s) {   //处理字符串
	static int a[n] ;
	int cnt = 0;
	for (int i = 0 ; i < s.size() ; i ++) {
		if (s[i] == 'x') {
			a[cnt ++] = 0 ;
			continue ;
		}
		if (s[i] >= '0' && s[i] <= '9') {
			a[cnt ++] = s[i] - '0' ;
		}
	}
	return a ;
}

int getcnt(int *a) {  //统计逆序对数量
	int cnt = 0;
	for (int i = 0 ; i < 9 ; i ++) {
		if (a[i] == 0)
			continue ;
		for (int j = i + 1 ; j < 9 ; j ++) {
			if (a[j] == 0)
				continue ;
			if (a[j] < a[i])
				cnt ++ ;
		}
	}
	return cnt ;
}

int cantor(int *s) { //康托展开
	int res = 0, cnt = 0 ;
	for (int i = 0 ; i < n ; i ++) {
		cnt = 0 ;
		for (int j = i + 1 ; j < n ; j ++) {
			if (s[i] > s[j])
				cnt ++ ;
		}
		res += fac[n - 1 - i] * cnt ;
	}
	return res ;
}

int h(int *s) {			//曼哈顿距离
	int pos, res = 0 ;
	for (int i = 0 ; i < 3 ; i ++) {
		for (int j = 0 ; j < 3 ; j ++) {
			pos = i * 3 + j ;
			if (s[pos] == 0)
				continue ;
			res += abs(i - goal_state[s[pos] - 1][0]) + abs((j - goal_state[s[pos] - 1][1]));
		}
	}
	return res ;
}

class Chess {
	public:
		int hash, f ;
		Chess(const int &hash, const int &f) : hash(hash), f(f) {  //康托哈希值,花费值
		}
		bool operator<(const Chess &t)const {
			if (f == t.f)
				return mp[hash] > mp[t.hash] ;
			return f > t.f ;
		}
};

void reverseCantor(int hash, int s[], int &space) {		//逆康托展开
	bool visited[n] = {} ;
	int tmp ;
	for (int i = 0 ; i < n ; i ++) {
		tmp = hash / fac[n - 1 - i] ;
		for (int j = 0 ; j < n ; j ++ ) {
			if (!visited[j]) {
				if (tmp == 0) {
					s[i] = j ;
					if (j == 0)
						space = i ;
					visited[j] = true ;
					break ;
				}
				tmp -- ;
			}
		}
		hash %= fac[n - i - 1] ;
	}
}

void printPath() {		//回溯打印路径
	char queue[31] ;
	int t = 0 ;
	int c = aim ;
	while (parent[c] != -1) {
		queue[t] = step[c] ;
		t ++ ;
		c = parent[c] ;
	}
	for (int i = t - 1 ; i >= 0 ; i --) {
		cout << queue[i] ;
	}
	puts("") ;
}

void A_star(int start, int f) {
	priority_queue<Chess> q ;
	q.push(Chess(start, f)) ;
	int preHash, hash, state[n], space ;
	while (!q.empty()) {
		Chess preChess = q.top() ;
		preHash = preChess.hash ;
		if (preHash == aim) {
			printPath() ;
			return ;
		}
		q.pop() ;
		reverseCantor(preHash, state, space) ;
		for (int i = 0 ; i < 4 ; i ++) {
			int tx = space / 3 + dx[i], ty = space % 3 + dy[i] ;
			if (tx < 0 || tx >= 3 || ty < 0 || ty >= 3)
				continue ;
			int tz = tx * 3 + ty ;
			state[space] = state[tz] ;
			state[tz] = 0 ;
			hash = cantor(state) ;
			if (!vis[hash]) {
				step[hash] = op[i] ;
				mp[hash] = mp[preHash] + 1 ;
				vis[hash] = true ;
				parent[hash] = preHash ;
				q.push(Chess(hash, mp[hash] + h(state))) ;
			} else if (mp[hash] > mp[preHash] + 1 ) {
				step[hash] = op[i] ;
				mp[hash] = mp[preHash] + 1  ;
				parent[hash] = preHash ;
				q.push(Chess(hash, mp[hash] + h(state))) ;
			}
			state[tz] = state[space] ;
			state[space] = 0 ;
		}
	}
	puts("unsolvable") ;
}

int main() {
	string s ;
	while (getline(cin, s)) {
		int *a = solve(s) ;
		if (getcnt(a) & 1) {
			puts("unsolvable") ;
			continue ;
		}
		int hash = cantor(a) ;
		memset(vis, false, sizeof(vis)) ;
		memset(mp, 0, sizeof(mp)) ;
		vis[hash] = true ;
		parent[hash] = -1 ;
		mp[hash] = 0 ;
		A_star(hash, h(a)) ;
	}

	return 0;
}

水平有限,欢迎指正

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值