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。

 

    原文作者:DFS
    原文地址: https://www.cnblogs.com/li-daphne/p/5543282.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