leetcode刷题之DFS专题

参考资料:
深度优先搜索—wikipedia
210课程表II—leetcode官方解答

DFS深度优先搜索算法

DFS(即深度优先搜索)是图论中一种常见的算法,常用于二叉树数据结构,能够实现对树或图中每个节点的遍历。本文将从leetcode的一些例题中详尽介绍这种算法的实现原理和在实际问题中的应用方式。

一、概念

  1. 原理:如其名字,DFS会尽可能深地搜索树的分支。当节点v所在边都已被探寻过,搜索将回溯到发现节点v的那条边的起始节点。
  2. 结束条件:树或图中的所有节点都已被遍历。
  3. 如果还存在未被遍历的节点,则选择其中一个作为源节点并重复以上过程,整个过程反复进行知道所有节点都被遍历为止。

二、实现方法(利用栈)

DFS有广泛的应用,这里为了阐述其方法的具体实现流程,暂以寻找目标值为例子,假设我们的目的是:从一个拓扑结构中找到一个节点的值与target相同

  1. 将根节点放入stack中;
  2. 从stack中取出第一个节点,并检验它是否为target;
    如果是,则结束搜索并回传结果。
    如果不是,则将其某一个尚未检验果的直接子节点加入stack中。
  3. 重复步骤2;
  4. 如果不存在未遍历过的直接子节点,则将上一级的节点加入stack中;重复步骤2;
  5. 重复步骤4;
  6. 如果stack为空,则表示整张图都检查过了,return null。

拓扑排序问题

一、背景

在介绍leetcode的具体题目之前,我们先介绍一类大问题,很多leetcode的题目都具有一定的共性,它们拥有相似的数据结构,类似的输入以及要求的输出,甚至相同的解题技巧。那么针对DFS而言,有一类能解决的大问题便是拓扑排序问题。

定义

对一个有向无环图G进行拓扑排序,是将G中的所有节点排成一个线性序列,使得图中任意一对节点(u,v)(假设边<u, v>属于E(G)),则u在线性序列中出现在v之前。这样的线性序列称为满足拓扑次序的序列,简称拓扑序列
简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序

推论

  1. 如果图G中存在环,那么图G不存在拓扑排序。
    如果有环,u在v前的同时也可以说v在u前面,定义则不成立。
  2. 如果图G是有向无环图,那么它的拓扑排序可能不止一种。
    举例:如果G包含n个节点却没有任何边,那么任意一种编号的排列都可以作为拓扑排序。

求拓扑排序的方法

  1. DFS
  2. BFS
  3. 时间复杂度:均为O(n+m)
    其中n和m分别为有向图G的节点数和边数。
    因为判断G是否存在拓扑排序,至少要对其进行一次完整的遍历,所以不可能优于O(m+n)
    具体如何去实现在后续例题中根据例子进行详解。

课程表问题

在leetcode中,课程表问题是一个非常常见、变种很多,且具有代表性的问题。常用的解决算法包括DFS、BFS、回溯和递归等。由于本专题是DFS专题,和本人针对这类问题的常用解决策略,这里仅介绍DFS的解决方案。

