0
点赞
收藏
分享

微信扫一扫

【算法修炼】图论算法三(并查集的应用、Kruskal最小生成树算法、Dijkstra最短路径算法、SPFA最短路径算法)

君之言之 2022-02-23 阅读 57

图论算法三

一、并查集算法的应用

再来回顾下并查集算法的模板:

class UF {
    // 连通分量个数
    int count;
    // 存储若干棵树
    int[] parent;
    // 记录每棵树的重量
    int[] size;

    // 构造函数
    public UF(int n) {
        this.count = n;
        parent = new int[n];
        size = new int[n];
        // 图中的每个节点指向自己
        for (int i = 0; i < n; i++) {
            parent[i] = i;
            size[i] = 1;
        }
    }

    // 找 x节点的根节点
    public int find(int x) {
        // 用路径压缩
        while (parent[x] != x) {
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return x;
    }

    // 连通p、q节点
    public void union(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        // 根节点相同,说明本来就是连通的
        if (rootP == rootQ) {
            return;
        }
        // 小树接在大树下,更平衡
        if (size[rootP] > size[rootQ]) {
            parent[rootQ] = rootP;
            size[rootP] += size[rootQ];
        } else {
            parent[rootP] = rootQ;
            size[rootQ] += size[rootP];
        }
        // 连通分量个数--
        count--;
    }

    // 判断 p 和 q 是否连通
    public boolean connected(int p, int q) {
        return find(p) == find(q);
    }

    // 返回连通分量个数
    public int count() {
        return count;
    }
}

算法的关键点有 3 个:

1、用 parent 数组记录每个节点的父节点,相当于指向父节点的指针,所以 parent 数组内实际存储着一个森林(若干棵多叉树)。

2、用 size 数组记录着每棵树的重量,目的是让 union 后树依然拥有平衡性,而不会退化成链表,影响操作效率。

3、在 find 函数中进行路径压缩,保证任意树的高度保持在常数,使得 union 和 connected API 时间复杂度为 O(1)。

别忘了路径压缩,小树接在大树后面,更平衡。

等式方程的可满足性(中等)

在这里插入图片描述
前文说过,动态连通性其实就是一种等价关系,具有「自反性」「传递性」和「对称性」,其实 == 关系也是一种等价关系,具有这些性质。所以这个问题用 Union-Find 算法就很自然。

核心思想是,将 equations 中的算式根据 == 和 != 分成两部分,先处理 == 算式,使得他们通过相等关系各自勾结成门派(连通分量);然后处理 != 算式,检查不等关系是否破坏了相等关系的连通性。

class Solution {
    public boolean equationsPossible(String[] equations) {
        // 26个英文字母
        UF uf = new UF(26);
        // 先让相等的字母形成连通分量 ==
        for (String eq : equations) {
            if (eq.charAt(1) == '=') {
                char x = eq.charAt(0);
                char y = eq.charAt(3);
                uf.union(x - 'a', y - 'a');
            }
        }
        // check不等关系是否会打破相等关系的连通性
        // 如果没有打破,说明这个不等关系是不会影响其它等式的判断
        // a != b,打破就是说 a 与 b 原来是连通的,现在不等式又连通了,那肯定false
        for (String eq : equations) {
            if (eq.charAt(1) == '!') {
                char x = eq.charAt(0);
                char y = eq.charAt(3);
                // 如果相等关系成立(也就是之前已经连通了),则逻辑冲突
                if (uf.connected(x - 'a', y - 'a')) return false;
            }
        }
        return true;
    }
}
class UF {
    // 连通分量个数
    int count;
    // 每棵树
    int[] parent;
    // 每棵树的节点数
    int[] size;

    UF(int n) {
        this.count = n;
        this.parent = new int[n];
        this.size = new int[n];
        // 图中的每个节点指向自己
        for (int i = 0; i < n; i++) {
            parent[i] = i;
            size[i] = 1;
        }
    }

