Bootstrap

图论算法(应用场景及部分模版代码展示)

DFS(深度优先搜索)

主要思想:沿着一个方向一直搜索,直到该方向不能再继续前进,则换方向前进

在DFS中通常需要进行递归。但是不一定要写退出递归的判断条件。若我们在进入递归之前就对数值预先做了判断,那么就不需要在dfs函数中添加退出条件。

BFS(广度优先搜索)

主要流程:先把本节点所有连接的节点遍历一遍,走到下一个节点的时候,再把该节点的所有节点遍历一遍。

相对于DFS,BFS搜索到的路径一定是最短路径。在进行BFS进行搜索的时候,通常需要使用栈和队列保存上一次遍历的节点。若需要存储最短路径值,则可以在栈和队列中加入分割变量,如null值;或者通过二维矩阵来记录当从起始点遍历到图中任一节点的距离长短。

Prim(实现最小生成树)

最小生成树解决的问题:将所有的节点以最小的代价连接在一起。相对于求最短路径来说,最小生成树能够实现连通所有的节点,而最短路径不一定要包含所有的节点。

主要流程:第一步选取距离生成树最近的节点,第二步将当前节点加入生成树,第三步是更新非生成树节点到生成树的距离。

需要理解的一点就是minDist数组,保存的是当前索引节点到生成树的距离的最小值,即将该节点连接到生成树中需要花费的最小代价。

Kruskal(实现最小生成树)

该算法的解决的问题同Prim算法相同。但Prim是从遍历节点的角度去考虑的,而Kruskal算法是从遍历边的角度去考虑的。

主要流程:先将各节点相连的边的按照权值的大小进行升序排列,然后依次将最小权值连接的节点加入到生成树中,根据并查集判断当前两个节点是否已经在生成树中,若已经存在则不参加。

具体实现细节:可以将所有的边存储到List集合当中,然后重写List中的sort算法,使其按照边的权重进行排序。

并查集

该算法主要解决的问题是判断两个节点是否在图中属于连通状态。针对于无向图

并查集模版:

class DisJoint{
    private int[] father;
 
    public DisJoint(int N) {
        father = new int[N];
        for (int i = 0; i < N; ++i){
            father[i] = i;
        }
    }
 
    public int find(int n) {
        return n == father[n] ? n : (father[n] = find(father[n]));
    }
 
    public void join (int n, int m) {
        n = find(n);
        m = find(m);
        if (n == m) return;
        father[m] = n;
    }
 
    public boolean isSame(int n, int m){
        n = find(n);
        m = find(m);
        return n == m;
    }
 
}

dijkstra最短路径算法

迪杰斯特拉算法主要用于解决求最短路径问题。给出一个有向图,一个起点,一个终点,问起点到终点的最短路径。

dijkstra算法的两个注意点:

1.该算法可以同时求起点到所有终点的最短路径。

2.该算法只能用于求权值非负数的情况。

主要流程:首先将源目标点的距离设置为0。第一步选取源点到哪个节点最近且该节点没有被访问过。第二步更新该节点被访问过。第三步就是更新非访问节点到原点的距离(即更新minDist数组)。

该算法的思想与Prim相似,唯一区别就是Prim算法minDist数组保存的是非访问节点到最小生成树的最小距离,而dijkstra保存的是非访问节点到源点的最短距离。

简单模版(供理解)

        //变量说明:minDist[n]。n代指节点数,初始值为Integer.maxValue,源节点初始值为0。
        //grid[][],表示节点之间的关系,用邻接矩阵表示。其值初始值为最大值,若节点相连则表示权值。
        for (int i = 1; i <= n; i++) { // 遍历所有节点

            int minVal = Integer.MAX_VALUE;
            int cur = 1;
            // 1、选距离源点最近且未访问过的节点
            for (int v = 1; v <= n; ++v) {
                if (!visited[v] && minDist[v] < minVal) {
                    minVal = minDist[v];
                    cur = v;
                }
            }

            visited[cur] = true;  // 2、标记该节点已被访问

            // 3、第三步,更新非访问节点到源点的距离(即更新minDist数组)
            for (int v = 1; v <= n; v++) {
                if (!visited[v] && grid[cur][v] != Integer.MAX_VALUE && minDist[cur] + grid[cur][v] < minDist[v]) {
                    minDist[v] = minDist[cur] + grid[cur][v];
                }
            }
        }

Bellman_ford最短路径

由于djikstra算法无法解决出现边权值为负数的情况,针对于此情况需要使用Bellman_ford算法。

主要流程:对所有边进行n-1次松弛操作。何为松弛?即更新当前路径到源点的最短距离。松弛操作的核心代码如下:

if (minDist[B] > minDist[A] + value) minDist[B] = minDist[A] + value

