图的最短路径和最小生成树算法

一. 最短路径

单源最短路径问题:给定一个图G = ( V , E ) ,求源结点s ∈ V 到图中每个结点v ∈ V的最短路径。
 

1.dijkstra算法

Dijkstra算法就适用于解决带权重的有向图上的单源最短路径问题,同时算法要求图中所有边的权重非负
针对一个带权有向图G,将所有结点分为两组S和Q,S是已经确定最短路径的结点集合,在初始时为空(初始时就可以将源节点s放入,毕竟源节点到自己的代价是0),Q 为其余未确定最短路径的结点集合,每次从Q中找出一个起点到该结点代价最小的结点u ,将u 从Q 中移出,并放入S 中,对u 的每一个相邻结点v 进行松弛操作。松弛即对每一个相邻结点v ,判断源节点s到结点u 的代价与u 到v 的代价之和是否比原来s 到v 的代价更小,若代价比原来小则要将s 到v 的代价更新为s 到u 与u 到v 的代价之和,否则维持原样。(即以以u为拓展点, 去调整更新相邻的未拓展点与源点之间的最短路径), 如此一直循环直至集合Q 为空,即所有节点都已经查找过一遍并确定了最短路径,至于一些起点到达不了的结点在算法循环后其代价仍为初始设定的值,不发生变化。Dijkstra算法每次都是选择V-S中最小的路径节点来进行更新,并加入S中,所以该算法使用的是贪心策略
 

import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Scanner;

public class Dijkstra {

    public static void main(String[] args) {
        //图用list[]数组表示, list[i] = {......}, i为出点, 的边
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        //用邻接矩阵
        int[][] matrix = new int[n][n];
        int m = scanner.nextInt();//m条边
        for (int i = 0; i < n; i++) {
            Arrays.fill(matrix[i], 0x3f3f3f3f);
        }
        for (int i = 0; i < n; i++) {
            matrix[i][i] = 0;
        }
        for (int i = 0; i < m; i++) {
            int v = scanner.nextInt();
            int j = scanner.nextInt();
            matrix[v][j] = matrix[j][v] = scanner.nextInt();
        }
        int[] dp = getMinDistance(matrix);
        System.out.println(Arrays.toString(dp));

    }

    /**
     * 源点即起始点默认为0
     * 图的节点是0 到 n - 1
     *
     * @param matrix
     * @return
     */
    public static int[] getMinDistance(int[][] matrix) {
        int n = matrix.length;
        int[] dp = new int[n];
        boolean[] visited = new boolean[n];
//visited数组判断当前顶点是否拓展过了
        PriorityQueue<int[]> queue = new PriorityQueue<>(new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[1] - o2[1];
            }
        });
        //优先级队列加入的二元组,第一个元素是入点, 
        //第二点是边的大小,小根堆用来找离源点最近并且还没有拓展的点
        visited[0] = true;
        for (int j = 1; j < n; j++) {
            dp[j] = matrix[0][j];
            queue.add(new int[]{j, dp[j]});
        }
        int count = 1;
        while (count < n) {//所有点都拓展了, 就说明计算结束了
            int[] res = null;
            //队列不为空且顶点已经拓展了, 那么就一直找到没有拓展的点
            while (!queue.isEmpty() && visited[queue.peek()[0]]) {
                queue.poll();
            }
            if (!queue.isEmpty()) {
                res = queue.peek();
            }
            visited[res[0]] = true;
            //更新其他未拓展点与源点的距离
            for (int j = 1; j < n; j++) {
                if (!visited[j]) {
                    dp[j] = Math.min(dp[j], dp[res[0]] + matrix[res[0]][j]);
                    queue.add(new int[]{j, dp[j]});
                }
            }
            count++;
        }
        return dp;
    }

}

2.Floyd-Warshall算法

Floyd-Warshall算法是解决任意两点间的最短路径的一种算法。这是一种动态规划思路的算法, 即区间上的动态规划.

Floyd算法考虑的是一条最短路径的中间节点,即简单路径p={v1,v2,…,vn}上除v1和vn的任意节点。 设k是p的一个中间节点,那么从i到j的最短路径p就被分成i到k和k到j的两段最短路径p1,p2。p1是从i到k且中间节点属于{1,2,…,k-1}取得的一条最短路径。p2是从k到j且中间节点属于{1,2,…,k-1}取得的一条最短路径

import java.util.Arrays;
import java.util.Scanner;

