图论相关知识强化-长期更新

前言:图论的主要问题,如最小生成树、最短路径、拓扑排序、关键路径、查并集
提示:

图论

查并集

注:用来判断环,为了后续的kruskal

	// 查并集的存储数组,初始化一般将父结点指向自己
    private static int[] disjoint;
    
    // 查并集的递归写法,路径压缩优化
    private static int findRecur(int k) {
        if(disjoint[k] == k)
            return k;
        return disjoint[k] = findRecur(disjoint[k]);
        // findRecur(disjoint[k]); // 非路径压缩优化,直接这里返回就可
    }

    // 查并集的迭代写法,路径压缩优化
    private static int findIter(int k) {
        int r = k;
        while(disjoint[r] != r){
            r = disjoint[r];
        }
        //return r; // 非路径压缩优化,直接这里返回就可
        int cur = k, next;
        while(r != cur){
            next = disjoint[cur];
            disjoint[cur] = r; // 将根节点的所有子孙结点的父结点全部设置为根节点
            cur = next;
        }
        return r;// 找到的根节点
    }

	// 合并
    private static void merge(int xi, int yi) {
        int t1 = findRecur(xi);
        int t2 = findRecur(yi);
        if(t1 != t2){
            disjoint[t1] = t2;
        }
    }

最小生成树

prim普里姆

最小生成树(Kruskal(克鲁斯卡尔)和Prim(普里姆))算法动画演示
注:
1、下面的二维数组是目前看看过的比较直观的表现形式
2、经常把下表填一填,熟悉过程
在这里插入图片描述
最小生成树的测试用例:
/*
6 9
2 4 11
3 5 13
4 6 3
5 6 4
2 3 6
4 5 7
1 2 1
3 4 9
1 3 2
结果19

9 14
0 1 4
0 7 8
1 2 8
1 7 11
2 3 7
2 5 4
2 8 2
3 4 9
3 5 14
4 5 10
5 6 2
6 7 1
6 8 6
7 8 7
结果37
*/
注:下面代码思路中用到了上述selected数组,minDist数组,为了显示路径,部分方法用到了parent数组

