每日一题之 hiho1312 八数码问题

这篇博客讨论了八数码游戏的计算机解决方案,重点介绍了使用启发式搜索,特别是宽度优先搜索和A*算法。通过康托展开和逆康托展开优化了状态空间,将3x3棋盘转化为长度为9的数组,并设计了评估函数,如曼哈顿距离,以提高搜索效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

描述
在小Ho的手机上有一款叫做八数码的游戏,小Ho在坐车或者等人的时候经常使用这个游戏来打发时间。

游戏的棋盘被分割成3x3的区域,上面放着标记有1~8八个数字的方形棋子,剩下一个区域为空。

在这里插入图片描述
游戏过程中,小Ho只能移动棋子到相邻的空区域上。当小Ho将8个棋子都移动到如下图所示的位置时,游戏就结束了。

在这里插入图片描述
小Hi:小Ho,你觉得如果用计算机来玩这个游戏应该怎么做?

小Ho:用计算机来玩么?我觉得应该是搜索吧,让我想一想。

提示:启发式搜索

输入
第1行:1个正整数t,表示数据组数。1≤t≤8。

接下来有t组数据,每组数据有3行,每行3个整数,包含0~8,每个数字只出现一次,其中0表示空位。

输出
第1…t行:每行1个整数,表示该组数据解的步数。若无解输出"No Solution!"

样例输入
3
1 2 3
4 5 6
7 8 0
1 2 3
4 5 6
8 7 0
8 0 1
5 7 4
3 6 2
样例输出
0
No Solution!
25

思路:

引用一波提示
小Ho:这个问题和上一次一样嘛,用宽度优先搜索来求解。

然后把这个3x3的二维数组拉伸成一个长度为9的数组,将长度为9的数组作为状态。

那么最终状态就是{1,2,3,4,5,6,7,8,0}。

由于每一个位置的有9种可能,所以我建立一个9维数组来判重进行搜索就好了。

小Hi:9维数组,每一维的大小为9。小Ho,你确定这不会超过内存限制么?

小Ho:9的9次方等于387420489,好像是挺大的。不过应该没问题吧。

小Hi:怎么可能没问题!这个数据已经很大了好么!

小Ho:那该怎么办啊?

小Hi:小Ho,你仔细观察题目的状态。由于每个数字一定只会出现一次,每个状态对应的恰好是1~9的一个排列。

那么1~9的全排列有多少种呢?

小Ho:这个我知道,是9!,一共362880种。

小Hi:没错,总共只有不到40万种不同的情况。如果我们能够使用一个方法来表示不同排列的状态,那么是不是就可以把判重的状态数量压缩到40万以内了呢?

小Ho:恩,没错。但是有什么好的方法么?

小Hi:当然有啦,这里我们需要用的事全排列的知识。小Ho你知道全排列是有顺序的么?

小Ho:恩,知道。比如3个数的全排列,按顺序就是:

123, 132, 213, 231, 312, 321
小Hi:没错,那么第二个问题:假如我给你一个全排列,你能计算出它是第几个排列么?

小Ho:(⊙v⊙),这个我不知道。

小Hi:我就知道你不知道,让我来告诉你吧。这里我们需要用到一个叫做康托展开的方法。

对于一个长度为n的排列num[1…n],其序号X为:

X = a[1](n-1)!+a[2](n-2)!+…+a[i]*(n-i)!+…+a[n-1]*1!+a[n]*0!
其中a[i]表示在num[i+1…n]中比num[i]小的数的数量
举个例子,比如213:

num[] = {2, 1, 3}
a[] = {1, 0, 0}
X = 1 * 2! + 0 * 1! + 0 * 1! = 2
我们如果将3的全排列从0开始编号,2号对应的正是213。

其写做伪代码为:

Cantor(num[])
	X = 0
	For i = 1 .. n
		tp = 0
		For j = i + 1 .. n
			If (num[j] < num[i]) Then
				tp = tp + 1
			End If
		End For
		X = X + tp * (n - i)!
	End For
	Return X

那么接下来,第三个问题!

小Ho:你说吧!

小Hi:已知X,如何去反向求解出全排列?

小Ho:我觉得应该还是从康托展开的公式入手。

< 小Ho拿出草稿纸,在上面推算了一会儿 >

根据X的求值公式,可以推断出对于a[i]来说,其值一定小于等于n-i。那么有:

a[i]≤n-i, a[i](n-i)!≤(n-i)(n-i)!<(n-i+1)!
也就是说,对于a[i]来说,无论a[i+1…n]的值为多少,其后面的和都不会超过(n-i)!

那么也就是说,如果我用X除以(n-1)!,得到商c和余数r。其中c就等于a[1],r则等于后面的部分。

这样依次求解,就可以得到a[]数组了!

比如求解3的全排列中,编号为3的排列:

3 / 2! = 1 … 1 => a[1] = 1
1 / 1! = 1 … 0 => a[2] = 1
0 / 0! = 0 … 0 => a[3] = 0
然后就是根据a[]来求解num[],让我想一想。

我知道了!由于a[i]表示的是num[i+1…n]中比num[i]还小的数字。