public class Floyd {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();//输入有几个顶点
        //用邻接矩阵
        int[][] matrix = new int[n][n];
        int m = scanner.nextInt();//有m条边
        for (int i = 0; i < n; i++) {
            Arrays.fill(matrix[i], 0x3f3f3f3f);
        }
        for (int i = 0; i < n; i++) {
            matrix[i][i] = 0;
        }
        for (int i = 0; i < m; i++) {
            int v = scanner.nextInt();//出点
            int j = scanner.nextInt();//入点
            matrix[v][j] = matrix[j][v] = scanner.nextInt();//权值
        }

        int[][] dp = getMinDistance(matrix);

        for (int i = 0; i < n; i++) {
            System.out.println(Arrays.toString(dp[i]));
        }
    }

    public static int[][] getMinDistance(int[][] matrix) {
        int n = matrix.length;
        int[][] dp = new int[n][n];//dp[i][j] i顶点到j顶点的最短路径
        for (int i = 0; i < n; i++) {
            System.arraycopy(matrix[i], 0, dp[i], 0, n);
        }
        //由于中间点都是从0到n - 1, i和j很难直接确定, 
        //如果都是按一定规律, 那么会出现k比i,j都大时, 有的路径还没有算
        //所以中间点的变化作为遍历顺序

        for (int k = 0; k < n; k++) {//中间顶点为k
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    dp[i][j] = Math.min(dp[i][j], dp[i][k] + dp[k][j]);
                }
            }
        }

        return dp;
    }
}

二. 最小生成树

连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从其中删去任何一条边,生成树 就不在连通;反之,在其中引入任何一条新边,都会形成一条回路。

若连通图由n个顶点组成,则其生成树必含n个顶点和n-1条边。因此构造最小生成树的准则有三 条:

1. 只能使用图中的边来构造最小生成树

2. 只能使用恰好n-1条边来连接图中的n个顶点

3. 选用的n-1条边不能构成回路

构造最小生成树的方法:Kruskal算法和Prim算法。这两个算法都采用了逐步求解的贪心策略。

1.Kruskal算法

任给一个有n个顶点的连通网络N={V,E},

首先构造一个由这n个顶点组成、不含任何边的图G={V,NULL},其中每个顶点自成一个连通分量,
其次不断从E中取出权值最小的一条边(若有多条任取其一),若该边的两个顶点来自不同的连通分量,则将此边加入到G中。
如此重复,直到所有顶点在同一个连通分量上为止。

核心:每次迭代时,选出一条具有最小权值,且两端点不在同一连通分量上的边,加入生成树。

实现起来很简单, 将所有的边加入优先级队列, 优先级队列采用小根堆, 选择最小边,同时为了避免成环, 使用并查集去判断要加入的边是否会成环, 只要两个顶点不全是是最小生成树那个集合就可以.并查集在操作时, 还可以进行路径压缩

import java.util.*;

public class Kruskal {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        //用邻接矩阵
        int[][] matrix = new int[n][n];
        int m = scanner.nextInt();//m条边
        for (int i = 0; i < n; i++) {
            Arrays.fill(matrix[i], 0x3f3f3f3f);
        }
        for (int i = 0; i < n; i++) {
            matrix[i][i] = 0;
        }
        PriorityQueue<int[]> minQueue = new PriorityQueue<>(new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[2] - o2[2];
            }
        });

        for (int i = 0; i < m; i++) {
            int v = scanner.nextInt();
            int j = scanner.nextInt();
            matrix[v][j] = matrix[j][v] = scanner.nextInt();
            minQueue.add(new int[]{v, j, matrix[v][j]});
        }
        List<int[]> minTree = minTree(minQueue, n);
        for (int[] edge : minTree) {
            System.out.println(Arrays.toString(edge));
        }
        System.out.println(minTreeSum);
    }

    public static int minTreeSum = 0;

    public static List<int[]> minTree(PriorityQueue<int[]> minQueue, int n) {
        //new一个并查集
        UnionSet set = new UnionSet(n);
        //v是最小生成树的第一条最小边的源点, 用这个点去判断是否成环
        int v = minQueue.peek()[0];
        List<int[]> res = new LinkedList<>();
        while (res.size() < n - 1) {
            if (minQueue.peek() != null) {
                int[] edge = minQueue.peek();
                minQueue.poll();
                //如果两个顶点都是最小生成树集合, 那么必定成环
                if (!(set.isSameSet(v, edge[0]) && set.isSameSet(v, edge[1]))) {
                    set.merge(v, edge[0]);
                    set.merge(v, edge[1]);
                    res.add(edge);
                    minTreeSum += edge[2];
                }
            }
        }
        return res;
    }
}


class UnionSet {
    int[] elem;

    UnionSet(int n) {
        this.elem = new int[n];
        Arrays.fill(elem, -1);
    }

    public int getRoot(int i) {
        //找根的同时使用路径压缩
        int temp = i;
        while (elem[i] != -1) {
            i = elem[i];
            if (i == elem[i]) {
                return i;
            }
        }
        //i是根, temp不是根
        elem[temp] = i;
        return i;
    }