public class MST{
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        while (sc.hasNext()) {
            int n = sc.nextInt(); // 结点个数
            int m = sc.nextInt(); // 边的个数
            // 初始化邻接矩阵
            int[][] edges = new int[n][n]; // 邻接矩阵表示
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    if (i != j) {
                        edges[i][j] = Integer.MAX_VALUE;
                    }
                }
            }
            // 初始化邻接表
            List<HashMap<Integer, Integer>> adjacency = new ArrayList<>(n);
            while (adjacency.size() < n) {
                adjacency.add(new HashMap<Integer, Integer>()); // 一时没找到合适的初始化数组大小的方法
            }
            // 接受输入
            for (int i = 0; i < m; i++) {
                // 注意这里顶点编号从1开始
                int v = sc.nextInt();
                int u = sc.nextInt();
                int w = sc.nextInt();
                // 起始点编号若从1开始,需要先减一
//                u--;
//                v--;
                // 起始点编号从0开始, 邻接矩阵部分
                edges[v][u] = w;
                edges[u][v] = w;
                // 邻接表部分
                adjacency.get(u).put(v, w);
                adjacency.get(v).put(u, w);
            }
            prim(edges);
            prim(adjacency);
        }

    }
    
        // prim适用于邻接表
    private static int prim(List<HashMap<Integer, Integer>> adjacency) {
        int inf = Integer.MAX_VALUE;
        int n = adjacency.size(); // 顶点个数
        int[] dist = new int[n]; // 已添加顶点到小标为i的未添加顶点的最小距离
        boolean[] visit = new boolean[n]; // 若顶点已被添加到树里,则为true
        // 设置起始结点为下标0
        // 初始化数组
        for (int i = 0; i < n; i++) {
            dist[i] = inf;
        }
        int sum = 0; // 存储最小生成树的路径长度
        int minIndex = 0;
        visit[0] = true;
        for (int count = 1; count < n; count++) {
            // Update
            Map<Integer, Integer> list = adjacency.get(minIndex);
            for (Map.Entry<Integer, Integer> sub : list.entrySet()) {
                int dest = sub.getKey();
                int distance = sub.getValue();
                if (!visit[dest] && distance < dist[dest]) {
                    dist[dest] = distance;
                }
            }
            // Scan
            int min = inf;
            for (int i = 0; i < n; i++) {
                if (!visit[i] && dist[i] < min) {
                    min = dist[i];
                    minIndex = i;
                }
            }
            // Add
            sum += min;
            visit[minIndex] = true;
        }
        System.out.println(sum);
        return sum;
    }

    // 核心就是Scan、Add、Update,适用于邻接矩阵
    private static int prim(int[][] edges) {
        int inf = Integer.MAX_VALUE;
        int n = edges.length;
        int[] dist = new int[n]; // 存储已连接顶点到未连接顶点的最短距离
        boolean[] visit = new boolean[n]; // 若顶点已被添加到树里,则为true
        // 设置起始结点为下标0
        // 初始化数组
        for (int i = 1; i < n; i++) {
            // 这里i起始为0或1均可
            dist[i] = inf;
        }
        int sum = 0; // 存储最小生成树的路径长度
        int minIndex = 0;
        visit[0] = true;
        for (int count = 1; count < n; count++) {
            // Update, 以新添加结点为弧头,未添加结点为弧尾,更新最短距离
            for (int i = 0; i < n; i++) {
                if (!visit[i] && edges[minIndex][i] < dist[i]) {
                    dist[i] = edges[minIndex][i]; // 更新最短距离
                }
            }
            // Scan, 以未添加顶点为弧尾,寻找权值最小的边
            int min = inf;
            for (int i = 0; i < n; i++) {
                if (!visit[i] && dist[i] < min) {
                    min = dist[i];
                    minIndex = i;
                }
            }
            // Add
            sum += min; // 最小生成树添加边
            visit[minIndex] = true; // 加入一个新的结点,同时更新与新节点相连的所有结点信息
        }
        System.out.println(sum);
        return sum;
    }

    // prim邻接矩阵,这里是加入parent为了显示生成路径,所以代码相对做出一些改变
    private static int prim_parent(int[][] edges) {
        int inf = Integer.MAX_VALUE;
        int n = edges.length;
        int[] dist = new int[n]; // 存储已连接顶点到未连接顶点的最短距离
        boolean[] visit = new boolean[n]; // 若顶点已被添加到树里,则为true
        int[] parent = new int[n]; // 父节点
        // 设置起始结点为下标0
        // 初始化数组
        for (int i = 0; i < n; i++) {
            dist[i] = inf;
            parent[i] = -1; // 表示没有父节点
        }
        int sum = 0; // 存储最小生成树的路径长度
        // 设置起始结点
        int minIndex = 0; // 最小距离的弧尾结点的下标
        visit[0] = true; // 以第一个结点为起始结点
        for (int count = 1; count < n; count++) { // 这里因设置了起始结点,只用计算n-1次
            // Update, 以新添加结点为弧头,未添加结点为弧尾,更新最短距离
            for (int i = 0; i < n; i++) {
                if (!visit[i] && edges[minIndex][i] < dist[i]) {
                    dist[i] = edges[minIndex][i]; // 更新最短距离
                    parent[i] = minIndex; // 设置父节点
                }
            }
            // Scan, 以未添加顶点为弧尾,寻找权值最小的边
            int min = inf;
            for (int i = 0; i < n; i++) {
                if (!visit[i] && dist[i] < min) {
                    min = dist[i];
                    minIndex = i;
                }
            }
            sum += min; // 最小生成树添加边
            // Add
            visit[minIndex] = true; // 加入一个新的结点,同时更新与新节点相连的所有结点信息
            System.out.println(Arrays.toString(visit));
            System.out.println(Arrays.toString(dist));
            System.out.println(Arrays.toString(parent));
            System.out.println("************");
        }
        System.out.println(sum);
        return sum;
    }
}

kruskal克鲁斯卡尔

注:排序挑选最小,避免成环,判断环用查并集