那么只需要从num[1]开始,依次从尚未使用的数字中选取第a[i]+1小的数字填入就可以了!

紧接着上面的例子:

a[] = {1, 1, 0}
unused = {1, 2, 3}, a[1] = 1, num[1] = 2
unused = {1, 3}, a[2] = 1, num[2] = 3
unused = {1}, a[3] = 0, num[3] = 1
=> 2, 3, 1
231也确实是3的全排列中编号为3的排列。

小Hi:小Ho,你真棒!你使用的这个方法也被称为逆康托展开,写作代码的话:

unCantor(X):
	a = []
	num = []
	used = [] // 长度为n的boolean数组,初始为false
	For i = 1 .. n
		a[i] = X / (n - i)!
		X = X mod (n - i)!
		cnt = 0
		For j = 1 .. n
			If (used[j]) Then
				cnt = cnt + 1
				If (cnt == a[i] + 1) Then
					num[i] = j
					used[j] = true
					Break
				End If
			End If
		End For
	End For
	Return num

通过康托展开以及康托逆展开,我们就将该问题的状态空间压缩到了9!,在空间复杂度上得到了优化。

小Ho:那么这次的问题不就解决了!

小Hi:远远没那么简单哦,其实这个问题还有一个时间上的优化。

小Ho:但是宽度优先搜索不就是最快寻找到解的方法了么?还有更好的方法么?

小Hi:当然有了,我们有一种叫做启发式搜索的方法。

在启发式搜索的过程中,不再是一定按照步数最优的顺序来搜索。

首先在启发式搜索中,我们每次找到当前“最有希望是最短路径”的状态进行扩展。对于每个状态的我们用函数F来估计它是否有希望。F包含两个部分:

F = G + H
G:就是普通宽度优先搜索中的从起始状态到当前状态的代价,比如在这次的问题中,G就等于从起始状态到当前状态的最少步数。

H:是一个估计的值,表示从当前状态到目标状态估计的代价(步数)。

H是由我们自己设计的,H函数设计的好坏决定了A*算法的效率。H值越大,算法运行越快。

但是在设计评估函数时,需要注意一个很重要的性质:评估函数的值一定要小于等于实际当前状态到目标状态的代价(步数)。

否则虽然你的程序运行速度加快,但是可能在搜索过程中漏掉了最优解。相对的,只要评估函数的值小于等于实际当前状态到目标状态的代价,就一定能找到最优解

在这个问题中可以表述为:评估函数得到的从当前状态到目标的状态需要行动的步数一定不能超过实际上需要行动的步数。

所以,我们可以将评估函数设定为:1-8八数字当前位置到目标位置的曼哈顿距离之和。(为什么这样设计留给读者思考。当然也有其他符合条件的估计函数,不同估计函数效率如何也留给读者自行比较。)

F:评估值和状态值的总和。

同时在启发式搜索中将原来的一个队列变成了两个队列:openlist和closelist。

在openlist中的状态,其F值还可能发生变化。而在closelist中的状态,其F值一定不会再发生改变。

整个搜索解的流程变为:

计算初始状态的F值,并将其加入openlist
从openlist中取出F值最小的状态u,并将u加入closelist。若u为目标状态,结束搜索;
对u进行扩展,假设其扩展的状态为v:若v未出现过,计算v的f值并加入openlist;若v在openlist中,更新v的F值,取较小的一个;若v在closelist中,抛弃该状态。
若openlist为空,结束搜索。否则回到2。
利用这个方法可以避免搜索一些明显会远离目标状态的状态,从而缩小搜索空间,早一步搜索到目标结果。

在启发式搜索中,最重要的是评估函数的选取,一个好的评估函数能够更快的趋近于目标状态。

将上述过程写做伪代码为:

search(status):
	start.status = status
	start.g = 0	// 实际步数
	start.h = evaluate(start.status)
	start.f = start.g + start.h
	
	openlist.insert(start)
	
	While (!openlist.isEmpty()) 
		u = openlist.getMinFStatus()
		closelist.insert(u)
		For v is u.neighborStatus
			If (v in openlist) Then
				// 更新v的f值
				If (v.f > v.h + u.g + 1) Then
					v.f = v.h + u.g + 1
				End If
			Else If (v in closelist)
				continue
			Else 
				v.g = u.g + 1
				v.h = evaluate(v.status)
				v.f = v.g + v.h
				openlist.insert(v)
			End If
		End For
	End While

其中openlist.getMinFStatus()可以使用堆来实现。

启发式搜索在某些情况下并不一定好用,一方面取决于评估函数的选取,另一个方面由于在选取状态时也会有额外的开销。而快速趋近目标结果所减少的时间,能否弥补这一部分开销也是非常关键的。

所以根据题目选取合适的搜索方法才是最重要的。

#include <iostream>
#include <cmath>
#include <cstring>
#include <string>
#include <vector>
#include <queue>
#include <set>
#include <algorithm>

using namespace std;

const int maxn = 1e5+7;

int dir[4][2] = {{0,1},{1,0},{0,-1},{-1,0}};
int G[3][3];

int nn[10]; //初始状态
int nums[10];
int a[10];
bool used[10];

