算法爬坑记——BFS 与 拓扑排序

宽度优先搜索

BFS 是最常考的,而BFS 中,最常考的是拓扑排序

BFS的代码实现:

BFS代码模板:

不需要分层:

Queue<T> queue = new LinkedList<T>;  // queue 的作用是存储之后需要访问的点
Set<T> set = new HashSet<T>();  // set 的作用是判断这个点有没有访问过
queue.offer(head);
set.add(head);
while(!queue.isEmpyt()) {
    T this_node = queue.pop();
    for (T its_neighbors : this_node.neighbors) {
        if (set.contains(its_neighbors)) {   // 如果这个点访问过,那么就直接continue
            continue;
        }
    queue.offer(its_neighbors);
    set.add(its_neighbors);
}
// 此代码中没有分层操作

需要分层的版本:

Queue<T> queue = new LinkedList<T>(); //存储需要访问的节点
Set<T> set = new HashSet<T>();        // 存储访问过的节点
queue.offer(head);
set.add(head);
while (!queue.isEmpty()) {
    int size = queue.size();         // 限定只遍历这一层的节点
    for (int i = 0; i < size; i++) { // 使用 for 循环遍历这一层的节点
        T this_node = queue.poll();
        for (T its_neighbors: this_node.neighbors) {
            if (set.contains(its_neighbors)) {
                continue;
            }
        queue.offer(its_neighbros);
        set.contains(its_neighbors);
    }
// 这个是需要分层的版本

实现queue的另外的方法:

1. 使用两个队列实现BFS:

// T 表示任意你想存储的类型
Queue<T> queue1 = new LinkedList<>();
Queue<T> queue2 = new LinkedList<>();
Set<T> set = new HashSet<T>();   // 这里需要一个set来避免重复访问
queue.offer(startNode);
set.add(startNode);
int currentLevel = 0;
while (!queue1.isEmpty()) {
   int size = queue1.size();
    for (int i = 0; i < size; i++) {
        T head = queue1.poll();
        for (T its_neighbors: head.neighbors) {
            if (set.contains(its_neighbors) {
                continue;
            }
            queue2.offer(neighbor);
        }
    }
    Queue<T> temp = queue1;
    queue1 = queue2;
    queue2 = temp;

    queue2.clear();
    currentLevel++;
}

2. 采用dummy node

dummy node 通常不保存任何值。其作用是指向链表的第一个节点(head 节点)。这样就可以方便对head进行操作或者删除。BFS中的dummy node 的作用是放在每层遍历的结尾,表示这层的节点已经遍历完全了。代码如下:

Queue<T> queue = new LinkedList<>();
queue.offer(startNode);
queue.offer(null);
currentLevel = 0;
// 因为有 dummy node 的存在,不能再用 isEmpty 了来判断是否还有没有拓展的节点了
while (queue.size() > 1) {
    T head = queue.poll();
    if (head == null) {
        currentLevel++;
        queue.offer(null);
        continue;
    }
    for (all neighbors of head) {
        queue.offer(neighbor);
    }
}

什么时候使用BFS:

有的时候就是考你能否看出使用BFS

1. 图的遍历:层级遍历(BFS所做的事情就是层级遍历);由点及面(把与给定点连同的点都找到);拓扑排序(找到点与点之间的依赖关系)

层级遍历:Binary tree Level order Traversal; 二叉树的序列化及反序列化:Serialize and Deserialize Binary Tree; Binary Tree Level Order Traversal(不知道ArrayList能否这么运行) ; 

由点及面:Clone Graph

拓扑排序:Topological sorting; Course Schedule I II III; Alien Dictionary。拓扑排序的思路很简单,但是解决问题用到的数据结构有点意思,用到了HashMap。拓扑排序的作用是检验一些事件的依赖关系,不能有环状依赖。

2. 最短路径:本质上和层级遍历是一样的。但是仅用于边长相等的情况(即简单图的情况)。因为最短路径就是最少的层数。DP也可以解决最短路径问题。如果是最长路径,可以用DP 或者 DFS

最短路径:Word Ladder(可以看出这个是隐式图,因为各个单词之间是有联系的)。一看是简单图,百分之百是BFS。

3. 非递归的所有方案

二叉树上的BFS:

二叉树上的BFS主要是 层级遍历 和 二叉树的序列化

二叉树上的BFS:二叉树的层级遍历。BFS使用一种特殊数据结构 队列。 图的每一层都存到队列中。扩展的时候,遍历每一层的每一个节点,找到这个节点的所有儿子。二叉树的层级遍历属于BFS的模板。注意模板里要有两重循环,但是如果是图而不是树的话,需要三重循环。

层级遍历的DFS 做法:记录每个点的level,然后把这个点append到相应level的list中;进行多次DFS,每次只记录某一层的点。虽然这种方法效率很低(比BFS低),但是没有额外空间的耗费,不需要存储某一层的所有节点。

DFS方法没有额外的空间耗费,而BFS由于需要记录每一层的节点,所以有额外的空间耗费。

BFS DFS 的时间复杂度都是多少呢????

二叉树的序列化问题:

什么是序列化:将内存中的object变成string之后存储在硬盘中。

序列化的目的:1. 由于内存的特性,一但断电,里面存储的数据就会消失。所以要及时将数据转移到不会消失的硬盘里去。2. 当想要在机器之间传递数据的时候,需要将数据序列化成字符流数据,通过网络传输和接受。

常见的序列化格式:XML; JSON; Thift; ProtoBuff

矩阵中的BFS:

Number of Islands; Knight Shortest Path;Knight Shortest Path II;

矩阵可以理解为隐式图。(因为矩阵可以看作是相互连接的)

矩阵与图是不同的。假设矩阵中有R行C列,那么会有R ×C 个点, R×C×2条边。所以在矩阵中进行BFS的时候,时间复杂度为O(R*C) 因为需要把所有点和所有边都走一遍。

Numbers of islands:这个题思路很清晰,但是还有两个需要注意的点。矩阵BFS问题的特色——需要用坐标变换; 需要验证经过坐标变换得到的点是否在矩阵内部。

Knight Shortest Path: 和上面那个题是差不多的。但是这里有一个优化的follow up。可以用双向BFS进行优化。

Number of Islands:由点及面,和 clone graph 是一致的。用坐标变换数组表示四个方向。要判断这个点是否在界内。如果用 DFS, 由于递归深度是N^2,所以可能会 stack overflow

Knight Shortest Path: 八个方向。给出起点和终点,每次跳跃长度又是一样的,典型简单图的BFS。用BFS 做出的结果就是最短的。加速方法:双向BFS。因为给了起点和终点。这样节省的时间复杂度不是1/2,而是根号关系。会节省很多时间。

Knight Shortest Path II:可以用动态规划来做。

Topological sorting:DFS 也可以做拓扑排序,但是不推荐。什么是脚本代码???

拓扑排序不需要分层遍历,也不需要hashmap(BFS中使用hashmap的原因是防止一个点被反复访问)。如果最后得到的排序数组中元素的个数和总共元素的个数不一致,说明这里面有环状依赖,说明这个拓扑排序是错误的。有且仅有一个拓扑排序,就是要保证每次从queue中取出的量是1.

Alien Dictionary: 需要你自己去建立一个图,怎么存储图,如何变成priority queue。

Zombie in matrix

build post office

图的BFS:

图和树的本质区别:图有环,树没环。树是由n- 1个点将n个点连接起来的。

图中BFS的时间复杂度:假设图中有 N 个点, M条边,则M最大是 O(N^2) 级别的。图上的BFS时间复杂度 = O(N + M) 。因为每个点要访问一次,每条边也要访问一次。需要注意,并不是两个循环叠加起来,时间复杂度就是两个循环的乘积,有可能是和!最坏的情况是O(N^2),但是最坏情况一般不会出现。

图是一种去中心化的结构,每个点之间是邻居,是平等关系。图中某点的邻居的邻居可能是这个点本身。

树是一种中心化的结构,每个点之间是父子关系。

所以图中需要hashmap 记录哪些点访问过了,哪些点没有访问过。

Clone graph: 需要先由一个点,找到其他所有点(由点及面的过程)并不需要分层遍历,但是仍然可以用BFS。

word ladder:隐式图最短路径。一看这个图是简单图,百分之百是用BFS。但是你要能够知道这是个图。在有关问题中,hashset 和 queue是一起出现的。hashmap 的时间复杂度是 O(size of key)


宽度优先搜索的优化——双向宽度有限搜索

双向宽度有限搜索的目的是求出从起点到重点的最短路径。其定义是,从起点和重点同时开始向中间搜索,当这两个搜索交汇时,搜索到的路径之和就是最短路径。

可将计算量降低到原来搜索的根号量级。原理如下:

假设每个节点相连的节点数都为N。假设单一方向的BFS需要走X层,则一共需要访问的节点是X^N。

如果是从两端向中间走,则每个BFS需要走的层数是 N/2。则每个BFS需要访问的节点数是 X^N/2.一共只需访问 2 * X^N/2.

代码如下:

// 实际上就是两个BFS写在一起,没有什么别的难度
public int doubleBFS(UndirectedGraphNode start, UndirectedGraphNode end) {
    if (start.equals(end)) {
        return 1;
    }
    // 起点开始的BFS队列
    Queue<UndirectedGraphNode> startQueue = new LinkedList<>();
    // 终点开始的BFS队列
    Queue<UndirectedGraphNode> endQueue = new LinkedList<>();
    startQueue.add(start);
    endQueue.add(end);
    int step = 0;
    // 记录从起点开始访问到的节点
    Set<UndirectedGraphNode> startVisited = new HashSet<>();
    // 记录从终点开始访问到的节点
    Set<UndirectedGraphNode> endVisited = new HashSet<>();
    startVisited.add(start);
    endVisited.add(end);
    while (!startQueue.isEmpty() || !endQueue.isEmpty()) {
        int startSize = startQueue.size();
        int endSize = endQueue.size();
        // 按层遍历
        step ++;
        for (int i = 0; i < startSize; i ++) {
            UndirectedGraphNode cur = startQueue.poll();
            for (UndirectedGraphNode neighbor : cur.neighbors) {
                if (startVisited.contains(neighbor)) {//重复节点
                    continue;
                } else if (endVisited.contains(neighbor)) {//相交
                    return step;
                } else {
                    startVisited.add(neighbor);
                    startQueue.add(neighbor);
                }
            }
        }
        step ++;
        for (int i = 0; i < endSize; i ++) {
            UndirectedGraphNode cur = endQueue.poll();
            for (UndirectedGraphNode neighbor : cur.neighbors) {
                if (endVisited.contains(neighbor)) {
                    continue;
                } else if (startVisited.contains(neighbor)) {
                    return step;
                } else {
                    endVisited.add(neighbor);
                    endQueue.add(neighbor);
                }
            }
        }    
    }
    return -1; // 不连通
}

相关问题:

Shortest path in undirected graph

Knight shortest path

Knight shortest path II


猜你喜欢

转载自blog.csdn.net/chichiply/article/details/80592569