爱丽丝的幻方 题解

题意

如下图。

Statement

image

image

思路

请先了解 刚体变换

后文一切下标从 \(0\) 开始计算。

读题后不难发现,所有六种操作都是对一个矩阵进行刚体变换。换句话说,在变换过程中,矩阵各元素的相对位置保持不变。一个特例是平移操作,但其本质依然如此,只是需要加上取模。

刻画局面

既然是刚体变换,我们可以考虑通过一个参照点和两个方向向量来刻画一个局面。我们不妨选定数字 \(1\) 为参照。因为幻方合法,所以目标状态中,\(2\) 一定在 \(1\) 右侧,\(n+1\) 一定在 \(1\) 下方。所以我们知道 \(1\to 2\) 的向量 \(\vec{A}\)\(1\to n+1\) 的向量 \(\vec{B}\) 一定垂直。考虑局面总数:\(1\) 所在位置可能有 \(n^2\) 个,而向量 \(\vec{A}\)\(\vec{B}\) 的位置关系有 \(8\) 种(见下图),故最多有 \(8n^2\) 个局面。使用 bfs 求最小距离可以接受。

于是我们这样定义局面:\((id, r, c)\) 表示当前局面中,\(1\) 处在 \((r, c)\) 位置,且向量 \(\vec{A}\)\(\vec{B}\) 组成第 \(id\) 种位置关系。这里 \(8\) 种位置关系可以自由编号。一种编号方式如下:

1 - 2  2 - 1  n+1      n+1  2              2  1 - n+1  n+1 - 1
|          |  |          |  |              |  |              |
n+1      n+1  1 - 2  2 - 1  1 - n+1  n+1 - 1  2              2
 (0)    (1)    (2)    (3)     (4)      (5)      (6)      (7)

局面的转移

那么接下来就该考虑如何实现这六种操作。

首先考虑 \(id\) 的转移。这里没什么技巧,因为编号是人为的。我们只需要手动模拟共 \(48\) 种转移,打表即可。

然后考虑 \((r, c)\) 的转移。根据刚体变换的定义,我们有