    // 找根节点
    public int find(int x) {
        while (parent[x] != x) {
            // 路径压缩
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return x;
    }

    // 判断是否连通
    public boolean connected(int p, int q) {
        return find(p) == find(q);
    }

    // 连通两个节点
    public void union(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        // 本来就联通
        if (rootQ == rootP) return;
        if (size[rootP] > size[rootQ]) {
            // 小树接在大树后
            parent[rootQ] = rootP;
            size[rootP] += size[rootQ];
        } else {
            parent[rootP] = rootQ;
            size[rootQ] += size[rootP];
        }
        // 连通分量--
        count--;
    }

    // 返回连通分量个数
    public int count() {
        return count;
    }
}

一定要学会把问题转换为并查集的思路,这样能够帮助解决很多问题。

二、Kruskal最小生成树

如果一幅图没有环,完全可以拉伸成一棵树的模样。说的专业一点,树就是「无环连通图」。

那么什么是图的「生成树」呢,其实按字面意思也好理解,就是在图中找一棵包含图中的所有节点的树。专业点说,生成树是含有图中所有顶点的「无环连通子图」。

容易想到,一幅图可以有很多不同的生成树,比如下面这幅图,红色的边就组成了两棵不同的生成树
在这里插入图片描述
对于加权图,每条边都有权重,所以每棵生成树都有一个权重和。比如上图,右侧生成树的权重和显然比左侧生成树的权重和要小。

那么最小生成树很好理解了,所有可能的生成树中,权重和最小的那棵生成树就叫「最小生成树」。

这里需要使用Union-Find 并查集算法,来保证图中生成的是树(不包环)。

并查集算法是如何做到的?先来看看这道题
给你输入编号从 0 到 n - 1 的 n 个结点,和一个无向边列表 edges(每条边用节点二元组表示),请你判断输入的这些边组成的结构是否是一棵树。
在这里插入图片描述
这些边构成的是一颗树,应该返回true:
在这里插入图片描述
在这里插入图片描述
对于这道题,我们可以思考一下,什么情况下加入一条边会使得树变成图(出现环)?

显然,像下面这样添加边会出现环:

在这里插入图片描述
而下面这样添加就不会出现环:
在这里插入图片描述

而判断两个节点是否连通(是否在同一个连通分量中)就是 Union-Find 算法的拿手绝活,所以这道题的解法代码如下:

// 判断输入的若干条边是否能构造出一棵树结构
boolean validTree(int n, int[][] edges) {
    // 初始化 0...n-1 共 n 个节点
    UF uf = new UF(n);
    // 遍历所有边,将组成边的两个节点进行连接
    for (int[] edge : edges) {
        int u = edge[0];
        int v = edge[1];
        // 若两个节点已经在同一连通分量中,会产生环
        if (uf.connected(u, v)) {
            return false;
        }
        // 这条边不会产生环,可以是树的一部分
        uf.union(u, v);
    }
    // 要保证最后只形成了一棵树,即只有一个连通分量
    return uf.count() == 1;
}

class UF {
    // 见上文代码实现
}

Kruskal 算法
所谓最小生成树,就是图中若干边的集合(我们后文称这个集合为 mst,最小生成树的英文缩写),你要保证这些边:

1、包含图中的所有节点,就是整个图的连通分量 = 1(全部节点连起来了)
2、形成的结构是树结构(即不存在环),就是用并查集的connected函数查看是否连通(相同根节点)。
3、权重和最小。

有之前题目的铺垫,前两条其实可以很容易地利用 Union-Find 算法做到,关键在于第 3 点,如何保证得到的这棵生成树是权重和最小的?

这里就用到了贪心思路:

所有边按照权重从小到大排序,从权重最小的边开始遍历,如果这条边和mst中的其它边不会形成环,则这条边是最小生成树的一部分,将它加入mst集合;否则,这条边不是最小生成树的一部分,不要把它加入mst集合。

最低成本联通所有城市(中等)

请添加图片描述
就是找最小生成树!每座城市相当于图中的节点,连通城市的成本相当于边的权重,连通所有城市的最小成本即是最小生成树的权重之和。

二维数组的排序(选择第几个元素为排序依据)

int[][] nums = new int[][]{{1,4,5},{2,3,5},{7,8,5}};
// 按照0 1 2,第2个元素排序,从小到大
// 如果第2个元素相同,那就按第1个元素排序
Arrays.sort(nums, new Comparator<int[]>() {
    @Override
    public int compare(int[] o1, int[] o2) {
        if (o1[2] == o2[2]) {
        	// 按第1个元素排序(默认从小到大)
            return o1[1] - o2[1];
        } else {
            return o1[2] - o2[2];
        }
    }
});
class Solution {
    int minimumCost(int n, int[][] connections) {
        // 城市编号 1...n,所以初始化大小为n + 1
        UF uf = new UF(n + 1);
        // 对所有边按权重排序
        Arrays.sort(connections, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[2] - o2[2];
            }
        });
        // 记录最小生成树的权重之和
        int mst = 0;
        for (int[] edge : connections) {
            int u = edge[0];
            int v = edge[1];
            int weight = edge[2];
            // 如果这条边会产生环,则不能加入mst
            if (uf.connected(u, v)) {
                continue;
            }
            // 如果不会产生环,那就可以加入mst,属于最小生成树
            mst += weight;
            uf.union(u, v);
        }
        // 保证所有节点被连通,就是连通分量count = 1
        // 但是由于题目下标从1开始,节点0自身就是独立的
        // 所以count应该=2
        return uf.count() == 2 ? mst : -1;
    }
}
class UF {
    // 连通分量个数
    int count;
    // 每棵树
    int[] parent;
    // 每棵树的节点数
    int[] size;

