POJ1077 Eight(A* + 康托展开)

原题链接:http://poj.org/problem?id=1077

前置知识:康托展开 和 康托逆展开

解决什么问题?

能构造一个 \(1\sim N\) 的全排列 和 \(0\sim N!-1\) 之间的双射,在解决全排列的哈希问题上有奇效。
康托展开即是将全排列映射到自然数,而康托逆展开则是将自然数映射到一个全排列。

怎么解决?

对于一个 \(N\) 项全排列 \(\{p\}\),定义其康托展开就是其在所有 \(1\sim N\) 排列中的字典序的排名,再定义 \(rank[i]\) 表示除去 \(p_1 \sim p_{i-1}\),有多少个正整数小于 \(p_i\)。那么显然,有计算式:

\[Cantor(p) = \sum_{i = 1}^N rank[i] \times (N-i)! \]

稍微解释一下,就是逐步确定总的排名,依次计算每一位对排名的贡献。对于第 \(i\)\(p_i\),比它小的可能性有 \(rank[i]\) 种,而每一种贡献了 \((N-i)!\) 个排名。
那么逆展开怎么做?也只要逆向逐步确定即可,并且根据康托展开的定义,我们能且仅能确定一个全排列。
具体实现如下:

int cantor(vector<int> p) {
	int ret = 0;
	for (int i = 0; i < 9; i++) {
		int rnk = p[i]; for (int j = 0; j < i; j++) if (p[j] < p[i]) rnk--;
		ret += (rnk - 1) * fac[8 - i];
	}
	return ret; // rank in [0, 9! - 1]
}

vector<int> cantorRev(int num) {
	vector<int> ret, p; ret.resize(9); p.resize(9);
	for (int i = 0; i < 9; i++) p[i] = i + 1;
	for (int i = 8; i >= 0; i--) {
		int rnk = num / fac[i] + 1;
		for (int j = 0; j < 9; j++) if (p[j]) if (--rnk == 0) { ret[8 - i] = p[j]; p[j] = 0; break; }
		num %= fac[i];
	}
	return ret;
}

补充一下,我的实现中康托展开的复杂度是 \(O(N^2)\) 的,而我们显然能够使用树状数组快速统计出 \(rank[i]\),时间复杂度就能降为 \(O(N\log N)\),但是本题中 \(N=9\),优化意义不大,因此没有写树状数组。同样地,康托逆展开也可以用平衡树(\(set\))优化到 \(O(N\log N)\),然而平衡树常数巨大,有可能产生逆优化。

这么做有什么好处?

相比线性哈希等做法,代码实现方便、美观,且这样节省空间,因为构造的是 \(0 \sim N!-1\) 的映射,在时间复杂度上其实并没有太大的优势。

道理我都懂,这题怎么做?

本题做法很多,有朴素 \(bfs\)、双向 \(bfs\)\(A*\) 等等。
双向 \(bfs\) 是个很不错的做法,从 \(S\)\(T\) 同时开始搜索,直到路径第一次有交即可。
本文主要使用启发式搜索算法 \(A*\) 实现,定义的启发式估价函数是所有对应点对的曼哈顿距离之和,具体内容已经在《算法竞赛进阶指南》中讲述过,我在此不再赘述。
通过康托展开技巧,我们可以省去 \(map\) 等哈希方法,代码也更加清晰。
刚开始我使用的是 \(vector\) 来操作,结果 \(T\) 飞了

展开查看 vector 版代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <cmath>
#include <queue>
#include <algorithm>
using namespace std;
#define mp make_pair
typedef pair<int, int> pii;
const int maxn = 500005;
const int inf = 0x3f3f3f3f;
int vis[maxn], dis[maxn], f[maxn];
int fac[10];
char dir[4] = {'u', 'd', 'l', 'r'};
int head[maxn], nxt[maxn << 2], tail[maxn << 2], type[maxn << 2], ecnt;

void init() {
	fac[0] = 1;
	for (int i = 1; i <= 9; i++) fac[i] = fac[i - 1] * i;
	memset(head, 0, sizeof(head));
}

int cantor(vector<int> p) {
	int ret = 0;
	for (int i = 0; i < 9; i++) {
		int rnk = p[i]; for (int j = 0; j < i; j++) if (p[j] < p[i]) rnk--;
		ret += (rnk - 1) * fac[8 - i];
	}
	return ret; // rank in [0, 9! - 1]
}

