深度优先搜索(depth first search,简称深搜)是一种极其常用的算法,简单来说,符合以下策略的就可以称为深度优先搜索。
在图中行走,没有走过的点称为“新点”,所有走过的点称为“旧点”。开始时所有的点都是新点,从任意节点1出发,走向任意一个新节点,同时将新节点标记为旧节点,然后重复此步骤。如果发现不能再走到下一个新节点,则回退到上一个走过来的节点,重复此步骤,直至可以找到新节点为止。如果走到了目标节点,则返回true;如果直到回退到节点1还是无法找到新节点,则称不存在节点1到目标节点的路径。
可以用以下伪码来表示:
bool Dfs(v){
if(v为终点)
return true
else if(v为旧点)
return false
//v为新点
v = 旧点
对和v相邻的每个节点U{
if(Dfs(U) == true)
return true
}
return false
}
如果仅仅要求遍历一个图而不需要寻找路径,则可以这样改动
Dfs(v){
if(v为旧点)
return false
v = 旧点
对和v相邻的每个节点U{
if(Dfs(U) == true)
return true
}
return false
}
int main(){
将所有点都标记为新点
while(能够找到新点){
Dfs(k)
}
}
如果要求需要记录走到终点的路径,则算法可以简单改动为以下
Node path[MAX_LEN]; //path表示走过的路径
int depth; //depth表示当前深度
bool Dfs(v){
if(v为终点)
return true;
else if(v为旧点)
return false;
//v为新点
v = 旧点;
++depth;
对和v相邻的每个节点U{
if(Dfs(U) == true)
return false;
}
--depth; //无法再找到可以走的新点,回溯
return false;
}
1.邻接表
使用vector数组来表示每个点连接的边,边的信息包含另一个节点还可能包含边的权值等。
优点:对于边较为稀少的图(大部分的图边都是比较稀少)可以节省空间和时间,算法复杂度大概为O(n+e)
缺点:对于边很稠密的图,由于每个点都要表示,所以边比较稠密时(e很大)反而不如邻接矩阵的O(n^2)来的划算
2.邻接矩阵
直接使用一个二维数组来表示边,例如v[i][j]表示i节点和j节点之间的边,一般来说值为true或false,有权值时可以写一个struct来加上权值
优点:对于边很稠密的图,由于每个点都要表示,邻接矩阵的O(n^2)来比较划算
缺点:大部分情况下边稀疏时比较浪费空间
总结:一般情况下都使用邻接表,极少数情况下使用邻接矩阵,但有时追求速度时也可以用邻接矩阵来写(比较方便)
其实Dfs的本质也就是在图上找路,可能是找一条/找多条,也可能是找最优解,但本质不变。很多题看起来似乎与Dfs毫不相关,但其实仔细分析之后其实都可以转化为Dfs问题,不一定是题中明确指出了一幅图才想起来用Dfs,很多时候图中的节点和状态并没有那么直观。
构成图的结点也可以称为状态,一副图也就是一个状态空间。起点就是初始状态,终点就是目标状态。一个状态可以通过转移到下一个状态,多个状态又对应多个状态(废话。一对一或者一对多还用得着Dfs吗),我们按照枚举的思想遍历每个联通的路就行了。所以寻找一个状态,用合适的语言来描述一个状态是最关键的一步。Dfs不同于动态规划,在转移的时候并不会太过复杂。
举两个例子。
迷宫问题
如下图的一个小迷宫,像连连看一样输入两坐标,连接寻找两坐标的最小线段数(也就是拐了几个弯+1)。
这个题乍一看似乎不太像Dfs,但其实就是。我们来思考一下“状态”会是什么?首先我们需要起点终点坐标x1,y2,x2,y2,然后还需要一个方向,因为答案就是输出方向变了几次,再需要一个当前步数,因为答案要求输出最小步数。
这样一来就成了Dfs(int x1, int y1, int x2, int y2, int curentStep, int direction)
我们的初始状态就是Dfs(x1, y1, x2, y2, ∞,-1),目标状态就是Dfs(x2, y2, x2, y2, minStep,..)
而图也就是题中所描述的,可以走到也就是联通的,有墙就是不连通的
染色问题
染色问题经常会出现在DFS的问题中,例如城堡问题,房间问题都需要用到染色的思想,也就是把每个联通的图染成一种颜色,多个不连通的图染为不同颜色。一般会要求求极大联通子图。
例题:Poj2815 城堡问题https://vjudge.net/problem/OpenJ_Bailian-2815
题解:https://blog.csdn.net/a1097304791/article/details/81944400
剪枝
剪枝是一种常用甚至必须使用的加快搜索速度办法,通俗来讲也就是预判到了某一钟情况再继续下去已经一定得不到想要的结果,就可以提前剪掉,像给树之间剪掉树枝那样。
一般有以下几种。
1.可行性剪枝:提前预判继续走下去能不能走到终点,如果不能,就直接剪掉。
2.最优性剪枝:提前预判这样走下去会不会是最优解,如果不是,就直接剪掉
具体的剪枝方案要针对每个题来看,往往很多题目如果找不出来足够多的剪枝方案的话根本就过不去,而往往最难找到的就是最优性剪枝。
例题:Poj1724 寻路问题https://vjudge.net/problem/POJ-1724
题解:https://blog.csdn.net/a1097304791/article/details/81947263
搜索顺序
在搜索的时候往往要讲究顺序,不同的顺序会导致搜索快慢的天壤之别。举个例子来说,当你玩七巧板的时候,你是先放大的呢,还是先放小的呢?