    UF(int n) {
        this.count = n;
        this.parent = new int[n];
        this.size = new int[n];
        // 图中的每个节点指向自己
        for (int i = 0; i < n; i++) {
            parent[i] = i;
            size[i] = 1;
        }
    }

    // 找根节点
    public int find(int x) {
        while (parent[x] != x) {
            // 路径压缩
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return x;
    }

    // 判断是否连通
    public boolean connected(int p, int q) {
        return find(p) == find(q);
    }

    // 连通两个节点
    public void union(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        // 本来就联通
        if (rootQ == rootP) return;
        if (size[rootP] > size[rootQ]) {
            // 小树接在大树后
            parent[rootQ] = rootP;
            size[rootP] += size[rootQ];
        } else {
            parent[rootP] = rootQ;
            size[rootQ] += size[rootP];
        }
        // 连通分量--
        count--;
    }

    // 返回连通分量个数
    public int count() {
        return count;
    }
}

有了上面的代码,可以发现,并查集也可以用于无向图中环的检测。

连接所有点的最小费用(中等)

请添加图片描述
比如题目给的例子:

points = [[0,0],[2,2],[3,10],[5,2],[7,0]]

算法应该返回 20,按如下方式连通各点:
请添加图片描述
很显然这也是一个标准的最小生成树问题:每个点就是无向加权图中的节点,边的权重就是曼哈顿距离,连接所有点的最小费用就是最小生成树的权重和。

本题相当于把边的权重换一种方式进行告诉。

class Solution {
    int minCostConnectPoints(int[][] points) {
        int n = points.length;
        // 生成所有边和权重
        List<int[]> edges = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                int xi = points[i][0], yi = points[i][1];
                int xj = points[j][0], yj = points[j][1];
                // 添加边、权重
                edges.add(new int[] {
                        i, j, Math.abs(xi - xj) + Math.abs(yi - yj)
                });
            }
        }
        // 将边按照权重从小到大排序
        Collections.sort(edges, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[2] - o2[2];
            }
        });
        // 执行Kruskal算法
        int mst = 0;
        UF uf = new UF(n);
        for (int[] edge : edges) {
            int u = edge[0];
            int v = edge[1];
            int weight = edge[2];
            // 如果这条边会产生环,就不能加入mst
            if (uf.connected(u, v)) {
                continue;
            }
            // 不会产生环,就属于最小生成树
            mst += weight;
            uf.union(u, v);
        }
        return mst;
    }
}
// UF类实现见上文代码

