高级搜索
文章目录
信息
题解进度
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相关习题
- 【ICPC 1996 乌尔姆站 德国】hdu 1372 - Knight Moves
- 【HDU 2011 校级赛】hdu 3085 - Nightmare Ⅱ
【题解】Knight Moves
题意:
就问你从棋盘上一个位置到另外一个位置的最短路。
思路:
我觉得你只要看懂了上面的伪代码的话,应该就能写了。
甚至如果你厉害的话,普通搜索也能过(不确定
[我的代码 1372 BBFS.cpp](https://github.com/TieWay59/HappyACEveryday/blob/master/2020codes/hdu/1372 BBFS.cpp)
【题解】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短路不算太难,每一步都很清晰:
- 首先建立反向图(就是所有边和你原来的图相反的图,无向图就免去这一步)
- 然后在反向图从终点开始做一遍单源最短路(可以时dijkstra)
- 从起点开始做优先队列优化的BFS。
- Astar的体现在于优先队列中的节点的优先级和用启发函数要有关。
- 也就是按照
f(u)=dis_start(u)+dis_final(u)
从小到达排序。 - 当第K次遇到终点的时候,当前的距离就是K短路。
是不是看起来很暴力?确实很暴力,记住这样做的最坏复杂度是 O ( K N l o g ( N ) ) O(KNlog(N)) O(KNlog(N))的。
- https://oi-wiki.org/graph/kth-path/
- 世界上有很低复杂度的科技 O ( N l o g ( N ) + M l o g ( M ) + K l o g ( K ) ) 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短路相关习题
- 【HDU 2017 暑期多校】hdu 6181- Two Paths 也就是人人都会的次短路。
- 【ICPC 2017 沈阳站 网络赛】 计蒜客 A1992 - Made In Heaven 也就是人人都会的K短路。
- 【CCPC 2019 网络赛】hdu 6705 - path 不定点K短路 想法题
【题解】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
题意:
这是一般的询问K短路,这个题比较重要,是沈阳网络赛的题目。
给大家唠叨两句,我们集训队暑期队伍排名,都会参照网络赛比赛的排名的。
这个题看起来好像不能用普通的Astar做,实际上他题目给你了一个长度剪枝,要充份利用。
[我的代码 A1992 K短路 Astart 剪枝.cpp](https://github.com/TieWay59/HappyACEveryday/blob/8e937dc620db56668f565d1e38a337e40a6f4903/2020codes/jisuanke/A1992 K短路 Astart 剪枝.cpp)
【题解】path
题意:
有向图模型,q次询问整个图上任意源汇的第K短路。
q次询问很多,你当然要预处理出前max(k)的答案。
思路:
枚举所有路径是不可能的,我们期望的是,在最少的枚举次数下,枚举到尽量短的路径。
对于一条当前剩下路径中最短的路径 P c u r P_{cur} Pcur,设这个路径最后一条边为 P c u r : E ( u , i ) P_{cur}:E(u,i) Pcur:E(u,i) ,表示从 u u u出发的第 i i i短的边。
那么根据这条路径,下一条比这个路径长的路径可能有(假设存在):
- P : E ( u , i + 1 ) P:E(u,i+1) P:E(u,i+1)
- P c u r + E ( t o ( u , i ) , 0 ) P_{cur} + E(to(u,i),0) Pcur+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*相关习题
-
【ICPC 2004 上海站】hdu 1667 - The Rotation Game
-
【ICPC 2006 横滨站 日本】uva 1374 - Power Calculus
-
【HDU 2006 校赛】 hdu 1560 - DNA sequence
-
【SCOI 2005 四川】luogu P2324 - 骑士精神 link
-
【ICPC 2001 坎普尔站 印度】uva 11212 - Editing a Book 源地址
-
【ICPC 2001 大田站 韩国】uva 1603 - Square Destroyer
-
【ICPC 1997 乌尔姆站 德国】uva 529 - Addition Chains 源地址
-
【ICPC 1998 SCUSA站】hdu 1043 - Eight 也就是人人都会的八数码啦。
【题解】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。这样可以进一步优化。
【题解】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巧妙剪枝,时间复杂度很难卡出那么多不同的优化的做法。
所以近五年很少有出这样的题,历史上也很难找到类似的题。
但作为准备者,我不敢保证说以后就不会有了,而且很可能这样的题目再次现身,会是以很难的题目出现的。(因为简单的基础题大家刷的越来越多了)
看起来,现代的搜索题,更多在花样上创新,而不会在老花样上吊胃口了。