雙向BFS和啟發式搜尋的應用

2022-05-24 11:51:10 字數 4690 閱讀 2841

題目鏈結 p5507 機關

有12個旋鈕,每個旋鈕開始時處於狀態 \(1\) ~ \(4\) ,每次操作可以往規定方向轉動乙個旋鈕 (\(1\rightarrow2\rightarrow3\rightarrow4\rightarrow1\)) ,並且會觸發一次連鎖反應:處於某個狀態的旋鈕在旋轉時會引起另乙個旋鈕發生相同方向的轉動(另乙個旋鈕轉動不會再觸發連鎖反應)。問將12個旋鈕都置為 \(1\) 至少需要幾次轉動,並輸出每次轉動的旋鈕編號。

直接暴力地進行單向 \(\mathrm\) ,每次轉動都有 \(12\) 種選擇,時間複雜度是 \(o(12^})\) ,看資料範圍,最高的步數可達 \(17\) 步,必定 \(\mathrm\) 。但是這樣簡單如果優化的比較好可以得 \(50\) ~ \(60\) 分(沒吸氧氣,吸了氧氣反而更低了)。

單向bfs評測記錄

超時的主要原因是搜尋樹過於龐大,而我們會發現本題起始狀態和終止狀態是明確的,這時我們就可以使用神奇的雙向 \(\mathrm\) 來限制搜尋樹的生長。

雙向 \(\mathrm\) 非常適合在起始狀態和終止狀態明確的條件下使用,做法是從起點和終點同時進行單向 \(\mathrm\) ,讓兩邊 \(\mathrm\) 搜尋樹的生長受到對面搜尋樹的限制,不要過於野蠻生長,偏離目標太遠。自己畫了一張很醜很醜的對比圖,應該可以便於理解。

可以看到雙向 \(\mathrm\) 可以在某一狀態發現相同時就停止了,通過回溯可以找到沿路選擇的點。再看看本題的資料範圍,最大的點正向和反向 \(\mathrm\) 最多是 \(9\) 步, \(12^9\) 是 \(5\times10^8\) 的量級,勉強可以在一秒衝過去。事實上我最大的點用時在 \(200ms\) ~ \(300ms\) 之間,還是很穩的。

最好的一次雙向bfs記錄

可以把兩個二進位制位當做乙個**制位,把每個旋鈕狀態減一後就剛好可以存下了,即1對應0,2對應1,以此類推。先講一下讀入處理。

int button,start = 0;

for(i,0,11)

我**中的旋鈕編號和狀態全部進行了減一處理(後面描述時我都會說+1),方便位運算操作。注意記錄初始狀態時要將 \(i*2\)(即左移一位),因為我們把兩個二進位制位當做乙個**制位了,後面也有這樣的乘2處理。再用乙個陣列 \(nxt\) 記錄第 \(i+1\) 個旋鈕在 \(j+1\) 狀態下進行旋轉時,會帶動第 \(nxt[i][j]+1\) 個旋鈕轉動。

首先正向和反向的 \(\mathrm\) 的轉移方式是不一樣的。設當前轉到的是第 \(i+1\) 個旋鈕,它現在處於 \(j+1\) 狀態。

我們把正向方向定義為1,反向方向定義為2,當前方向為 \(\mathrm\) ,當前所有按鈕狀態為 \(\mathrm\) ,有:

int si,snext,nx,nextstate;

for(i,0,11) else

}

\(code:\)

#include using namespace std;

#define for(i,sta,en) for(int i = sta;i <= en;i++)

inline int read()

while(isdigit(ch)) return sum * fu;

}const int n = 1<<24;

bool vis[n];

int nxt[14][6],fa[n],choice[n],v[n],flag,m1,m2,mid,ans1[30],ans2[30];

queueq;

int main()

vis[start] = vis[0] = 1; //是否訪問過

v[start] = 1; v[0] = 2; //區分方向

q.push(start);

q.push(0);

while(!q.empty() && !flag) else

//如果這個狀態在之前訪問過

if(vis[nextstate])

vis[nextstate] = 1;

v[nextstate] = direction; //繼承方向

fa[nextstate] = state; //用於回溯操作

choice[nextstate] = i + 1; //記錄本次操作

q.push(nextstate);}}

int cnt1 = 0,state = m1,cnt2 = 0;

//正向回溯

while(state != start)

//逆向回溯

state = m2;

while(state != 0)

//總步數,還要加上中間那一步mid操作

printf("%d\n",cnt1+cnt2+1);

for(int i = cnt1; i; i--) printf("%d ", ans1[i]);

printf("%d ",mid);

for(i,1,cnt2) printf("%d ", ans2[i]);

return 0;

}

雙向 \(\mathrm\) 已經夠快了,但是我們可以使用更快的啟發式搜尋。常用的啟發式搜尋有 \(\mathrm\) 和 \(\mathrm\) ,聽說前者被卡了,我們就用 \(\mathrm\) 吧。這裡先介紹這個演算法的實現,然後給出正確性證明。