为什么要松弛n-1次是此算法的关键点。对于一次松弛操作相当于计算起点到达与起点一条边相连的节点的最短距离。N个节点,则从源节点到目标节点最多经过N-1个节点,故只需经历N-1次对全部边的松弛操作,即可求得从源节点到目标节点的最短路径。

简单模版(供理解)

    static class Edge {
        int from;
        int to;
        int val;
        public Edge(int from, int to, int val) {
            this.from = from;
            this.to = to;
            this.val = val;
        }
    }
    //其中edge为自定义类,保存边的信息
    //minDist保存该索引下的节点到源节点的最短路径值
    int[] minDist = new int[n+1];
    Arrays.fill(minDist,INF);
    int start = 1;
    minDist[start] = 0;
    //进行N-1次松弛操作
    for(int i = 1; i <= n; i++){
        //遍历所有边,对其进行松弛操作
        for(Edge edge : edges){
            if(minDist[edge.from] != INF && edge.val + minDist[edge.from] < minDist[edge.to]){
                minDist[edge.to] = minDist[edge.from] + edge.val;
            }
        }
    }

Bellman_ford最短路径(队列优化版本)

从上述对Bellman_ford的介绍得知,其算法需要对所有边进行N-1次松弛操作,故其时间复杂度为O(n*k),其中n表示节点数量,k表示边的数量。但是这其中包含了许多冗余操作。只需要对上一次松弛的时候更新过的节点作为出发节点所连接的边进行松弛就足够了,其余节点的松弛操作并不影响结果

主要思想:首先将源节点加入队列,松弛与源节点相连的节点。然后将与源节点相连的节点放入队列,重复上述操作即可。在放入队列的时候,需要判断该节点是否已经在队列当中,所以还需要维护一个布尔矩阵。

简单模版(供理解)

        //其中graph为嵌套链表,表示该索引下有哪些边与其连接。
        //定义:  List<List<Edge>> graph = new ArrayList<>();

        boolean[] visited = new boolean[n+1];
        int[] minDist = new int[n+1];
        Arrays.fill(minDist,INF);
        int start = 1;
        minDist[start] = 0;
        Queue<Integer> queue = new ArrayDeque<>();
        //将初始节点放入队列当中
        queue.offer(start);
        while (!queue.isEmpty()){
            int node = queue.poll();
            //取出节点,并维护队列中节点状态
            visited[node] = false;
            //对与当前节点相连的边进行松弛操作
            for(Edge edge : graph.get(node)){
                //需要两重判断,第一重判断当前边是否需要松弛,第二重判断与该节点的连接节点是否已经在队列当中。
                if(minDist[node] + edge.val < minDist[edge.to]){
                    minDist[edge.to] = minDist[node] + edge.val;
                    if(!visited[edge.to]){
                        queue.offer(edge.to);
                        visited[edge.to]= true;
                    }
                }
            }
        }

Bellman_ford解决图中是否出现了负权回路:

通过bellman_ford算法可以得知,当图中没有出现负权回路时,经过N-1次松弛操作,minDist数组的值应为固定值。

所以,对于基础的Bellman_ford算法可以通过第N次松弛操作判断minDist数组是否发生改变来发现图中的负权回路。

若通过队列优化版的Bellman_ford算法来判断图中是否出现负权回路,则需要维护一个边松弛次数的数组,当该数组中有值达到N时,则说明出现了负权回路。

Bellman_ford算法解决单源有限最短路径问题:

问题描述:求从源点到目标点的最短路径,但路径最多只能经历K个节点。

解决思路:对于一次松弛操作相当于计算起点到达与起点一条边相连的节点的最短距离。所以只需要进行K+1次松弛操作即可得到只经历K个节点的最短路径。但是!如果只单单进行K-1次松弛操作,若图中出现了负权回路,则可能得到的结果并不符合预期。这是因为在对边进行松弛操作时,不仅仅与起点一条边相连的节点更新了,所有节点有更新了。

解决办法:需要保存上一轮minDist数组值,根据上一轮minDist数组值进行计算。

简单模版(供理解)

    static class Edge {
        int from;
        int to;
        int val;
        public Edge(int from, int to, int val) {
            this.from = from;
            this.to = to;
            this.val = val;
        }
    }
    //其中edge为自定义类,保存边的信息
    //minDist保存该索引下的节点到源节点的最短路径值
    int[] minDist = new int[n+1];
    Arrays.fill(minDist,INF);
    int start = 1;
    minDist[start] = 0;
    //进行K+1次松弛操作
    for(int i = 1; i <= K+1; i++){
        int[] minDistCopy = Arrays.copy(minDist);
        //遍历所有边,对其进行松弛操作
        for(Edge edge : edges){
            if(minDist[edge.from] != INF && edge.val + minDist[edge.from] < minDist[edge.to]){
                minDist[edge.to] = minDistCopy[edge.from] + edge.val;
            }
        }
    }

;