int n = 9;
int factorial[9] = {1,1,2,6,24,120,720,5040,40320};
int finalStatus = 46233;


int Cantor(int nums[]) {
	int res = 0;
	for (int i = 1; i <= n; ++i) {
		int tmp = 0;
		for (int j = i+1; j <= n; ++j) {
			if (nums[j] < nums[i])
				tmp += 1;
		}
		res += tmp * factorial[n-i];
	}

	return res;
}

void unCantor(int x) {
	memset(used,0,sizeof(used));

	for (int i = 1; i <= n; ++i) {
		int fac = factorial[n-i];
		a[i] = x / fac;
		x = x % fac;
		int cnt = 0;
		for (int j = 1; j <= n; ++j) {
			if (!used[j]) {
				cnt += 1;
				if (cnt == a[i] + 1) {
					nums[i] = j-1;
					used[j] = 1;
					break;
				}
			}
		}
	}
}

struct Node {
	int status;
	int g,h,f;
	bool operator <(const Node &rhs) const {
		return f < rhs.f;
	}
};

int dist(int x, int y) {
	int xx = abs(x/3 - y/3);
	int yy = abs(x%3 - y%3);

	return xx + yy;
}

int evaluate(int s) {
	unCantor(s);
	int ans = 0;
	int pre, now;
	for (int i = 1; i <= 9; ++i) {
		now = nums[i];
		if (now == 0) continue;
		pre = i;
		ans += dist(pre-1, now-1);
	}

	return ans;
}

class comp {
public:
	bool operator() (const Node& lhs, const Node& rhs) const {
		return lhs.f > rhs.f;
	}
};

void getMap() {

	for (int i = 0; i < 3; ++i) {
		for (int j = 0; j < 3; ++j)
			G[i][j] = i*3 + j + 1;
	}
}

bool judge(int x, int y) {
	if (x >= 0 && x < 3 && y >= 0 && y < 3)
		return true;
	return false;
}

vector<int> getMove(int index) {
	getMap();
	vector<int>ans;
	int x = (index - 1) / 3;
	int y = (index - 1) % 3;

	for (int i = 0; i < 4; ++i) {
		int dx = x + dir[i][0];
		int dy = y + dir[i][1];
		if (judge(dx,dy)) {
			ans.push_back(G[dx][dy]);
		} 
	}
	sort(ans.begin(),ans.end());
	return ans;
}

vector<Node> getNeighborStatus(Node u) {
	vector<Node>ans;
	unCantor(u.status);
	int zeroIndex = 0;
	for (int i = 1; i <= 9; ++i) {
		if (nums[i] == 0) {
			zeroIndex = i;
			break;
		}
	}

	vector<int>movePosVec = getMove(zeroIndex);
	vector<int>::iterator it;

	for ( it = movePosVec.begin(); it != movePosVec.end(); ++it) {
		int pos = *it;
		swap(nums[zeroIndex], nums[pos]);
		Node t;
		t.status = Cantor(nums);
		swap(nums[zeroIndex], nums[pos]);
		ans.push_back(t);
	}

	return ans;
}

bool inOpenList(Node v, set<Node>& openListSet, Node& ref) {
	set<Node>::iterator it = openListSet.find(v);
	if (it == openListSet.end()) 
		return false;
	ref = *it;
	return true;
}

bool inCloseList(Node v, set<int>& closeList) {

	return closeList.find(v.status) != closeList.end();
}

int aStar(int status) {
	Node start;
	start.status = status;
	start.g = 0;
	start.h = evaluate(start.status);
	start.f = start.g + start.h;

	priority_queue<Node, vector<Node>, comp> openList;
	set<Node>openListSet;

	openList.push(start);
	openListSet.insert(start);

	set<int>closeList;
	Node ref;

	while(!openList.empty()) {
		Node u = openList.top();
		openList.pop();
		if (openListSet.find(u) != openListSet.end()) {
			openListSet.erase(openListSet.find(u));
		}

		closeList.insert(u.status);

		if (u.status == finalStatus) return u.g;

		vector<Node>neigbors = getNeighborStatus(u);
		vector<Node>::iterator it;
		for (it = neigbors.begin(); it != neigbors.end(); ++it) {
			Node v = *it;
			if (inOpenList(v, openListSet, ref)) {
				if (ref.f  > ref.h + u.g + 1) {
					ref.f = ref.h +u.g + 1;
				}
			}
			else if (inCloseList(v, closeList)) continue;
			else {
				v.g = u.g + 1;
				v.h = evaluate(v.status);
				v.f = v.g + v.h;
				openList.push(v);
				openListSet.insert(v);
			}
		}
	}
	return -1;
}

void solve() {

    int t;
    cin >> t;
    for(int i=1;i<=t;i++){
        for(int j=1;j<=9;j++) {
            cin >> nn[j];
        }
        int status = Cantor(nn);

        if (status == -1) cout << "No Solution!" << endl;
        else {
        	int res = aStar(status);
        	if (res == -1) cout << "No Solution!"<<endl;
        	else 
        		cout << res <<endl;
        }
    }
}


int main() {

	solve();

	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值