在 \(\mathrm\) 演算法中,我們要利用當前狀態的資訊對狀態進行評價,以此來決定下一次的操作,極大地限制了搜尋樹的生長。這裡用函式 \(f^*\) 來表示 \(x\) 狀態的代價:\(f^*(x)= g(x)+h^*(x)\)。其中 \(g (x)\) 表示從初始狀態到當前狀態所付出的最小代價(在本題中意義為操作步數),而 \(h^*(x)\)是從當前狀態到目標狀態走最佳路徑所付出的代價。在實際**中我們使用的其實是 \(f(x)=g (x)+h(x)\) ,因為我們實際上是不知道這個 \(h^*(x)\) 的,但是我們可以用 \(h(x)\) 去估計它,要保證 \(h(x)\) 是 \(h^*(x)\) 的下界,即對任意狀態均有 \(h(x)≤h^*(x)\) ,這個估計能保證搜尋正確性。

在**中我們用以下方式估計 \(h^*(x)\) : \(12\) 個旋鈕在不考慮牽連時都轉到 \(1\) 要多少步,再除以 \(2\),這樣就可以保證得到的 \(h(x)\) 肯定會比實際要轉的次數 \(h^*(x)\) 要少(一次操作恰好就可以讓兩個旋鈕都向目標狀態轉一次,而實際上可能會讓某個旋鈕轉過目標狀態,從而要轉更多次數)。

\(h(x)\) 是乙個比較玄學的東東,沒有唯一的定義,不同的定義可能會導致程式執行效率和結果不同,這題中你還可以乘乙個係數給他,能明顯加快執行效率。經過筆者多次測試,發現給 \(h\) 乘上係數從 \(1.1\) ~ \(2.3\) 都能 \(\mathrm\) 這道題,但是乘 \(2.4\) 時會 \(\mathrm\) 掉乙個點。變化趨勢是這個係數越大,跑得越快,最大的點可以跑進 \(100ms\) 。這是因為係數越大越接近真實值 \(h^*(x)\),但是更大的係數不能保證 \(h(x)≤h^*(x)\)。

**實現類似 \(\mathrm\) 演算法,定義乙個結構體存狀態和這個狀態對應的估價函式值 \(f\) 。每次從小根堆中取出 \(f\) 最小的狀態進行轉移,存狀態和轉移狀態的操作和上面雙向 \(\mathrm\) 相同,這裡直接給出**,證明放後面。

\(code:\)

#include using namespace std;

#define for(a,sta,en) for(int a = sta;a <= en;a++)

inline int read()

while(isdigit(ch)) return sum * fu;

}const int n = 1<<24;

int g[n],nxt[14][6],fa[n],ans[30],choice[n];

struct node

bool operator

};priority_queueq;

int main()

q.push(node(start)); //呼叫建構函式,順便計算出估價函式值

g[start] = 0;

while(!q.empty())}}

int cnt = 0,state = 0;

while(state != start)

printf("%d\n",cnt);

for(int i = cnt;i;i--) printf("%d ",ans[i]);

return 0;

}

證明正確性,只需證明 \(\mathrm\) 在選擇到目標節點 \(t\) 時(即在優先佇列 \(top\) 時),目標節點的最優解已找到。

令 \(x\) 為任意擴充套件到的節點(優先佇列中的元素),有 \(f(t)\le f(x)\) 。

做完這道題建議去做一下 p1379 八數碼難題 ,可以同時用單向 \(\mathrm\) ,雙向 \(\mathrm\) ,\(\mathrm\) 和 \(\mathrm\) 做這道題,如果每個方法都寫一下一定受益良多

啟發式搜尋

啟發式搜尋 heuristically search 又稱為有資訊搜尋 informed search 它是利用問題擁有的啟發資訊來引導搜尋,達到減少搜尋範圍 降低問題複雜度的目的,這種利用啟發資訊的搜尋過程稱為啟發式搜尋。例題 八數碼問題 運用優先佇列,根據目前已經確定的位置算出目前的價值,並匯入...

啟發式搜尋

啟發式搜尋 啟發式搜尋就是在狀態空間中的搜尋對每乙個搜尋的位置進行評估,得到最好的位置,再從這個位置進行搜尋直到目標。這樣可以省略大量無謂的搜尋路徑,提高了效率。在啟發式搜尋中,對位置的估價是十分重要的。採用了不同的估價可以有不同的效果。在啟發式搜尋中,我們每次找到當前 最有希望是最短路徑 的狀態進...

啟發式和元啟發式的區別

啟發式策略 heuristic 是一類在求解某個具體問題時,在可以接受的時間和空間內能給出其可行解,但又不保證求得最優解 以及可行解與最優解的偏離 的策略的總稱。許多啟發式演算法是相當特殊的,依賴於某個特定問題。啟發式策略在乙個尋求最優解的過程中能夠根據個體或者全域性的經驗來改變其搜尋路徑,當尋求問...