三、Dijkstra 算法框架

首先,我们先看一下 Dijkstra 算法的签名:

// 输入一幅图和一个起点 start,计算 start 到其他节点的最短距离
int[] dijkstra(int start, List<Integer>[] graph);
// 输出一个记录最短路径权重的数组

在这里插入图片描述

其次,我们也需要一个 State 类来辅助算法的运行:

class State {
    // 图节点的 id
    int id;
    // 从 start 节点到当前节点的距离
    int distFromStart;

    State(int id, int distFromStart) {
        this.id = id;
        this.distFromStart = distFromStart;
    }
}
  • 普通 BFS 算法中,根据 BFS 的逻辑和无权图的特点,第一次遇到某个节点所走的步数就是最短距离,所以用一个 visited 数组防止走回头路,每个节点只会经过一次。搜索算法中,找最小步数也是用BFS算法,因为它可以保证第一次到达的就是最小的步数。

  • 加权图中的 Dijkstra 算法和无权图中的普通 BFS 算法不同,在 Dijkstra 算法中,你第一次经过某个节点时的路径权重,不见得就是最小的(这也好理解,BFS只看节点,不会关注权重),所以对于同一个节点,我们可能会经过多次,而且每次的 distFromStart 可能都不一样,所以不会使用visited数组防止走回头路,因为需要走回头路,比如下图:

在这里插入图片描述
我会经过节点 5 三次,每次的 distFromStart 值都不一样,那我取 distFromStart 最小的那次,不就是从起点 start 到节点 5 的最短路径权重了么?

好了,明白上面的几点,我们可以来看看 Dijkstra 算法的代码模板。

其实,Dijkstra 可以理解成一个带 dp table(或者说备忘录)的 BFS 算法,伪码如下:

class State {
    // 图节点的id
    int id;
    // 从 start 节点到当前节点的距离
    int distFromStart;

    State(int id, int distFromStart) {
        this.distFromStart = distFromStart;
        this.id = id;
    }
}

// 返回节点 from 到节点 to 之间的边的权重(看实际情况,可能也不需要)
int weight(int from, int to);

// 输入节点 s 返回 s 的相邻节点(看实际情况,可能也不需要)
List<Integer> adj(int s);

// 输入一幅图和一个起点 start,计算 start 到其他节点的最短距离
int[] dijkstra(int start, List<Integer>[] graph) {
    // 图中节点的个数
    int V = graph.length;
    // 记录最短路径的权重,你可以理解为 dp table
    // 定义:distTo[i] 的值就是节点 start 到达节点 i 的最短路径权重
    int[] distTo = new int[V];
    // 求最小值,所以 dp table 初始化为正无穷
    Arrays.fill(distTo, Integer.MAX_VALUE);
    // base case,start 到 start 的最短距离就是 0
    distTo[start] = 0;

    // 优先级队列,distFromStart 较小的排在前面
    // 普通的链表也可以,使用预先队列是为了提高效率
    Queue<State> pq = new PriorityQueue<>((a, b) -> {
        return a.distFromStart - b.distFromStart;
    });

    // 从起点 start 开始进行 BFS
    pq.offer(new State(start, 0));

    while (!pq.isEmpty()) {
        State curState = pq.poll();
        int curNodeID = curState.id;
        int curDistFromStart = curState.distFromStart;
        // 存在更短路径,那就continue
        if (curDistFromStart > distTo[curNodeID]) {
            // 已经有一条更短的路径到达 curNode 节点了
            continue;
        }
        // 将 curNode 的相邻节点装入队列
        for (int nextNodeID : adj(curNodeID)) {
            // 看看从 curNode 达到 nextNode 的 “权值和” (距离)是否会更短
            int distToNextNode = distTo[curNodeID] + weight(curNodeID, nextNodeID);
            // 从起点经过 curNode 达到 nextNode的 “权值和” 更短,那就需要更新
            if (distTo[nextNodeID] > distToNextNode) {
                // 更新 dp table
                distTo[nextNodeID] = distToNextNode;
                // 将这个节点以及距离放入队列
                pq.offer(new State(nextNodeID, distToNextNode));
            }
        }
    }
    return distTo;
}

