入门教程 高级搜索

高级搜索

信息

题解进度

Solved#TitleEditorial
SolvedA【ICPC 2004 上海站】【题解】The Rotation Game
B【ICPC 2006 横滨站 日本】
C【SCOI 2005 四川】
D【HDU 2006 校赛】
E【ICPC 2001 坎普尔站 印度】
F【ICPC 2001 大田站 韩国】
G【ICPC 1997 乌尔姆站 德国】
SolvedH【ICPC 1996 乌尔姆站 德国】一般搜索 双向BFS【题解】Knight Moves
SolvedI【HDU 2011 校级赛】奇怪搜索 双向BFS【题解】Nightmare Ⅱ
SolvedJ【ICPC 1998 SCUSA站】八数码 IDA*【题解】Eight
SolvedK【HDU 2017 暑期多校】次短路 A*【题解】Two Paths
SolvedL【ICPC 2017 沈阳站 网络赛】K短路 A*【题解】Made In Heaven
SolvedM【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 ( 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短路相关习题


【题解】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)的答案。

思路:

枚举所有路径是不可能的,我们期望的是,在最少的枚举次数下,枚举到尽量短的路径。

对于一条当前剩下路径中最短的路径 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*相关习题


【题解】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巧妙剪枝,时间复杂度很难卡出那么多不同的优化的做法。

所以近五年很少有出这样的题,历史上也很难找到类似的题。

但作为准备者,我不敢保证说以后就不会有了,而且很可能这样的题目再次现身,会是以很难的题目出现的。(因为简单的基础题大家刷的越来越多了)

看起来,现代的搜索题,更多在花样上创新,而不会在老花样上吊胃口了。

点击查看源网页
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值