图算法之最短路径总结---结合力扣每日一题进行讲解

最短路径总结

最佳在写每日一题的时候发现了连续两天所出的题目都是和图最短路径有关的,借此机会,总结一下图最短路径的常见算法以及对应的适用情况。

相关力扣题目:

算法特点

面对需要计算图最短路径的题目,要选着对应算法的时候,需要观察题目的一些条件,比如:

  • 是否有负边。【要是有负边,那就不能使用Dijkstra算法】
  • 是否有负环。【要是有负环,那就不能使用Floyd算法】
  • 图中节点个数的数据量。
  • 是需要计算单源最短路径还是计算多元最短路径。【如果是计算单源最短路径,那么就可以使用Dijkstra算法,要是是计算多源最短路径,就需要使用Floyd算法】。

上面提到了负环这个词,那么什么是负环呢,并不是说这个环里面存在负数就是负环。

负环:是这个环里面,所有边+起来的值<0,这就导致了负环的情况,这个情况Floyd算法是不能解决问题的。

算法特性

算法名称时间复杂度场景计算单源/多源
Dijkstra算法O(n^2)有负边的时候不能进行使用
Floyd算法O(n^3)有负环的时候不能进行使用
BellmanFord算法O(mn)可以用来检查是否有负环

题目一:关闭分部的可行集合数目

通过观察这个题目,首先我们看见他最多只会有10个节点,而且不存在负边,我们可以使用【Floyd】算法进行解题。

这个题目其实就是计算图中任意两个节点之间的路径,是否会大于maxDistance,要是给定的这个图,任意两个节点之间的距离都小于题目所规定的maxDistance就可以了,但是在这个基础上,还有一个条件,那就是,他会随机的关闭某个分部,那么关闭了这个分部就表示这个节点在这个图上已经是消失了,所以,我们可以枚举所有的图中可能存在的条件:

  • 关闭1节点
  • 关闭2节点

然后通过枚举所有的情况,来判断每一种情况里面的任意两个节点之间的距离是否有大于maxDistance,要是没有,说明是正确答案中的其中一个部分。

技巧

在这里有一个枚举所有情况的一个技巧,就是采用二进制压缩数组,那么什么是二进制压缩数组

举个例子:

我们图中有四个节点,那么,我们可以用四位二进制数来代替它,其中0表示这个分部是关闭的,1表示这个分部是开着的,举个例子,对于1010来说,从右到左分别表示第0-3个节点是否是处于开着或者关着的状态。如果后面需要判断,我第3个节点是否是处于开着的状态,我可以直接用(1010 & (1<<3))是否大于0的这个情况,要是说大于0,就说明这个3号节点是开着的,要是小于0,说明是关闭的,我们就使用这种二进制压缩法来记录每个节点的状态。

所以我们一开始会枚举所有的情况,枚举所有情况的方式如下:

for (int i = 0; i < (1 << n); i++) {
    if (check(i, n, maxDistance, roads)) {
        res++;
    }
}

这里可以看见有(1 << n),在n=4的情况下,(1 << n)其实这个二进制数字就是10000,对于这个for循环来说,他会从二进制0开始一直枚举到二进制1111,一共15种情况。

本体答案:

    public int numberOfSets(int n, int maxDistance, int[][] roads) {
        // 这个题是无向图,两个节点之间可能有多条边
        // 用二进制数组来判断所有的情况
        int res = 0;
        for (int i = 0; i < (1 << n) ; i++) {
            if (check(i, n, maxDistance, roads)) {
                res++;
            }
        }
        return res;
    }

    private boolean check(int u, int n, int maxDistance, int[][] roads) {
        int[][] dis = floyd(roads, n, u);
        for (int i = 0; i < n; i++) {
            // 找个两个可用的点作为起点
            if ((u & (1 << i)) == 0) { // 不可用
                continue;
            }
            for (int j = 0; j < n; j++) {
                if ((u & (1 << j)) > 0 && dis[i][j] > maxDistance) { // 不可用
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * @param edge   边
     * @param n      点的个数
     * @param enable 可用情况
     * @return
     */
    private int[][] floyd(int[][] edge, int n, int enable) {
        int[][] res = new int[n][n];
        for (int i = 0; i < n; i++) {
            Arrays.fill(res[i], Integer.MAX_VALUE / 2);
        }
        for (int i = 0; i < n; i++) {
            res[i][i] = 0;
        }
        for (int i = 0; i < edge.length; i++) {
            if ((enable & (1 << edge[i][0]))==0 || (enable & (1 << edge[i][1]))==0) continue;
            res[edge[i][0]][edge[i][1]] = Math.min(res[edge[i][0]][edge[i][1]], edge[i][2]);
            res[edge[i][1]][edge[i][0]] = Math.min(res[edge[i][1]][edge[i][0]], edge[i][2]);
        }

        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                for (int k = 0; k < n; k++) {
                    res[j][k] = Math.min(res[j][k], res[j][i] + res[i][k]);
                }
            }
        }
        return res;
    }

题目二:访问消失节点的最少时间

可以看见这个题不存在负边,但是节点个数较多,所以这里考虑采用Dijkstra算法。使用了优先队列的形式

答案:

public int[] minimumTime(int n, int[][] edges, int[] disappear) {

        List<int[]>[] adj = new List[n];
        for (int i = 0; i < n; i++) {
            adj[i] = new ArrayList<int[]>();
        }
        for (int[] edge : edges) {
            int u = edge[0], v = edge[1], length = edge[2];
            adj[u].add(new int[]{v, length});
            adj[v].add(new int[]{u, length});
        }
        PriorityQueue<int[]> pq = new PriorityQueue<int[]>((a, b) -> a[0] - b[0]);
        pq.offer(new int[]{0, 0});

        // 先用迪杰斯特拉
        boolean[] visited = new boolean[n];
        Arrays.fill(visited, false);

        // 这里是初始化dis数组
        int[] dis = new int[n];
        for (int i = 0; i < n; i++) {
            dis[i] = Integer.MAX_VALUE/2;
        }
        dis[0] = 0;
        while (!pq.isEmpty()) {
            int[] poll = pq.poll();
            int choseNodeId = poll[0];
            if (visited[choseNodeId]) continue;;
            visited[choseNodeId] = true;
            List<int[]> adjs = adj[choseNodeId];
            for (int[] ints : adjs) {
                int adNodeId = ints[0];
                int adTime = ints[1];
                if (!visited[adNodeId]){
                    if (Math.min(dis[adNodeId], dis[choseNodeId]+adTime) >= disappear[adNodeId]) continue;
                    dis[adNodeId] = Math.min(dis[adNodeId], dis[choseNodeId]+adTime);
                    pq.offer(new int[]{adNodeId, dis[adNodeId]});
                }
            }
        }
        for (int i = 0; i < disappear.length; i++) {
            if (disappear[i]<dis[i]) {
                dis[i] = -1;
            }
        }
        return dis;
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值