上面的代码会计算出start起点,到任意节点的最短路径,返回结果为数组distTo,distTo[2]就是起点start到第2个节点的最短路径。

学习自:https://labuladong.gitee.io/algo/2/19/43/

对比普通的 BFS 算法,你可能会有以下疑问:

在这里插入图片描述


在这里插入图片描述


在这里插入图片描述
这里需要说明的是,Dijkstra算法求解最短路径,只能求解非负权值的问题,如果有负权值(准确的说是负环),则不能够求解!
在这里插入图片描述
如果用dijkstra算法来计算最短路径,一定会出错。
因为按照dijkstra算法,它的原则是基于贪心进行查找,所以它的查找顺序应该是:BEDC。

造成的结果就是:找到第二个节点时,就会直接判断E的最短路径为-3.并且后续节点的查找也不会改变这个值。

但是显然,我们知道E的最短路径为-4.

出现这种情况的根本原因是,负环(含负值的环)出现。导致贪心的查找逻辑无法继续成立。用通俗的话来说,就是面对负权值的环时,算法在查找时容易”鼠目寸光“,很简单的下定结论。

在这里插入图片描述
在这种情况下,djikstra算法就能正常工作,并且不受负权值影响。

它的查找顺序应该是:BCDE。而CD由于没有与E形成环,所以负权值就不再会破坏算法平衡,成为了一个表示大小的数字。

最后的结果显然是0.虽然这只是一个例子,但是我们可以很容易的想到,只要不出现负环,任何一个负权值图我们都可以用这种方法计算。

解决负环问题需要使用Bellman-Ford算法实现,注意!有些同学可能会想,通过偏移,使得权值全部大于0不就行了吗?这种想法是错误的,不能够解决负环的问题,不然也不会有专门的算法来解决负环问题。


在这里插入图片描述

// 输入起点 start 和终点 end,计算起点到终点的最短距离
int dijkstra(int start, int end, List<Integer>[] graph) {

    // ...

    while (!pq.isEmpty()) {
        State curState = pq.poll();
        int curNodeID = curState.id;
        int curDistFromStart = curState.distFromStart;

        // 在这里加一个判断就行了,其他代码不用改
        if (curNodeID == end) {
            return curDistFromStart;
        }

        if (curDistFromStart > distTo[curNodeID]) {
            continue;
        }

        // ...
    }

    // 如果运行到这里,说明从 start 无法走到 end
    return Integer.MAX_VALUE;
}

因为优先级队列自动排序的性质,每次从队列里面拿出来的都是 distFromStart 值最小的,所以当你第一次从队列中拿出终点 end 时,此时的 distFromStart 对应的值就是从 start 到 end 的最短距离。(相当于是实现普通BFS算法中,第一次到达终点的一定是距离最小的方案)

这个算法较之前的实现提前 return 了,所以效率有一定的提高。


网络延迟时间(中等)

在这里插入图片描述
在这里插入图片描述
需要注意的是,最后的答案应该是起点到所有顶点的最大的最短距离,这样才能保证所有的节点接收到信号。

class Solution {
    public int networkDelayTime(int[][] times, int n, int k) {
        // 先建图
        List<int[]>[] graph = new LinkedList[n + 1];
//         也可以写成:
//        List<Integer>[] graph = new LinkedList[n];
        // List数组记得对每一个List进行初始化,才能使用
        // 注意节点从1开始,数组大小要开成:n + 1
        for (int i = 1; i <= n; i++) {
            graph[i] = new LinkedList<>();
        }
        for (int i = 0; i < times.length; i++) {
            int from = times[i][0];
            int to = times[i][1];
            int weight = times[i][2];
            // from -> List<(to, weight)>
            // 邻接表存储图结构,同时存储权重信息weight
            graph[from].add(new int[] {to, weight});
        }
        // 开始dijkstra算法,k为起点
        int[] distTo = dijkstra(k, graph);

        // 找到最长的那一条最短路径就是答案
        int res = 0;
        for (int i = 1; i < distTo.length; i++) {
            if (distTo[i] == Integer.MAX_VALUE) {
                // 有节点到达不了
                return -1;
            }
            res = Math.max(res, distTo[i]);
        }
        return res;
    }

