P1379 八數碼難題 題解(雙向寬搜)

博客園同步

原題鏈接

簡要題意:

給定一個 3×33 \times 3 的矩陣,每次可以把空格旁邊(四方向)的一個位置移到空格上。求到目標狀態的最小步數。

前置知識:

單向寬搜的寫法

OK\text{OK},現在我們來考慮雙向寬搜。

假設 AABB 兩個人被困在了迷宮的兩個角落,現在他們首先要互相找到對方;他們都會分身術。你認爲下面哪一種方法最快:

  • AA 主動分身去各個路口分支找 BBBB 原地待命。

  • BB 主動分身去各個路口分支找 AAAA 原地待命。

  • AABB 同時分身去各個路口分支找對方。

無可厚非是最後一種方法最快。但請不要誤解:現實生活中我們提倡前兩種方案,因爲現實中沒有人會分身的

誠然,互相找的效率是最高的。可是你可能會問了:

假設一共 44 步找到對方,兩人各走 22 步和一個人找 44 步不是一樣的嗎?

粗想一下,確實如此。但是在 爆炸性指數級的壓力 之下,完全不同。

就在這個問題的基礎上,假設每走 11 步都有 44 種選法(即四方向)。

那麼,一個人找的時間是 44=2564^4 = 256.

兩個人同時找對方的時間是 42+42=324^2 + 4^2 = 32.

數據說明,快了 88 倍。這單是 44 步就快了 88 倍!

經過粗略的計算,假設一共要走 2020 步的話,雙向找比單向快 524288524288 倍,約 5.2×1055.2 \times 10^5,假設時間限制是 1s1s 的話,顯然這兩個程序的分數是有着極大差異的!

這是因爲,雙向搜索在本質上把步數減半了,而 在指數上減半會讓冪大大降低,因此雙向搜索會更快。

那麼雙向搜索適用於哪些題目呢?

  • 明確知道起點和終點的。比方說這種題(現編的):
對於已知的一個數,每次可以將其連續 k 個數字同時 +1.
求讓它至少有 p 位連續相同的步數。

顯然,終點不明確,無法搜索。你不可能把所有的終點都枚舉一遍。

  • 明確知道搜索深度的。即明確知道多少步會走到。比方說 埃及分數
將一個分數分解爲若干分數的和。有一些分母不能使用。

顯然,你不知道會分解成多少個分數。因此本題需要使用 迭代加深搜索(IDDFS) 而非 bfs\text{bfs}.

然後,那你會問了:八數碼這一題,我也不知道最多會有多少步呀?

  • 那麼,你不會自己隨機造嗎?

  • 我怎麼造啊?

  • 用隨機種子搞一個 00 ~ 88 的任意排列,然後取出最大答案啊

  • 我連 std\text{std} 都不會寫啊

哦!對。本題你可以稍微分析一下,你會發現,既然肯定能走到,你的直覺:一定不超過 3030 步。(事實如此)

雙向寬搜如何實現呢?

  • 將起點和終點一起入隊,用 vis\text{vis} 記錄是否訪問過。起點拓展的狀態 vis=1vis = 1,終點拓展的狀態 vis=2vis = 2,否則 vis=0vis = 0. 並用 ansans 記錄當前的步數。

  • 對當前狀態 uu,轉爲矩陣並進行四方向的轉移,形成新的狀態 vv

  • 若當前狀態已搜過,分情況:

    • visu=visvvis_u = vis_v,直接跳過
    • visu+visv=3vis_u + vis_v = 3,輸出 ansu+ansvans_u + ans_v,停止搜索
    • visv0vis_v \not = 0,則 visvvisu,ansvansu+1vis_v \gets vis_u , ans_v \gets ans_u+1,入隊,繼續搜索。
  • 接着入隊,進行下一輪搜索。

  • 重複搜索直到隊列爲空(當然本題保證不會無解,因此隊列不會爲空,但嚴謹地說明一下)或已有答案。

你會注意到 visvisansans 都需要用 map\text{map},這無疑讓我們多掛了兩個 log\log. 假設隊列的一個 log\log,一共有 33log\log.(針對入隊的狀態個數出現了 33log\log) 每次取出狀態需要變爲矩陣,×9\times 9.

那麼假設一共搜到狀態個數是 nn,那麼:

時間複雜度:O(9nlog3n)\mathcal{O}(9n \log^3 n).

這是理論上的硬性分析。這能用最慢點 10ms10ms3131 個點共 149ms149ms 的優秀時間通過,也就說明,nn10310^3 級別的。

如果你不信,代入 n=104n=10^4,可得:

9×104×143=90000×2744=2469600009 \times 10^4 \times 14^3 = 90000 \times 2744 = 246960000‬

大概是 2.4×1082.4 \times 10^8 !你覺得這 10ms10ms 能跑完?

要是能跑完,那洛谷評測機早成 神威 · 太湖之光了,1s1s 那就能跑 2.4×10102.4 \times 10^{10} 了,那還不亂套了,O(n2)\mathcal{O}(n^2) 都可以穩過 10510^5 了!

然而實際,時間複雜度:O(wys)\mathcal{O}(\text{wys}).

實際得分:100pts100pts.

#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;

inline int read(){char ch=getchar(); int f=1; while(ch<'0' || ch>'9') {if(ch=='-') f=-f; ch=getchar();}
	int x=0; while(ch>='0' && ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*f;}

inline void write(int x) {
	if(x<0) {putchar('-');write(-x);return;}
	if(x<10) {putchar(char(x%10+'0'));return;}
	write(x/10);putchar(char(x%10+'0'));
}

int n,end=123804765,a[4][4];
const int dx[4]={0,0,1,-1};
const int dy[4]={1,-1,0,0};
queue<int> q;
map<int,int> vis,ans;

inline void bfs() {
	if(n==end) {puts("0");return;}
	q.push(n); q.push(end);
	ans[n]=0; ans[end]=1;
	vis[n]=1; vis[end]=2; //初始化
	while(!q.empty()) {
		int now=q.front(),fx,fy; q.pop();
		int t=now; /*printf("%d\n",now);*/
		for(int i=3;i>=1;i--) for(int j=3;j>=1;j--) {
			a[i][j]=now%10,now/=10;
			if(!a[i][j]) fx=i,fy=j; //轉化爲矩陣
		} for(int i=0;i<4;i++) {
			int nx=fx+dx[i],ny=fy+dy[i];
			if(nx<1 || nx>3 || ny<1 || ny>3) continue;
			swap(a[fx][fy],a[nx][ny]); now=0;
			for(int j=1;j<=3;j++)
			for(int k=1;k<=3;k++) now=now*10+a[j][k]; //再轉回來
			if(vis[now]==vis[t]) { //搜過
				swap(a[fx][fy],a[nx][ny]); //換回來
				continue;
			} if(vis[now]+vis[t]==3) {
				printf("%d\n",ans[t]+ans[now]); //記錄答案
				return;
			} ans[now]=ans[t]+1; vis[now]=vis[t];
			q.push(now); swap(a[fx][fy],a[nx][ny]); //入隊,記錄,換回
		}
	}
}

int main() {
	n=read(); bfs();
	return 0;
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章