public class Prim {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        while (sc.hasNext()) {
            int n = sc.nextInt(); // 结点个数
            int m = sc.nextInt(); // 边的个数
            // 为了方便运用kruskal,存储顶点联系与边权重的映射,key为两个顶点v + u, value为边的权重
            Map<String, Integer> map = new HashMap<String, Integer>();
            // 接受输入
            for (int i = 0; i < m; i++) {
                // 注意这里顶点编号从1开始
                int v = sc.nextInt();
                int u = sc.nextInt();
                int w = sc.nextInt();
                // 起始点编号若从1开始,需要先减一
//                u--;
//                v--;
                // kruskal所用的map部分
                map.put(v + " " + u, w);
            }
            kruskal(map, n);
        }

    }

    private static int kruskal(Map<String, Integer> map, int n) {
        // n 表示顶点数
        ArrayList<Map.Entry<String, Integer>> list = new ArrayList<>(map.entrySet());
        list.sort((o1, o2) -> o1.getValue() - o2.getValue()); // 按边权重升序
        int[] disjoint = new int[n]; // 查并集初始数组
        for (int i = 0; i < n; i++) {
            disjoint[i] = i; // 全部初始化指向自己
        }
        int sum = 0; // 最小生成树的路径长度
        int edges = 0; // 边的个数
        for (Map.Entry<String, Integer> sub : list) {
            String[] s = sub.getKey().split(" ");
            int v = Integer.parseInt(s[0]);
            int u = Integer.parseInt(s[1]);
            int tv = find(disjoint, v); // 找到根节点
            int tu = find(disjoint, u);
            if (tv == tu) {
                continue; // 若构成了环,取下一条边
            }
            // 没有构成环
            disjoint[tv] = tu;
            sum += sub.getValue();
            edges++; //添加一条边
            // 若已添加的边数 edges == n-1 顶点数减一,则最小生成树构建完成,提前结束
            if(edges == n-1)
                break;
        }
        System.out.println(sum);
        return sum;
    }

    // 查并集,压缩路径
    private static int find(int[] disjoint, int t) {
        if (disjoint[t] == t) {
            return t;
        }
        return disjoint[t] = find(disjoint, disjoint[t]);
    }
}

最短路径

注:理解原理并掌握最容易实现写法

dijkstra迪杰斯特拉-单源最短路径

王卓老师讲解:数据结构与算法基础–第11周07–6.6图的应用7–6.6.2最短路径2–Dijkstra算法

图论最短距离(Shortest Path)算法动画演示-Dijkstra(迪杰斯特拉)和Floyd(弗洛伊德)
注:下面可以看出和prim的相似性,只是distance和minDist每添加一个结点,设置的状态有区别
在这里插入图片描述

dijkstra测试用例:
/*
9 14
0 1 4
0 7 8
1 2 8
1 7 3
2 3 7
2 5 4
2 8 2
3 4 9
3 5 14
4 5 10
5 6 2
6 7 6
6 8 6
7 8 1
[0, 4, 10, 17, 24, 14, 13, 7, 8] // 从下标为0的点到其他各点的最短距离
*/