课程表II(母题):leetcode—210

  1. 题目描述:
    在这里插入图片描述

  2. 分析:
    这是一道经典的【拓扑排序】问题。要根据题目的条件来建立拓扑结构模型(构图)
    1)我们将每一门课看成一个节点;
    2)如果想要学习课程A之前必须完成课程B,那么我们从B到A连接一条有向边。
    根据拓扑排序的定义,B一定出现在A的前面,刚好满足题目要求的先修课的条件。
    题目转换成根据我们上述步骤建立的图,是否存在拓扑排序。求出该图的拓扑排序
    ,就可以得到一种符合要求的课程学习顺序。在本专题里我们仅用DFS来实现它。

  3. DFS求拓扑排序
    1)用一个栈来存储所有已经搜索完成的节点。
    针对栈的问题,我们特别关注栈顶位置的元素。
    对于某节点u,如果它所有相邻的节点都已经搜索完成,那么这些节点都已经在栈中了,此时我们就可以把u入栈,故此时u处于栈顶的位置
    u在栈顶意味着它出现在所有u的相邻节点的面前,因此对于u这个节点而言,它满足拓扑排序的要求。
    2)对图进行一遍DFS,当每个节点进行回溯的时候,我们把该节点放入栈中。
    3)从栈顶到栈底的序列为一种拓扑排序。

  4. 算法
    对于图中的任意节点,在搜索过程中都有如下三种状态:
    1)「未搜索」:我们还没有搜索到这个节点;
    2)「搜索中」:我们搜索过这个节点,但还没有回溯到该节点,即该节点还没有入栈,还有相邻的节点没有搜索完成);
    3)「已完成」:我们搜索过并且回溯过这个节点,即该节点已经入栈,并且所有该节点的相邻节点都出现在栈的更底部的位置,满足拓扑排序的要求。
    在每一轮的搜索搜索开始时,我们任取一个「未搜索」的节点开始进行深度优先搜索。

  1. 我们将当前搜索的节点 u 标记为「搜索中」,遍历该节点的每一个相邻节点 v:
    如果 v 为「未搜索」,那么我们开始搜索v,待搜索完成回溯到 u;
    如果 v 为「搜索中」,那么我们就找到了图中的一个环,因此是不存在拓扑排序的,退出遍历返回空数组;
    如果 v 为「已完成」,那么说明 v 已经在栈中了,而 u还不在栈中,因此 u 无论何时入栈都不会影响到 (u, v)之前的拓扑关系,所以不用进行任何操作。
  2. 当 u 的所有相邻节点都为「已完成」时,我们将 u 放入栈中,并将其标记为「已完成」。
  1. 代码
class Solution {
    // 存储有向图
    List<List<Integer>> edges;
    // 标记每个节点的状态:0=未搜索,1=搜索中,2=已完成
    int[] visited;
    // 用数组来模拟栈,下标 n-1 为栈底,0 为栈顶
    int[] result;
    // 判断有向图中是否有环
    boolean valid = true;
    // 栈下标
    int index;

    public int[] findOrder(int numCourses, int[][] prerequisites) {
        edges = new ArrayList<List<Integer>>();
        for (int i = 0; i < numCourses; ++i) {
            edges.add(new ArrayList<Integer>());
        }
        visited = new int[numCourses];
        result = new int[numCourses];
        index = numCourses - 1;
        for (int[] info : prerequisites) {
            edges.get(info[1]).add(info[0]);
        }
        // 每次挑选一个「未搜索」的节点,开始进行深度优先搜索
        for (int i = 0; i < numCourses && valid; ++i) {
            if (visited[i] == 0) {
                dfs(i);
            }
        }
        if (!valid) {
            return new int[0];
        }
        // 如果没有环,那么就有拓扑排序
        return result;
    }

    public void dfs(int u) {
        // 将节点标记为「搜索中」
        visited[u] = 1;
        // 搜索其相邻节点
        // 只要发现有环,立刻停止搜索
        for (int v: edges.get(u)) {
            // 如果「未搜索」那么搜索相邻节点
            if (visited[v] == 0) {
                dfs(v);
                if (!valid) {
                    return;
                }
            }
            // 如果「搜索中」说明找到了环
            else if (visited[v] == 1) {
                valid = false;
                return;
            }
        }
        // 将节点标记为「已完成」
        visited[u] = 2;
        // 将节点入栈
        result[index--] = u;
    }
}
  1. 时空复杂度分析
    1)时间复杂度:O(n+m),其中n为课程数,m为先修课程的要求数。
    2)空间复杂度:O(n+m),将构好的图存储成邻接表的形式供DFS,在这个过程中,我们最多需要O(n)的栈空间(递归)进行深度优先搜索,并且还需要若干个O(n)的空间存储节点状态、最终答案等。
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值