\[\vec{p'} = R\cdot \vec{p}+\vec{t} \]

其中 \(\vec{p}\)\(\vec{p'}\) 是变换前后的坐标,看作列向量;\(R\) 是正交矩阵,\(\vec{t}\) 是平移向量。我们只需要对每种操作构造对应的 \(R\)\(\vec{t}\) 即可。

以顺旋操作为例,我们要找 \(R\)\(\vec{t}\) 满足

\[\begin{bmatrix} r' \\ c' \end{bmatrix} = R\cdot \begin{bmatrix} r \\ c \end{bmatrix} +\vec{t} \]

并且我们知道 \(r'=c\)\(c'=n-r-1\),所以我们不难构造出

\[R=\begin{bmatrix} 0 &1 \\ -1 &0 \end{bmatrix} ,\ \vec{t}=\begin{bmatrix} 0 \\ n-1 \end{bmatrix} \]

以此类推,我们可以构造出其他所有操作的 \(R\)\(\vec{t}\) 。列举如下:

  • 右移

    \[R=\begin{bmatrix} 1 &0 \\ 0 &1 \end{bmatrix} ,\ \vec{t}=\begin{bmatrix} 0 \\ 1 \end{bmatrix} \]

  • 下移

    \[R=\begin{bmatrix} 1 &0 \\ 0 &1 \end{bmatrix} ,\ \vec{t}=\begin{bmatrix} 1 \\ 0 \end{bmatrix} \]

  • 转置

    \[R=\begin{bmatrix} 0 &1 \\ 1 &0 \end{bmatrix} ,\ \vec{t}=\begin{bmatrix} 0 \\ 0 \end{bmatrix} \]

  • 纵向镜像

    \[R=\begin{bmatrix} 1 &0 \\ 0 &-1 \end{bmatrix} ,\ \vec{t}=\begin{bmatrix} 0 \\ n-1 \end{bmatrix} \]

  • 横向镜像

    \[R=\begin{bmatrix} -1 &0 \\ 0 &1 \end{bmatrix} ,\ \vec{t}=\begin{bmatrix} n-1 \\ 0 \end{bmatrix} \]

转移时,套用上面的公式即可求出 \((r',c')\)。需要注意的是,\(r'\)\(c'\) 必须时刻对 \(n\) 取模。

去重的处理

这之后还有一个问题需要解决,就是 bfs 如何去重。我们考虑哈希。因为需要处理的参数只有三个,我们使用如下的哈希函数:

\[\text{Hash}(id, r, c) = id\cdot n^2+r\cdot n+c \]

这样最大的哈希值不会超过 \(9001000\),可以用桶数组去重。一个技巧是 bfs 时直接把哈希值存入队列,处理队头时根据上面的公式反推出 \((id, r, c)\)

边界条件

最后是起终点的处理。根据上面的描述,终点是 \((0,0,0)\),哈希值为 \(0\) 。考虑起点。事实上,我们不用记录整个初始矩阵。我们只关心 \(1\)\(2\)\(n+1\) 这三个数的位置。找到他们的位置后,即可算出方向向量 \(\vec{A}\)\(\vec{B}\),继而匹配到对应的 \(8\)\(id\) 之一即可。

复杂度

时间复杂度 \(O(8n^2)\) 。由于哈希函数的设计,空间占用略大,不超过 \(O(9n^2)\)

代码

#include<iostream>
#include<queue>
#include<cstring>
#define fastio ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
#define plus(x) ((x % n + n) % n)
#define MIKU 0
using namespace std;

int T, n;
struct State { int id, r, c; };
int R[6][4] = {					//预处理正交矩阵
	{ 1,  0,  0,  1}, 
	{ 1,  0,  0,  1},
	{ 0,  1,  1,  0},
	{ 0,  1, -1,  0},
	{ 1,  0,  0, -1},
	{-1,  0,  0,  1}
};
int trans[6][8] = {				//预处理 id 的转移
	{0, 1, 2, 3, 4, 5, 6, 7},
	{0, 1, 2, 3, 4, 5, 6, 7},
	{6, 4, 7, 5, 1, 3, 0, 2},
	{7, 5, 6, 4, 0, 2, 1, 3},
	{1, 0, 3, 2, 5, 4, 7, 6},
	{2, 3, 0, 1, 6, 7, 4, 5}
};
int ids[8][4] = {				//预处理 id 的匹配
	{ 0,  1,  1,  0},
    { 0, -1,  1,  0},
    { 0,  1, -1,  0},
    { 0, -1, -1,  0},
    {-1,  0,  0,  1},
    {-1,  0,  0, -1},
    { 1,  0,  0,  1},
    { 1,  0,  0, -1}
};
int t[6][2], dis[9001005];

void init() {
	int tmp[6][2] = { 			//根据 n 处理平移向量
		{0, 1}, 
		{1, 0}, 
		{0, 0}, 
		{0, n-1}, 
		{0, n-1}, 
		{n-1, 0}
    };
	memcpy(t, tmp, sizeof(tmp));
	memset(dis, -1, sizeof(dis));
}

int Hash(State s) {				//计算哈希值
	return s.id * n * n + s.r * n + s.c;
}

int bfs(State s) {
	int res = 0;
	queue<int> q;				//哈希值入队
	q.push(Hash(s)); dis[Hash(s)] = 0;
	while(!q.empty()) {
		int h = q.front(), d = dis[h]; q.pop();
		if(h == 0) { res = dis[h]; break; }	//到达终点
		int c = h % n; h /= n;	//反推三个参数
		int r = h % n; h /= n;
		int id = h;
		for(int i=0; i<6; i++) {
			int nid = trans[i][id];
			int nr = plus(R[i][0] * r + R[i][1] * c + t[i][0]);	//只是矩阵乘法
			int nc = plus(R[i][2] * r + R[i][3] * c + t[i][1]);	//记得取模
			int H = Hash({nid, nr, nc});
			if(dis[H] == -1) {	//去重
				q.push(H); dis[H] = d + 1;
			}
		}
	}
	return res;
}

int cg(int d) {					//处理因平移导致的错位
	if(d < -1) d += n;
	if(d > 1) d -= n;
	return d;
}

void work() {
	cin>>n;
	if(n == 1) {				//特判 n=1
		cout<<0<<'\n'; return;
	}
	init();
	int r = 0, c = 0, r1 = 0, c1 = 0, r2 = 0, c2 = 0;
	for(int i=0; i<n; i++) for(int j=0; j<n; j++) {
		int x; cin>>x;
		if(x == 1) r = i, c = j; 
		if(x == 2) r1 = i, c1 = j;
		if(x == n+1) r2 = i, c2 = j;
	}
	r1 -= r; r2 -= r; c1 -= c; c2 -= c;
	r1 = cg(r1); r2 = cg(r2); c1 = cg(c1); c2 = cg(c2);	//找到方向向量 A 和 B
	int id = 0;
	for(int i=0; i<8; i++) {	//匹配对应的 id
		if(r1==ids[i][0] && c1==ids[i][1] && r2==ids[i][2] && c2==ids[i][3]) {
			id = i; break;
		}
	}
	cout<<bfs({id, r, c})<<'\n';
}

int main() {
	fastio;
	freopen("magic.in", "r", stdin);
	freopen("magic.out", "w", stdout);
	cin>>T;
	while(T--) { work(); }
	return MIKU;
}
posted @ 2026-08-10 21:00  EtherealYz  阅读(4)  评论(0)    收藏  举报