入門教程 高級搜索

高級搜索

信息

題解進度

Solved # Title Editorial
Solved A 【ICPC 2004 上海站】 【題解】The Rotation Game
B 【ICPC 2006 橫濱站 日本】
C 【SCOI 2005 四川】
D 【HDU 2006 校賽】
E 【ICPC 2001 坎普爾站 印度】
F 【ICPC 2001 大田站 韓國】
G 【ICPC 1997 烏爾姆站 德國】
Solved H 【ICPC 1996 烏爾姆站 德國】一般搜索 雙向BFS 【題解】Knight Moves
Solved I 【HDU 2011 校級賽】奇怪搜索 雙向BFS 【題解】Nightmare Ⅱ
Solved J 【ICPC 1998 SCUSA站】八數碼 IDA* 【題解】Eight
Solved K 【HDU 2017 暑期多校】次短路 A* 【題解】Two Paths
Solved L 【ICPC 2017 瀋陽站 網絡賽】K短路 A* 【題解】Made In Heaven
Solved M 【CCPC 2019 網絡賽】不定點K短路 想法題 【題解】path

通知欄

3月30-4月5日這周的新生訓練準備訓練高級搜索,就是講雙向廣度搜索,迭代加深、A*算法,IDA*,你能負責安排嗎?

第七週 高級搜索

一、視頻:

1)搜索相關:

https://github.com/luoyongjun999/code/tree/master/%E8%A1%A5%E5%85%85%E8%B5%84%E6%96%99

2)A*:

https://www.bilibili.com/video/BV1D4411X71L

3)IDA*:
https://www.bilibili.com/video/BV1K4411R78Y

// 看前面一段簡介就差不多了

// 可能去網上看博客會有更多理解

二、專題訓練賽:

https://vjudge.net/contest/363902

// 可能會考慮陸續更一波題解

三、綜合訓練賽:
https://vjudge.net/contest/363899

雙向廣度搜索 (Bidirectional BFS)

這個其實是BFS的優化技巧,可以證明,從兩個起點開始BFS,比單點開始要優化很多。

所有BFS過的題,你都可以寫成雙向的(首先看題目時間卡不卡,其次看實現麻不麻煩)。

  • https://www.geeksforgeeks.org/bidirectional-search/

僞代碼

	BFS (queue_$){
        q.swap(queue)          //一個關鍵
        while (!q.empty()) {
            從q取出隊首點
            
            if 遇到另一個點集:
            	return 有解,距離
            
            for () {
                 dr := r + d
                 dc := c + d
                判斷合法性
                新點插入 queue_$ //另一個關鍵
       			更新標記數組,vis,dist
            }
        }
        return 無解
    };

	BiBFS(){
        res 
        queue_s	
        queue_t	
        分別插入對應的起點
        更新標記數組,vis,dist
            
        while 兩者不都爲空 :
        	// 這裏有點啓發式的思想,每次更新點集更小的隊列
            if s的狀態點數多於t,且queue_t非空 :
                res = BFS(queue_t);
            else :
                res = BFS(queue_s);
        
            if (有解且相遇) return res;
        }
        return 無解
    };

BBFS相關習題


【題解】Knight Moves

hdu 1372 - Knight Moves

題意:

就問你從棋盤上一個位置到另外一個位置的最短路。

思路:

我覺得你只要看懂了上面的僞代碼的話,應該就能寫了。

