深度优先搜索(基本款和加强款)

首先深度优先搜索是从图论中延展来的,他是一种适用性非常广泛的算法,同时也是一些高阶算法的组成部分。

先看一道比较简单的深搜题。这题给出的数据范围是1<=N,M<=100000,如果用dfs的话这个时间复杂度足够解决该题。1.用vector邻接数组储存这个图,好处是这个dfs函数可以不用写边界条件也能终止dfs状态 2.用visited[]数组存储各个顶点是否被访问过的标记。要注意的就是这两点,这个时候我们就可以直接写dfs函数了。

做法1:

#include <iostream>
#include <vector>

using namespace std;

const int N=100010;
vector<int> g[N];
int n,m,u,v,ok=1;
int visited[N];
void dfs(int n)
{
    visited[n]=1;//加标记
    for(int i=0;i<g[n].size();i++)
    {
        if(!visited[g[n][i]])dfs(g[n][i]);//要注意i只是该顶点vector的下标,并不是顶点!
    }//不需要还原标记数组了,没有必要
}
int main()
{
    cin>>n>>m;
    while(m--)
    {
        cin>>u>>v;
        g[u].push_back(v);//对g[u]这个容器进行v元素的压入操作
        g[v].push_back(u);//注意这个图是无向图,需要存储双向边
    }
    dfs(1);//从1这个顶点开始遍历
    for(int i=1;i<=n;i++)ok=visited[i]&&ok;//检查是否全部被访问
    if(ok)cout<<"YES"<<endl;
    else cout<<"NO"<<endl;
    return 0;
}

做法2:位运算做法

#include <iostream>
#include <vector>

using namespace std;

const int N=100010;
vector<int> g[N];
int n,m,u,v,ok=0;
int visited[N];
int status_total=0;
void dfs(int n,int status)
{
    for(int i=0; i<g[n].size(); i++)
    {
        if(!(status>>g[n][i]&1))
        {
            if(!(status>>g[n][i]&1))status_total=status_total|1<<g[n][i];
            dfs(g[n][i],status|1<<g[n][i]);
        }
    }
}
int main()
{
    cin>>n>>m;
    while(m--)
    {
        cin>>u>>v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    dfs(1,0);
    if(((status_total+2)>>(n+1))&1)
    {
        cout<<"YES"<<endl;
    }
    else cout<<"NO"<<endl;
    return 0;
}

在这里不建议用status代替visited,因为status的好处是不用去还原现场了,但是如果不想还原现场的话就需要设置一个status_total保存所有结果。

这是一道比较简单的图的深搜题。算是深度优先搜索的基本款。

下面说一下加强版的深度优先搜索。加强版DFS首先增加或者说变化的一点是顶点颜色。我们在基本款代码里使用visited数组来区分顶点,也就是visited[x]==true表示x顶点已经访问过,visited[x]=false表示还没访问过x顶点。加强版中使用"颜色"来区分顶点。

一开始所有的顶点都是白色的,白色代表顶点还没有被访问过,当我们第一次遍历按到一个顶点x时,会把顶点x染成灰色。灰色代表该顶点x已经被访问过(调用过DFS(x)),但是还没有访问结束(DFS(x)退出回溯),当前正在遍历的顶点还是经过x到达的。

如果顶点x的所有相连的顶点都被访问过了,马上要退出DFS(x)向上一层回溯。我们会将x的颜色染成黑色。黑色代表这个顶点的遍历已经结束,之后我们再也不会访问这个顶点。上面这个图就是我们已经遍历了1->2->3->4。并且在4号顶点发现无路可走,回溯到3号顶点时的状态。123是灰色,4是黑色,56是白色。

除了用三种颜色区别顶点的状态,我们还可以给”开始遍历一个顶点"事件和”结束遍历一个顶点“事件都打上一个时间戳,时间戳其实就是一个自增的整数,比如从1开始遍历的时间戳1.然后1->2开始遍历2号节点,时间戳是2。如果2再往后找不到新的顶点,那么2就要回溯,在回溯前会结束时间戳就会被标记为3。像上一个图,我们可以这样记时间戳。

直到整个图遍历结束之后,每个被访问到的顶点都会被打上了两个时间戳,一个是DFS(x)开始的时候,时间戳是D(x),另一个是DFS(x)要结束前,时间戳是F(x)。

其实这个DFS时间戳是很有意思的,有的地方叫做DFS序号。如果我们把每个顶点的时间戳看成区间,左端点是D(x),右端点是F(x),那么这些区间会有像括号一样的嵌套关系。

我们可以看出来任意两个顶点的区间有两个关系:1.两个区间相离2.一个区间包含另一个区间。不会出现像[1,10],[5,16]这样两个区间相互跨立的情况。这种关系可以形象的看成是一个匹配合法的括号序列。

利用DFS序号可以解决很多问题,比如这一道题:

这道题最直观最暴力的做法就是对于每一个询问,从y一直向上找父节点,如果遇到yes,如果一直到找到根节点还没有找到x的话,就输出no。这个算法对于每一个询问的时间复杂度是O(N)的,所以总的时间复杂度是o(NQ)的,这样看的话只能通过30%的数据,因为o(n^2)的算法最多只能解决的数据范围是n<=5000,所以我们可以利用D

FS序号来解决这一道题。我们可以发现x是y的祖先当且仅当x的区间包含y的区间。所以我们用DFS得到时间戳D(x)和F(x)之后这题就变得非常简单了。判断D(x)D(y)F(x)F(y)的大小关系,时间复杂度是o(1),所以Q个询问的时间复杂度是O(Q)的。再加上DFS一遍的时间复杂度是O(N)(因为是树,边数也是N),算法总体的时间复杂度是O(N+Q)的,显然比之前的O(NQ)快很多。现在我们来看看这题的完整代码。

#include <iostream>
#include <vector>
using namespace std;

const int N=100001;
vector<int> v[N];//vector容器
int n,q,t[N],f[N],ts=0;
void dfs(int x)
{
    ts++;//时间戳
    t[x]=ts;//开始时间戳
    for(int i=0;i<v[x].size();i++)
    {
        int y=v[x][i];
        dfs(y);//遍历下一个顶点(不需要再判断这个顶点是否被访问过了,没有必要)
    }
    ts++;
    f[x]=ts;//结束时间戳
}
int main()
{
    int m,l,x,y;
    cin>>n>>q;
    for(int i=2;i<=n;i++)
    {
        cin>>m>>l;
        v[m].push_back(l);//输入图
    }
    dfs(1);//从1这个根节点开始dfs
    while(q--)//询问
    {
        cin>>x>>y;
        if(t[x]<=t[y]&&f[x]>=f[y])cout<<"YES"<<endl;//祖先判断
        else cout<<"NO"<<endl;
    }
    return 0;
}

这里我们需要注意的一点是,我们不需要再判断这个顶点是否被访问过。因为它是一棵树,也就是有向连通图,一个顶点只会被访问一次(去掉他回溯访问的那次)。

这就是加强版的DFS算法。DFS序号是一个很有用的东西,在中高阶的算法中会经常出现。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值