    public boolean isSameSet(int i, int j) {
        if (i == j) {
            return true;
        }
        int root1 = getRoot(i);
        int root2 = getRoot(j);
        return root1 == root2;
    }

    public void merge(int i, int j) {
        int root = getRoot(i);
        elem[j] = root;
    }
}

2.Prim算法

import java.util.Comparator;
import java.util.HashSet;
import java.util.PriorityQueue;
import java.util.Set;

public int prim(GraphByMatrix minTree, char chV) {
//1. 先获取当前顶点的下标
    int srcI = getIndexOfV(chV);
//2. 定义一个X集合,把当前的起点下标存进去
    Set<Integer> setX = new HashSet<>();
    setX.add(srcI);
//3. 定义一个Y集合,存储目标顶点的元素
    Set<Integer> setY = new HashSet<>();
//4. 除了刚刚的起点,其他的顶点需要放到Y集合
    int n = matrix.length;
    for (int i = 0; i < n; i++) {
        if (i != srcI) {
            setY.add(i);
        }
    }
    //5. 从X集合中的点到Y集合的点中,连接的边中找出最小值 放到优先级队列
    PriorityQueue<Edge> minHeap = new PriorityQueue<>(new Comparator<Edge>() {
        @Override
        public int compare(Edge o1, Edge o2) {
            return o1.weight - o2.weight;
        }
    });
//6. 把当前顶点连接出去的所有的边 放入队列
    for (int i = 0; i < n; i++) {
        if (matrix[srcI][i] != 0) {
            minHeap.offer(new Edge(srcI, i, matrix[srcI][i]));
        }
    }
    int total = 0;
    int edgeCount = 0;
    while (!minHeap.isEmpty()) {
//7. 取出队列中的第一条边
        Edge min = minHeap.poll();
        int srcIndex = min.srcIndex;
        int destIndex = min.destIndex;
/**
 * 开始写的时候,不用写if,直接把选出来的边直接放入队列
 *
 * 14.添加边的时候,如果目标点也在我这个集合当中,则不能添加
 * 所以得判断X集合 ,否则就是环。
 *
 * 起始点本身就在X集合,所以这里只需要判断目标点即可
 */
        if (setX.contains(destIndex)) {
//System.out.println("构成环:");
// System.out.println(arrayV[srcIndex] + "-> " + arrayV[destIndex] + " : " + matrix[srcIndex][destIndex]);
        } else {
//8. 直接将该边 放入最小生成树
            minTree.addEdgeUseIndex(srcIndex, destIndex, matrix[srcIndex][destIndex]);
//9. 每选一条边 就打印一条语句
            System.out.println(arrayV[srcIndex] + "-> " + arrayV[destIndex] + " : " + matrix[srcIndex][destIndex]);
            edgeCount++;//需要添加n-1条边
            total += matrix[srcIndex][destIndex];
            if (edgeCount == n - 1) {
                break;
            }
            //10. 把这次的目标点,添加到X集合,变成了起点
            setX.add(destIndex);
//11.记得把之前的目标点,从Y集合删除掉
            setY.remove(destIndex);
//12. 遍历刚刚添加的新起点destIndex,连接出去的所有边,再次添加到优先级队列
            for (int i = 0; i < n; i++) {
// 13. !setX.contains(i) 判断目标点不能再X这个集合 例如: a->b 就包含了b->a
                if (matrix[destIndex][i] != 0 && !setX.contains(i)) {
                    minHeap.offer(new Edge(destIndex, i, matrix[destIndex][i]));
                }
            }
        }
    }
    if (
            edgeCount == n - 1) {
        return total;
    } else {
        return -1;
    }
}

public static void testGraphMinTree() {
    String str = "abcdefghi";
    char[] array = str.toCharArray();
    GraphByMatrix g = new GraphByMatrix(str.length(), false);
    g.initArrayV(array);
    g.addEdge('a', 'b', 4);
    g.addEdge('a', 'h', 8);
//g.addEdge('a', 'h', 9);
    g.addEdge('b', 'c', 8);
    g.addEdge('b', 'h', 11);
    g.addEdge('c', 'i', 2);
    g.addEdge('c', 'f', 4);
    g.addEdge('c', 'd', 7);
    g.addEdge('d', 'f', 14);
    g.addEdge('d', 'e', 9);
    g.addEdge('e', 'f', 10);
    g.addEdge('f', 'g', 2);
    g.addEdge('g', 'h', 1);
    g.addEdge('g', 'i', 6);
    g.addEdge('h', 'i', 7);
}
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雨落寒山意

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值