vector<int> cantorRev(int num) {
	vector<int> ret, p; ret.resize(9); p.resize(9);
	for (int i = 0; i < 9; i++) p[i] = i + 1;
	for (int i = 8; i >= 0; i--) {
		int rnk = num / fac[i] + 1;
		for (int j = 0; j < 9; j++) if (p[j]) if (--rnk == 0) { ret[8 - i] = p[j]; p[j] = 0; break; }
		num %= fac[i];
	}
	return ret;
}

int mmp[5][5], mmpT[5][5];
int manhattan(int num) {
	vector<int> p = cantorRev(num); int ret = 0;
	for (int i = 0; i < 9; i++) mmp[(i / 3) + 1][(i % 3) + 1] = p[i];
	for (int i = 0; i < 9; i++) mmpT[(i / 3) + 1][(i % 3) + 1] = i;
	for (int i = 1; i <= 3; i++) for (int j = 1; j <= 3; j++)
		for (int x = 1; x <= 3; x++) for (int y = 1; y <= 3; y++)
			if (mmp[i][j] == mmpT[x][y]) ret += abs(i - x) + abs(j - y);
	return ret;
}

void addedge(int u, int v, int t) {
	nxt[++ecnt] = head[u];
	head[u] = ecnt;
	tail[ecnt] = v;
	type[ecnt] = t;
}

void print(int num) {
	vector<int> tmp = cantorRev(num);
	for (int i = 0; i < 9; i++) cout << tmp[i] << " "; cout << endl;
}

pii last[maxn];
void Astar(int S, int T) {
	memset(dis, inf, sizeof(dis));
	memset(vis, 0, sizeof(vis));
	priority_queue<pii> pq;
	dis[S] = 0; pq.push(mp(-(0 + f[S]), S));
	while (!pq.empty()) {
		pii cur = pq.top(); pq.pop();
		int u = cur.second;
		if (vis[u]) continue; vis[u] = 1;
		if (u == T) break;
		for (int e = head[u]; e; e = nxt[e]) {
			int v = tail[e];
			if (dis[v] > dis[u] + 1) {
				dis[v] = dis[u] + 1;
				last[v] = mp(u, type[e]);
				pq.push(mp(-(dis[v] + f[v]), v));
			}
		}
	}
}

int main() {
	init(); vector<int> p; p.resize(9);
	for (int i = 0; i < 9; i++) {
		char c[5]; scanf("%s", c);
		if (c[0] == 'x') p[i] = 9;
		else p[i] = c[0] - '0';
	}
	int S = cantor(p), T = 0;
	for (int i = 0; i < fac[9]; i++) {
		p = cantorRev(i); int pos = 0;
		for (int j = 0; j < 9; j++) if (p[j] == 9) { pos = j; break; }
		if (pos - 3 >= 0) { // U
			swap(p[pos - 3], p[pos]);
			addedge(i, cantor(p), 0);
			swap(p[pos - 3], p[pos]);
		}
		if (pos + 3 < 9) { // D
			swap(p[pos + 3], p[pos]);
			addedge(i, cantor(p), 1);
			swap(p[pos + 3], p[pos]);
		}
		if (pos > 0 && pos / 3 == (pos - 1) / 3) { // L
			swap(p[pos - 1], p[pos]);
			addedge(i, cantor(p), 2);
			swap(p[pos - 1], p[pos]);
		}
		if (pos < 8 && pos / 3 == (pos + 1) / 3) { // R
			swap(p[pos + 1], p[pos]);
			addedge(i, cantor(p), 3);
			swap(p[pos + 1], p[pos]);
		}
	}
	for (int i = 0; i < fac[9]; i++) f[i] = manhattan(i);
	Astar(S, T);
	if (!vis[T]) puts("unsolvable");
	else {
		string ans;
		for (int u = T; u != S; u = last[u].first) ans.push_back(dir[last[u].second]);
		reverse(ans.begin(), ans.end());
		printf("%s\n", ans.c_str());
	}
	return 0;
}
改成数组就能过了
展开查看 AC 代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <cmath>
#include <queue>
#include <algorithm>
using namespace std;
#define mp make_pair
typedef pair<int, int> pii;
const int maxn = 500005;
const int inf = 0x3f3f3f3f;
int vis[maxn], dis[maxn], f[maxn];
int fac[10];
char dir[4] = {'u', 'd', 'l', 'r'};
int head[maxn], nxt[maxn << 2], tail[maxn << 2], type[maxn << 2], ecnt;

