zoukankan      html  css  js  c++  java
  • DFS和BFS遍历的问题

    来自https://github.com/soulmachine/leetcode

    广度优先搜索

    输入数据:没有什么特征,不像dfs需要有递归的性质。如果是树/图,概率更大。

    状态转换图:数或者DAG图(有向无环图)

    求解目标:求最短

    思考的步骤:

    1,是求路径长度,还是路径本身(动作序列)

      a,如果是求路径长度,则状态里面要存路径长度(或双端队列+一个全局变量)

      b,如果是求路径本身或动作序列

        i,要用一颗树存储宽搜过程的路径

        ii,是否能够预算状态个数的上限?

          能够预估状态总数,则开辟一个大数组,用树的双亲表示法;

          如果不能预估状态总数,则要使用一颗通用的树,这也是第4步的需要不充分条件。

    2,如何表示状态?即一个状态需要存储哪些必要的数据,才能够完整提供如何扩展到下一步状态的所有信息。一般记录当前位置或整体局面。

    3,如何扩展状态?这一步和第2步有关,状态里记录的数据不同,扩展方法就不同。

      对于固定不变的数据结构(一般题目直接将给出,作为输入数据),如二叉树、图。扩展方法简单,直接往下一层走。

      对于隐式图,要先在第一步里想清楚状态所带的数据,想清楚了这一点,就可以直到如何扩展了。

    4,如何判断重复?

      如果状态转换图是一棵树,则永远不会出现回路,不需要判重。

      如果状态转换图是一个图(这时候是一个图上的BFS),则需要判断重复。

        a,如果是求最短路径长度或一条路径,则只需要让“点”(就是状态)不重复出现,即可保证不出现回路

        b,如果是求所有路径,注意此刻,状态转换图是DAG,即允许两个父节点指向同一个字节点。具体实现时,每个节点要“延迟”加入到已访问集合visited,

         要等一层全部访问完后,再加入到visited集合。

        c,具体实现?

          i,状态是否存在完美哈希方案?即将状态一一映射到整数,互相之间不会冲突。

          ii,如果不存在,则需要使用通用的哈希表(自己实现,或使用STL,例如unordered_set)来判重;

            自己实现的哈希表,如果能够预估状态个数的上限,则可以开两个数组,head和next表示哈希表(下面有例子)。

          iii,如果存在,则可以开一个大布尔数组,来判重,且此刻可以精确计算出状态总数,而不仅仅是预估上限。

    5,目标状态是否已知?

      如果题目已经给出了目标状态,可以带来很大便利,这时候可以从起始状态出发,正向广搜,

      也可以从目标状态出发,逆向广搜,

      也可以同时出发,双向广搜。

    -----

    代码模板

    广搜需要一个队列,用于一层一层扩展,一个hashset,用于判重,一棵树(只求长度时不需要)用于存储整棵树。

      对于队列,可以用queue,也可以把vector当作队列使用。当求长度时,有两种做法:

        1,只用一个队列,但在状态结构体state_t里面增加一个整数字段level,表示当前所在的层次,当碰到目标状态时,直接输出level即可。

        这个方案可以很容易编程A*搜索,把queue替换为priority_queue即可。

        2,用两个队列,current,next,分别表示当前层次和下一层,另设一个全局整数level,表示层数(即路径长度),当碰到目标状态,输出level即可。

        这个方案,状态里可以村路径长度,只需全局设置一个整数level,比较节省内存;

      对于hashset

        如果有完美哈希方案,用布尔数组(bool visited[STATE_MAX]或vector<bool> visited(STATE_MAX,false)来表示;

        如果没有完美哈希方案,需要用STL里的set或unordered_set。

      对于树,

        如果用STL,可以用unordered_map<state_t,state_t> father表示一棵树,代码很简洁。

        如果能够预估状态总数的上限(设为STATE_MAX),可以使用数组state_t nodes[STATE_MAX],即树的双亲表示法来表示树,效率更高,但是代码更高。

    代码在这里

    ===============================================================

    深度优先搜索

    适用场景:

    输入数据,如果是递归数据结构,如单链表,二叉树,集合,则百分百可以用深搜;如果是非递归数据结构,如一维数组,二维数组,字符串,图则概率小一些。但是也有的。

    状态转换图,树或者图

    求解目标:必须走到最深处(例如对于树,必须要走到叶子节点)才能得到一个解,适合是恩搜。

    思考步骤:

    1,深搜常见的三个问题,求可行解的总数,求一个可行解,求所有可行解。

      a,如果是路径条数,则不需要存储路径

      b,如果哦是路径本身,则要用一个数组path存储路径。

        跟宽搜不同,宽搜虽然也是一条路径,但是需要存储扩展过程中的所有路径,在没找到答案之前所有路径都不能放弃。

        而深搜,在搜索过程中始终只有一条路径,因此用一个数组就可以了。

    2,只要求一个解,还是求所有解。

      只求一个解,找到一个解就返回。

      求所有解,找到一个后,还要继续遍历。

      广搜一般只要求一个解,(广搜也会求所有解,这时需要扩展到所有叶子节点,相当于在内存中存储整个状态转换图,非常占内存,因此广搜不适合求这类问题)。

    3,如果表示状态?

      即一个状态需要存储哪些必要的数据,才能够完整提供如何扩展到下一步状态的所有信息。跟广搜不同,深搜的惯用写法,不是把数据记录在状态struct里,而是

      添加函数参数(有时为了节省递归堆栈,用全局变量),struct里的字段与函数参数一一对应。

    4,如何扩展状态?

      这一步跟上一步相关。状态里记录的数据不同,扩展方法就不同,对于固定不变的数据结构(一般题目直接给出,作为输入数据),二叉树、图,扩展方法很简单,直接往下一步走就行了。对于隐式图,要先在第1步中想清楚状态所带的数据,才能直到扩展。

    5,终止条件?

      是指到了不能扩展的末端节点。对于树,是叶子节点。对于图或隐式图,是出度为0的节点。

    6,收敛条件?

      是指找到一个合法解的时刻。

      如果正向深搜(父节点处理完了,才进行递归,即父状态不依赖子状态,递归语句在最后,尾递归),则是指是否达到目标状态;

      如果是逆向搜索,(处理父状态时需要先知道子状态的结果,此时递归语句不在最后),则是指是否到达初始状态。

      很多时候,终止状态和收敛条件是合二为一的,很多人不会区分这两种条件。仔细区分这两种条件,是有必要的。

      为了判断是否到了收敛条件,要在函数接口里用一个参数记录当前的位置(或距离目标还有多远)。

      如果是求一个解,直接返回这个解;如果是求所有解,要在这里搜集,即把第一步中表示路径的数组path[]复制到解集合里。

    7,关于判重

      a,是否需要判重?

        如果状态转换图是一颗树,则不需要判重,因为在遍历的过程中不会出现重复;

        如果状态状态图是一个DAG,则需要判重。这一点和BFS不一样,BFS的状态转换图总是DAG,必须判重。

      b,怎么判重,跟广搜一样。同时,DAG说明存在重叠子问题,此时可以用缓存加速。见第8步(下一步)。

    8,如何加速?

      a,剪枝。深搜一定要好好考虑怎么剪枝,成本小收益大,加几行代码,就能大大加速。

          这里没有通用的方法,只能具体问题,具体分析,要充分观察,充分利用各种信息来剪枝,在中间节点提前返回。

      b,缓存。

        i,前提条件:状态转换图是一个DAG。DAG=>存在重叠子问题=>子问题的解会被重复利用,用缓存自然会由加速效果。

         如果依赖关系是树状的(例如树,单链表等),没必要加缓存,因为子问题只会一层层往下,用一次就再也不会用到,加了缓存也没什么效果。

        ii,具体实现:可以使用数组或hashmap。

          维度简单的,用数组;

          维度复杂的,用hashmap,c++有map,c++11以后有unordered_map,比map快。

    代码模板

    /**
    *@brief dfs模板
    *@param[in] input 输入数据指针
    *@param[out] path 当前路径,也是中间结果
    *@param[out] result 存放最终结果
    *@param[inout] cur or gap 标记当前位置或距离目标的距离
    *@return  1,路径长度,2路径本身
    */
    void dfs(type &input,type &path,type &result,int cur or gap){
        if(data is valid) return;//终止条件
        if(curr==input.size()){// 收敛条件,正向
            ///if(gap==0){}///逆向
            result.push_back(path);
        }
        
        if(可以剪枝) return;
        
        for(...){///执行所有的可能的扩展
            执行动作,修改path
            dfs(input,step+1,or gap--,result);
            恢复path
        }
    }        

    ---------

    深搜和回溯法?

    深搜:维基百科

    回溯法:维基百科

    回溯法=深搜+剪枝,一般在用深搜时,或多或少会用到剪枝,因此深搜和回溯法没有什么不一样的。

    深搜与递归recursion?

    深搜,是逻辑意义上的算法;递归是物理意义上的实现,递归和迭代iteration相对应。

    深搜,可以用递归实现,也可以用栈实现。而递归,一般总是用来实现深搜,可以说,递归一定是深搜,但是深搜不一定递归。

    递归由两种加速策略,1剪枝,对中间结果判断,提前返回。2缓存,缓存中间结果,防止重复计算,用空间换时间。

    其实,递归+缓存,就是memorization(翻译为备忘录法),就是top-down with cache(自顶向下+缓存),它是Donald Michie在1968年创造的术语,表示

      一种优化技术,在top-down形式的程序中,使用来避免重复计算,可以加速。

    memorization不一定用递归,就像深搜不一定用递归一样,可以在迭代iterative中使用memorization。递归也不一定用memorization,可以使用它来加速,但也不是必须的。

      只有在使用了缓存时,它才是memorizaiton。

  • 相关阅读:
    原型模型
    V模型
    瀑布模型
    微服务的特点 优点 缺点
    ip地址的分类
    DSSA特定领域软件体系结构
    Git操作 :从一个分支cherry-pick多个commit到其他分支
    【原理】从零编写ILI9341驱动全过程(基于Arduino)
    Arduino驱动ILI9341彩屏(一)——颜色问题
    STL库学习笔记(一)——什么是STL?
  • 原文地址:https://www.cnblogs.com/li-daphne/p/5543282.html
Copyright © 2011-2022 走看看