簡要題意:
給定一個 的矩陣,每次可以把空格旁邊(四方向)的一個位置移到空格上。求到目標狀態的最小步數。
前置知識:
,現在我們來考慮雙向寬搜。
假設 和 兩個人被困在了迷宮的兩個角落,現在他們首先要互相找到對方;他們都會分身術。你認爲下面哪一種方法最快:
-
主動分身去各個路口分支找 , 原地待命。
-
主動分身去各個路口分支找 , 原地待命。
-
和 同時分身去各個路口分支找對方。
無可厚非是最後一種方法最快。但請不要誤解:現實生活中我們提倡前兩種方案,因爲現實中沒有人會分身的。
誠然,互相找的效率是最高的。可是你可能會問了:
假設一共 步找到對方,兩人各走 步和一個人找 步不是一樣的嗎?
粗想一下,確實如此。但是在 爆炸性指數級的壓力 之下,完全不同。
就在這個問題的基礎上,假設每走 步都有 種選法(即四方向)。
那麼,一個人找的時間是 .
兩個人同時找對方的時間是 .
數據說明,快了 倍。這單是 步就快了 倍!
經過粗略的計算,假設一共要走 步的話,雙向找比單向快 倍,約 ,假設時間限制是 的話,顯然這兩個程序的分數是有着極大差異的!
這是因爲,雙向搜索在本質上把步數減半了,而 在指數上減半會讓冪大大降低,因此雙向搜索會更快。
那麼雙向搜索適用於哪些題目呢?
- 明確知道起點和終點的。比方說這種題(現編的):
對於已知的一個數,每次可以將其連續 k 個數字同時 +1.
求讓它至少有 p 位連續相同的步數。
顯然,終點不明確,無法搜索。你不可能把所有的終點都枚舉一遍。
- 明確知道搜索深度的。即明確知道多少步會走到。比方說 埃及分數:
將一個分數分解爲若干分數的和。有一些分母不能使用。
顯然,你不知道會分解成多少個分數。因此本題需要使用 迭代加深搜索(IDDFS) 而非 .
然後,那你會問了:八數碼這一題,我也不知道最多會有多少步呀?
-
那麼,你不會自己隨機造嗎?
-
我怎麼造啊?
-
用隨機種子搞一個 ~ 的任意排列,然後取出最大答案啊
-
我連 都不會寫啊
哦!對。本題你可以稍微分析一下,你會發現,既然肯定能走到,你的直覺:一定不超過 步。(事實如此)
雙向寬搜如何實現呢?
-
將起點和終點一起入隊,用 記錄是否訪問過。起點拓展的狀態 ,終點拓展的狀態 ,否則 . 並用 記錄當前的步數。
-
對當前狀態 ,轉爲矩陣並進行四方向的轉移,形成新的狀態 。
-
若當前狀態已搜過,分情況:
-
- ,直接跳過
-
- ,輸出 ,停止搜索
-
- ,則 ,入隊,繼續搜索。
-
接着入隊,進行下一輪搜索。
-
重複搜索直到隊列爲空(當然本題保證不會無解,因此隊列不會爲空,但嚴謹地說明一下)或已有答案。
你會注意到 和 都需要用 ,這無疑讓我們多掛了兩個 . 假設隊列的一個 ,一共有 個 .(針對入隊的狀態個數出現了 個 ) 每次取出狀態需要變爲矩陣,.
那麼假設一共搜到狀態個數是 ,那麼:
時間複雜度:.
這是理論上的硬性分析。這能用最慢點 , 個點共 的優秀時間通過,也就說明, 是 級別的。
如果你不信,代入 ,可得:
大概是 !你覺得這 能跑完?
要是能跑完,那洛谷評測機早成 神威 · 太湖之光了, 那就能跑 了,那還不亂套了, 都可以穩過 了!
然而實際,時間複雜度:.
實際得分:.
#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;
}