    // 类中类
    class State {
        // 图节点的id
        int id;
        // 从 start 节点到当前节点的最短路径
        int distFromStart;
        State(int id, int distFromStart) {
            this.id = id;
            this.distFromStart = distFromStart;
        }
    }

    int[] dijkstra(int start, List<int[]>[] graph) {
        // distTo[i],就是start到i节点的最短路径
        // distTo也是需要返回的结果数组
        int[] distTo = new int[graph.length];
        Arrays.fill(distTo, Integer.MAX_VALUE);
        // base case
        distTo[start] = 0;

        // 优先队列加快速度
        Queue<State> pq = new PriorityQueue<>(new Comparator<State>() {
            @Override
            public int compare(State o1, State o2) {
                // 更小的距离优先
                return o1.distFromStart - o2.distFromStart;
            }
        });

        // 起点入队
        pq.offer(new State(start, 0));
        while (!pq.isEmpty()) {
            State tmp = pq.poll();
            int curId = tmp.id;
            int curDist = tmp.distFromStart;
            // 当前节点的最短路径已经小于目前遍历的路径
            if (curDist > distTo[curId]) {
                continue;
            }
            // 遍历当前节点的相邻节点
            // 存储结构是:graph[start]中存储了多个[to, weight]数组
            for (int[] curNode : graph[curId]) {
                int nextNode = curNode[0];
                int weight = curNode[1];
                // 更新最短路径
                if (distTo[nextNode] > weight + curDist) {
                    distTo[nextNode] = weight + curDist;
                    pq.offer(new State(nextNode, weight + curDist));
                }
            }
        }
        return distTo;
    }
}

鉴于Dijkstra最短路径算法不能处理负环问题,但作为一种经典算法,需要学习其基本思想和实现方法。更好的算法,更具有包容性的算法是SPFA(Shortest Path Faster Algorithm)

※四、SPFA算法框架

spfa就是BellmanFord的一种实现方式,其具体不同在于,对于处理松弛操作时,采用了队列(先进先出方式)操作,从而大大提高了时间复杂度。

大概思路和dijkstra相似但是又大有不同,这里队列里存放的不再是1.距离最近且2.未被使用这两个条件同时存在的点了,

而是只保留一个条件:未被使用的点!所以又需要多的一个used数组来存储每个点是否被使用。

  • 初始化处理,邻接表头数组h(初始化为-1 ),距离数组dist(初始化为无穷大)
  • 将源点的序号加入队列中,dist[start] = 0,并将其使用状态(used)标记为true
  • 在队列不为空的前提下,抛出表头元素,并将其使用状态(used)标记为false
  • 用表头元素更新与它直接相连的点,若发生跟新则将其加入队列中,并将其使用状态(used)改为true
  • 结束之后判断是否能够到达目标节点,若可以直接返回,不可以返回题目给定内容(一般是顺便判定负环)

有负环,不可能有最短路径,因为会一直更新下去,Dijkstra不能检测负环,但是SPFA可以,这就是为什么更推荐SPFA算法的原因。

判断负环的方法:统计当前节点的最短路径包含的边的条数,如果 == n,就存在负环。

在这里插入图片描述
在这里插入图片描述