甚至如果你厲害的話,普通搜索也能過(不確定

[我的代碼 1372 BBFS.cpp](https://github.com/TieWay59/HappyACEveryday/blob/master/2020codes/hdu/1372 BBFS.cpp)


【題解】Nightmare Ⅱ

hdu 3085 - Nightmare Ⅱ

模型是方格地圖,有兩個人M和G要見面,每個人只能四向移動。其中M可以一單位時間移動三次,G可以一單位時間移動一次。圖上一開始有兩個Z,這個Z每個單位時間會分裂,佔滿曼哈頓距離爲2的所有位置。Z可以覆蓋X,但是MG不可以走到XZ。每一時刻M或G移動前,看作Z已經分裂完畢。注意,題意隱含:MG被Z覆蓋也會無法行動。

思路:這個提示也不算傳統的寬搜,不是很入門,可能還有點入墳。

  • 一開始我以爲,M的移動是可以確定的,於是把它的移動設成一個dir數組來判。後來才意識到,M移動的過程中三次位移每一次都不能落在Z上,那麼如果是有個第二次落腳的位置恰變成Z了,那麼後續的第三個位置是M達不到的。這告訴你,這個題非常的有鬼。
  • 由於G和Z都是單位時間變化的,M又有移動的特殊性,整體做法還是以雙向寬搜爲主,但是,M的每次移動都要單獨拿出來判,具體地說就是要有三次while(!queue.empty())給M點判移動。
  • 然後,由於人必須是同一個單位時間內相遇,而且每個時間人都必須移動,所以這不是常規的最短路問題,而是枚舉單位時間,更新所有上個時刻的位置,到下個位置,如果可以到達,如果在這個單位時間內,M首次走到G經過的位置(反之等價),可以說這就是相遇的最少時間。

我的代碼 3085 Bidirectional BFS.cpp

迭代加深 ID(Iterative Deepening), IDDFS

這個算法代碼上只是DFS稍微添加點東西而已。

迭代加深搜索,實際上就是做很多次深搜,同時逐漸提高每次深搜的深度限制。直到找到答案就結束。

可以證明,這樣做可以在時間上接近BFS,在空間上接近DFS,可以避免兩種基本搜索算法的極端。

在目標不深,分支很多的情況下,運行表現比較好。

A* (A_star)

這個算法的思想其實也不復雜,但是代碼細節多點。

A星算法的啓發性在於規定一個估價函數,來猜想一個點到目標的後續花費。

可以是不準的猜想,比如在方格模型中,用曼哈頓距離計算。

在代碼上的表現,常常和bfs,反向圖,最短路有關。

於是**“後繼節點”就可以通過“已有花費+可能的後續花費”**的某個函數來比較。

特別的是,這個算法不太用在算法競賽,而在遊戲開發中更有作用。

第K短路問題

就是求某個點到某個點的第k短的路徑長度。(我們所說的最短路一般就是K=1的情況)

放在這裏講也是因爲有一種Astar的簡單做法。

Astar做法的K短路不算太難,每一步都很清晰:

  1. 首先建立反向圖(就是所有邊和你原來的圖相反的圖,無向圖就免去這一步)
  2. 然後在反向圖從終點開始做一遍單源最短路(可以時dijkstra)
  3. 從起點開始做優先隊列優化的BFS。
  4. Astar的體現在於優先隊列中的節點的優先級和用啓發函數要有關。
  5. 也就是按照f(u)=dis_start(u)+dis_final(u)從小到達排序。
  6. 當第K次遇到終點的時候,當前的距離就是K短路。

是不是看起來很暴力?確實很暴力,記住這樣做的最壞複雜度是O(KNlog(N))O(KNlog(N))的。

  • https://oi-wiki.org/graph/kth-path/
  • 世界上有很低複雜度的科技O(Nlog(N)+Mlog(M)+Klog(K))O(Nlog(N)+Mlog(M)+Klog(K)),但是代碼很長很長,並且很難理解:https://www.isi.edu/natural-language/people/epp-cs562.pdf https://www.isi.edu/natural-language/people/epp-cs562.pdf

K短路相關習題


【題解】Two Paths

hdu 6181- Two Paths

題意:

這個題是給你無向圖要你求次短路,也就是K=2的情況。

比較一般吧,而且也有別的做法(改寫dijkstra即可)

小心處理爆int的問題。

[我的代碼 6181 K短路(邊權ll).cpp](https://github.com/TieWay59/HappyACEveryday/blob/8e937dc620db56668f565d1e38a337e40a6f4903/2020codes/hdu/6181 K短路(邊權ll).cpp)


【題解】Made In Heaven

計蒜客 A1992 - Made In Heaven

題意:

這是一般的詢問K短路,這個題比較重要,是瀋陽網絡賽的題目。

給大家嘮叨兩句,我們集訓隊暑期隊伍排名,都會參照網絡賽比賽的排名的。

這個題看起來好像不能用普通的Astar做,實際上他題目給你了一個長度剪枝,要充份利用。

[我的代碼 A1992 K短路 Astart 剪枝.cpp](https://github.com/TieWay59/HappyACEveryday/blob/8e937dc620db56668f565d1e38a337e40a6f4903/2020codes/jisuanke/A1992 K短路 Astart 剪枝.cpp)


【題解】path

hdu 6705 - path

題意:

有向圖模型,q次詢問整個圖上任意源匯的第K短路。

q次詢問很多,你當然要預處理出前max(k)的答案。

思路:

枚舉所有路徑是不可能的,我們期望的是,在最少的枚舉次數下,枚舉到儘量短的路徑。

對於一條當前剩下路徑中最短的路徑PcurP_{cur},設這個路徑最後一條邊爲Pcur:E(u,i)P_{cur}:E(u,i) ,表示從uu出發的第ii短的邊。

那麼根據這條路徑,下一條比這個路徑長的路徑可能有(假設存在):

  • P:E(u,i+1)P:E(u,i+1)
  • Pcur+E(to(u,i),0)P_{cur} + E(to(u,i),0)

先不去提這兩者怎麼比較,以及存在性的問題。

假如我們每次拿到一條當前的最短路徑,然後放回這兩種可能的後繼,是不是總能保證工作集合的完備,並且這個集合的體積不會很大。

於是可以總結出,路徑節點的表示(長度,u,i ),以及轉移過程。

剩下的問題是,初始情況應該是怎麼樣的。

我們要保證初始的最短路徑的枚舉集完備,並且後續路徑不會重複。

答:就是所有點的出發的最短邊的集合。(自己思考爲什麼)

具體的實現只需要會優先隊列就可以寫了,沒有什麼固定的章法。

[我的代碼 6705 任意源匯K短路 想法.cpp](https://github.com/TieWay59/HappyACEveryday/blob/a13239ffeaaf5a3252e3aabdec1b158ad25f1674/2020codes/hdu/6705 任意源匯K短路 想法.cpp)

IDA* (Iterative deepening A*)

顧名思義,就是把上面連個算法結合起來的算法。

  • https://blog.csdn.net/xiaonanxinyi/article/details/97896085?depth_1-utm_source=distribute.pc_relevant_right.none-task&utm_source=distribute.pc_relevant_right.none-task
  • 不存在的:wiki

IDA*相關習題


【題解】The Rotation Game

hdu 1667 - The Rotation Game

題意:

給你一個滾動遊戲的模型,8方向可以拉動數組循環滾動1格。問你把中間八個移動到成同一個數的狀態的最短移動方案,如果有多解要輸出字典序最小的解。

第二行要輸出最終格局中間八個數是哪一種。

思路:

首先我們來看怎麼轉化成圖論模型,也就是模擬的部分。

你可以把這個#每一行從左到右,然後從上到下,這樣編號,一個格局就變成一個長度爲24的數組,每個元素都屬於[1,2,3]

數組變成簡單結點的方法有很多,主要就是狀壓或者哈希。

我採用了哈希成一個4進制的大整數的做法。(實際上你可以優化成二進制的狀壓,給大家思考)

state = 0;
for (auto in:input)
    if ('0' <= in && in < '4')
        state = (state << 2) + int(in - '0');

然後狀態怎麼轉移。你想每一次拉動數組,所有元素都朝着一個方向位移,首部的元素放到末尾。這個過程其實可以等價於,把第一個元素冒泡交換到最後一個。

所以你需要的是實現一個子操作,交換。根據上面的編號和狀壓的方式,這個函數不難實現。

    const auto digitSwap = [](ll &s, ll a, ll b) {
        a = 2 * (a - 1);
        b = 2 * (b - 1);
        ll x = (s >> a) & 3;
        ll y = (s >> b) & 3;
        s = s - (x << a) - (y << b)
            + (x << b) + (y << a);
    };

然後就是終點狀態的表示。在這個問題中,我的思路是枚舉最終中間的數字是什麼(也就全1或者2,3)然後把其他數組位置填上0,哈希成一個終點狀態,每次當前狀態都跟這個節點比較即可。

有了以上的理解,你應該可以寫出一個爆搜的做法了,但是這樣會TLE,或者MLE。

怎麼優化呢?

  • 考慮迭代加深,枚舉深度去DFS,每次深度+1保證第一次遇到的答案是最短的。
  • 考慮一個啓發(也就是啓發函數h)你可以假設,中間八個位置,有幾個和目標狀態不一樣,就是最少還差幾步(當然看起來不一定,但這個考慮不會使得方案變差)這樣你就能確定一個狀態是否有可能在你枚舉的步長內達到終點。

有了這兩點優化,就是標準的IDA*的模樣了。

稍微順帶一提,我的狀態是用4進製表示的,其實你也可以通過每次修改起始狀態優化成二進制。因爲你想,如果你的最終狀態中心是1,那麼2和3的位置其實是對你搜索的過程沒有意義的,都可以看成是0。這樣可以進一步優化。

我的代碼 1667 IDAstar.cpp


【題解】Eight

hdu 1043 - Eight

八數碼問題其實是一個老掉牙的麻煩題,思維障礙主要有以下幾點:

  • 無解的結論:可以證明,輸入的數組(除了x以外)逆序對有奇數個,那麼這個八數碼是無解的。
  • 啓發式估價函數:用每個數字(除了x),與目標位置的曼哈頓距離,求和來表示預期的代價。
  • 其他細節:對於有解的情況,移動步數不應該很多(不會上千),但是同一步數的方案可能會很多。
  • 採取搜索方案:BFS空間危險,DFS深度危險,那麼就用迭代加深吧,好像還有估計函數,那就是IDA*了。

如果你已經懂了以上的思考點,並且瞭解了IDA*算法的框架,就可以開始敲了。

其實你不用把IDA*想的太複雜,也不需要去找模板,思路就是:你枚舉限制DFS深度多次去DFS,中間用啓發式估價函數剪枝就好了。這個題我的剪枝是:steps+h(state)>depth,這代表估計到預期的步數超過限制深度。

[我的代碼 1043 IDAstar2.cpp](https://github.com/TieWay59/HappyACEveryday/blob/master/2020codes/hdu/1043 IDAstar2.cpp)

我還去研究了洛谷的另外一個八數碼的題不輸出方案,只要輸出最小步數的。這個題的數據是卡A*的做法的。你看,這個算法多麼尷尬,在一定情況下還很容易被卡掉。

話題之外

我來解釋一下爲什麼要有這麼複雜的算法。

以及爲什麼例題都那麼——陳舊。

你可以先瀏覽一下這篇文章:A*,Dijkstra,BFS算法性能比較及A*算法的應用

上面這些花裏胡哨,看似高級的搜索算法,還是在解決一個老掉牙的問題,最短路。

這裏的“最短路”,不一定是圖形上的最短路徑,還可能是某個事物(首當其衝,滑塊拼圖)的最少次數之類的。

但是像Dijkstra在稠密的圖,或者方格圖上的表現,會遜色很多;還有一些抽象情況(滑塊拼圖)分支龐雜,無法建圖。所以會存在像IDA*針對上述這樣的情況更有效的算法。

但是,也是因爲針對性太特殊了,導致這樣的算法變不出太多的花樣。不是說出不了難題,是很難弄出好題。而且同樣的題可能用其他歪門邪道的方法也可以做出來,比如dfs巧妙剪枝,時間複雜度很難卡出那麼多不同的優化的做法。

所以近五年很少有出這樣的題,歷史上也很難找到類似的題。

但作爲準備者,我不敢保證說以後就不會有了,而且很可能這樣的題目再次現身,會是以很難的題目出現的。(因爲簡單的基礎題大家刷的越來越多了)

看起來,現代的搜索題,更多在花樣上創新,而不會在老花樣上吊胃口了。

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