public class Dijkstra {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        while (sc.hasNext()) {
            int n = sc.nextInt(); // 结点个数
            int m = sc.nextInt(); // 边的个数
            // 初始化邻接矩阵
            int[][] edges = new int[n][n]; // 邻接矩阵表示
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    if (i != j) {
                        edges[i][j] = Integer.MAX_VALUE;
                    }
                }
            }
            // 初始化邻接表
            List<HashMap<Integer, Integer>> adjacency = new ArrayList<>(n);
            while (adjacency.size() < n) {
                adjacency.add(new HashMap<Integer, Integer>()); // 一时没找到合适的初始化数组大小的方法
            }
            // 为了方便运用kruskal,存储顶点联系与边权重的映射,key为两个顶点v + u, value为边权重
            Map<String, Integer> map = new HashMap<String, Integer>();
            // 接受输入
            for (int i = 0; i < m; i++) {
                // 注意这里顶点编号从1开始
                int v = sc.nextInt();
                int u = sc.nextInt();
                int w = sc.nextInt();
                // 起始点编号若从1开始,需要先减一
//                u--;
//                v--;
                // 起始点编号从0开始, 邻接矩阵部分
                edges[v][u] = w;
                edges[u][v] = w;
                // 邻接表部分
                adjacency.get(u).put(v, w);
                adjacency.get(v).put(u, w);
            }
            int[] dijkstra = dijkstra(edges);
        }
    }
    // 邻接矩阵,update需要多判断一个条件,即新添加的结点到未添加的结点是否存在路径的问题
    private static int[] dijkstra(int[][] edges){
        int inf = Integer.MAX_VALUE;
        int n = edges.length;
        int[] dist = new int[n];
        boolean[] visit = new boolean[n]; // 若顶点已被添加到树里,则为true
        int[] parent = new int[n]; // 父节点
        // 设置起始结点为下标0
        // 初始化数组
        for (int i = 0; i < n; i++) {
            dist[i] = inf;
            parent[i] = -1; // 表示没有父节点
        }
        // 设置起始结点
        int minIndex = 0; // 最小距离的弧尾结点的下标
        visit[0] = true; // 以第一个结点为起始结点
        dist[0] = 0;
        for (int count = 1; count < n; count++) { // 这里因设置了起始结点,只用计算n-1次
            // Update, 以新添加结点为弧头,未添加结点为弧尾,更新最短距离
            for (int i = 0; i < n; i++) {
                // 从新添加的点走向所有未添加的点,若能走通且与新添加的点的最短距离之和 < 原先未添加点的最短距离就更新
                if (!visit[i] && edges[minIndex][i] < inf && (dist[minIndex] + edges[minIndex][i]) < dist[i]) {
                    dist[i] = dist[minIndex] + edges[minIndex][i]; // 更新最短距离
                    parent[i] = minIndex ; // 设置父节点
                }
            }
            // Scan, 以未添加顶点为弧尾,寻找权值最小的边
            int min = inf;
            for (int i = 0; i < n; i++) {
                if (!visit[i] && dist[i] < min) {
                    min = dist[i];
                    minIndex = i;
                }
            }
            // Add
            visit[minIndex] = true; // 加入一个新的结点,同时更新与新节点相连的所有结点信息
            System.out.println(Arrays.toString(visit));
            System.out.println(Arrays.toString(dist));
            System.out.println(Arrays.toString(parent));
            System.out.println("************");
        }
        return dist;
    }

    // 邻接表,邻接表取出的就是可行路径,只是需要更新最短路径
    private static int[] dijkstra(List<HashMap<Integer, Integer>> adjacency){
        int inf = Integer.MAX_VALUE;
        int n = adjacency.size();
        int[] dist = new int[n];
        boolean[] visit = new boolean[n]; // 若顶点已被添加到树里,则为true
        int[] parent = new int[n]; // 父节点
        // 设置起始结点为下标0
        // 初始化数组
        for (int i = 0; i < n; i++) {
            dist[i] = inf;
            parent[i] = -1; // 表示没有父节点
        }
        // 设置起始结点
        int minIndex = 0; // 最小距离的弧尾结点的下标
        visit[0] = true; // 以第一个结点为起始结点
        dist[0] = 0;
        for (int count = 1; count < n; count++) { // 这里因设置了起始结点,只用计算n-1次
            // Update, 以新添加结点为弧头,未添加结点为弧尾,更新最短距离
            Map<Integer, Integer> list = adjacency.get(minIndex);
            for (Map.Entry<Integer, Integer> sub : list.entrySet()) {
                int dest = sub.getKey();
                int distance = sub.getValue();
                if (!visit[dest] && dist[minIndex] + distance < dist[dest]) {
                    dist[dest] = dist[minIndex] + distance;
                    
                    parent[dest] = minIndex; // 更新父结点
                }
            }
            // Scan, 以未添加顶点为弧尾,寻找权值最小的边
            int min = inf;
            for (int i = 0; i < n; i++) {
                if (!visit[i] && dist[i] < min) {
                    min = dist[i];
                    minIndex = i;
                }
            }
            // Add
            visit[minIndex] = true; // 加入一个新的结点,同时更新与新节点相连的所有结点信息
            System.out.println(Arrays.toString(visit));
            System.out.println(Arrays.toString(dist));
            System.out.println(Arrays.toString(parent));
            System.out.println("************");
        }
        return dist;
    }
}

floyd

注:
方便理解:数据结构与算法基础–第11周08–6.6图的应用8–6.6.2最短路径3–Floyd算法
实际应用迭代:图论最短距离(Shortest Path)算法动画演示-Dijkstra(迪杰斯特拉)和Floyd(弗洛伊德)

在这里插入图片描述

拓扑排序

关键路径

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值