class Solution {
    public int networkDelayTime(int[][] times, int n, int k) {
        // 先建图
        List<int[]>[] graph = new LinkedList[n + 1];
//         也可以写成:
//        List<Integer>[] graph = new LinkedList[n];
        // List数组记得对每一个List进行初始化,才能使用
        // 注意节点从1开始,数组大小要开成:n + 1
        for (int i = 1; i <= n; i++) {
            graph[i] = new LinkedList<>();
        }
        for (int[] time : times) {
            int from = time[0];
            int to = time[1];
            int weight = time[2];
            // from -> List<(to, weight)>
            // 邻接表存储图结构,同时存储权重信息weight
            graph[from].add(new int[]{to, weight});
        }
        // 记录开始节点到任一节点的最短路径
        int[] distTo = new int[graph.length];
        Arrays.fill(distTo, Integer.MAX_VALUE);
        // 记录是否入队
        boolean[] vis = new boolean[graph.length];
        // 统计当前节点的遍历次数,用于判断负环
        int[] nums = new int[graph.length];
        // 初始条件
        distTo[k] = 0;
        vis[k] = true;
        // 只有一个点,不含边
        nums[k] = 0;
        // 是否为负环
        boolean flag = false;
        // SPFA开始,k为起点
        Queue<Integer> queue = new LinkedList<>();
        queue.offer(k);
        while (!queue.isEmpty()) {
            int curId = queue.poll();
            // 出队
            vis[curId] = false;
            // 遍历与该节点相邻的节点
            for (int[] next : graph[curId]) {
                int nextId = next[0];
                int weight = next[1];
                // 如果当前的更新距离更小才能更新
                if (distTo[nextId] > distTo[curId] + weight) {
                    // 更新距离
                    distTo[nextId] = distTo[curId] + weight;
                    // 当前节点的最短路径包含的边数 + 1
                    nums[nextId] = nums[curId] + 1;
                    if (nums[nextId] == n) {
                        // 是负环
                        flag = true;
                        break;
                    }
                    // 如果队列中没有,就入队
                    if (vis[nextId] == false) {
                        vis[nextId] = true;
                        queue.offer(nextId);
                    }
                }
            }
            // 是负环
            if (flag) {
                break;
            }
        }
        // 是负环了
        if (flag) {
            return -1;
        }
        int res = 0;
        for (int i = 1; i < graph.length; i++) {
            if (distTo[i] == Integer.MAX_VALUE) {
                return -1;
            }
            res = Math.max(res, distTo[i]);
        }
        return res;
    }
}

上面的代码中,为了方便,没有使用State类,队列就是直接存储节点ID即可。

一直在说负环,下面就给一道负环的题目,就是模板题

在这里插入图片描述
需要注意w >= 0时,要建立双向边。

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int t = scanner.nextInt();
        while (t > 0) {
            int n = scanner.nextInt();
            int m = scanner.nextInt();
            // 先建图
            List<int[]>[] graph = new LinkedList[n + 1];
            for (int i = 0; i < n + 1; i++) {
                graph[i] = new LinkedList<>();
            }
            for (int i = 0; i < m; i++) {
                int u = scanner.nextInt();
                int v = scanner.nextInt();
                int w = scanner.nextInt();
                if (w < 0) {
                    graph[u].add(new int[] {v, w});
                } else {
                    graph[u].add(new int[] {v, w});
                    graph[v].add(new int[] {u, w});
                }
            }
            int[] distTo = new int[n + 1];
            Arrays.fill(distTo, Integer.MAX_VALUE);
            // 从顶点1出发
            distTo[1] = 0;
            boolean[] vis = new boolean[n + 1];
            int[] nums = new int[n + 1];
            Queue<Integer> queue = new LinkedList<>();
            queue.add(1);
            vis[1] = true;
            nums[1] = 0;
            boolean flag = false;
            while (!queue.isEmpty()) {
                int curId = queue.poll();
                vis[curId] = false;
                for (int[] node : graph[curId]) {
                    int to = node[0];
                    int weight = node[1];
                    if (distTo[to] > distTo[curId] + weight) {
                        distTo[to] = distTo[curId] + weight;
                        nums[to] = nums[curId] + 1;
                        if (nums[to] == n) {
                            flag = true;
                            break;
                        }
                        if (vis[to] == false) {
                            vis[to] = true;
                            queue.offer(to);
                        }
                    }
                }
                if (flag) {
                    break;
                }
            }
            if (flag == false) {
                System.out.println("NO");
            } else {
                System.out.println("YES");
            }
            t--;
        }
    }   
}

博客又爆内存了,下面内容看图论算法四吧!

举报

相关推荐

0 条评论