void init() {
	fac[0] = 1;
	for (int i = 1; i <= 9; i++) fac[i] = fac[i - 1] * i;
	memset(head, 0, sizeof(head));
}

int cantor(int *p) {
	int ret = 0;
	for (int i = 0; i < 9; i++) {
		int rnk = p[i]; for (int j = 0; j < i; j++) if (p[j] < p[i]) rnk--;
		ret += (rnk - 1) * fac[8 - i];
	}
	return ret; // rank in [0, 9! - 1]
}

int mmp[5][5], mmpT[5][5];
int manhattan(int *p) {
	int ret = 0;
	for (int i = 0; i < 9; i++) mmp[(i / 3) + 1][(i % 3) + 1] = p[i];
	for (int i = 0; i < 9; i++) mmpT[(i / 3) + 1][(i % 3) + 1] = i;
	for (int i = 1; i <= 3; i++) for (int j = 1; j <= 3; j++)
		for (int x = 1; x <= 3; x++) for (int y = 1; y <= 3; y++)
			if (mmp[i][j] == mmpT[x][y]) ret += abs(i - x) + abs(j - y);
	return ret;
}

void addedge(int u, int v, int t) {
	nxt[++ecnt] = head[u];
	head[u] = ecnt;
	tail[ecnt] = v;
	type[ecnt] = t;
}

pii last[maxn];
void Astar(int S, int T) {
	memset(dis, inf, sizeof(dis));
	memset(vis, 0, sizeof(vis));
	priority_queue<pii> pq;
	dis[S] = 0; pq.push(mp(-(0 + f[S]), S));
	while (!pq.empty()) {
		pii cur = pq.top(); pq.pop();
		int u = cur.second;
		if (vis[u]) continue; vis[u] = 1;
		if (u == T) break;
		for (int e = head[u]; e; e = nxt[e]) {
			int v = tail[e];
			if (dis[v] > dis[u] + 1) {
				dis[v] = dis[u] + 1;
				last[v] = mp(u, type[e]);
				pq.push(mp(-(dis[v] + f[v]), v));
			}
		}
	}
}

int main() {
	init(); int p[9];
	for (int i = 0; i < 9; i++) {
		char c[5]; scanf("%s", c);
		if (c[0] == 'x') p[i] = 9;
		else p[i] = c[0] - '0';
	}
	int S = cantor(p), T = 0, id = 0;
	for (int i = 0; i < 9; i++) p[i] = i + 1;
	do {
		int pos = 0;
		for (int j = 0; j < 9; j++) if (p[j] == 9) { pos = j; break; }
		if (pos - 3 >= 0) { // U
			swap(p[pos - 3], p[pos]);
			addedge(id, cantor(p), 0);
			swap(p[pos - 3], p[pos]);
		}
		if (pos + 3 < 9) { // D
			swap(p[pos + 3], p[pos]);
			addedge(id, cantor(p), 1);
			swap(p[pos + 3], p[pos]);
		}
		if (pos > 0 && pos / 3 == (pos - 1) / 3) { // L
			swap(p[pos - 1], p[pos]);
			addedge(id, cantor(p), 2);
			swap(p[pos - 1], p[pos]);
		}
		if (pos < 8 && pos / 3 == (pos + 1) / 3) { // R
			swap(p[pos + 1], p[pos]);
			addedge(id, cantor(p), 3);
			swap(p[pos + 1], p[pos]);
		}
		f[id] = manhattan(p);
		id++;
	} while (next_permutation(p, p + 9));
	Astar(S, T);
	if (!vis[T]) puts("unsolvable");
	else {
		string ans;
		for (int u = T; u != S; u = last[u].first) ans.push_back(dir[last[u].second]);
		reverse(ans.begin(), ans.end());
		printf("%s\n", ans.c_str());
	}
	return 0;
}
时间复杂度 $O(9!\log 9!)$

反思与总结

我是上来就将整个图建好的,虽然改成了数组,但是运行速度还是堪忧。
看了网上的其它解法,好像只要边跑 \(A*\) 边建图即可,因为图中大部分点都是访问不到的。
这题判无解有个基于逆序对的高级思想,我并没有使用,因为 \(A*\) 完全跑的完,只要最后判一下终点有没有被访问过即可(其实是因为我不会证明这个结论,就不放上来献丑了)。

posted @ 2022-04-16 13:05  alfayoung  阅读(31)  评论(0编辑  收藏  举报