图论(1)模板题

一,最短路径问题

问题描述

小蓝学习了最短路径之后特别高兴,他定义了一个特别的图,希望找到图 中的最短路径。 小蓝的图由 2021 个结点组成,依次编号 1 至 2021。 对于两个不同的结点 a, b,如果 a 和 b 的差的绝对值大于 21,则两个结点 之间没有边相连;如果 a 和 b 的差的绝对值小于等于 21,则两个点之间有一条 长度为 a 和 b 的最小公倍数的无向边相连。 例如:结点 1 和结点 23 之间没有边相连;结点 3 和结点 24 之间有一条无 向边,长度为 24;结点 15 和结点 25 之间有一条无向边,长度为 75。 请计算,结点 1 和结点 2021 之间的最短路径长度是多少。 提示:建议使用计算机编程解决问题。

思路分析

无最短路径情况

  • 没有任何一条路径可以从起点到终点

  • 存在负权回路

n:顶点数,m:边数

最短路径有四种解法,分别是Floyd(弗洛伊德)算法,Dijkstra(迪杰斯特拉)算法,Bellman算法,spfa算法

Floyd(弗洛伊德)多源:算法最好写,而且可以求负权边,但时间复杂度为O(n3),空间复杂度为O(n2)

Dijkstra(迪杰斯特拉)单源:算法时间复杂度虽然为O(n2),但不可求负权边

Bellman算法虽然可以求负权边 单源,但时间复杂度为O(nm)

spfa算法:可求数据量很大,支持负权边,不稳定

选择:

由于该题是填空题,不要求空间复杂度和时间复杂度,只要求结果准确,所以在时间允许的情况下,可选择最容易写的Floyd算法

题目顶点数n=2021,用计算器算2021的三次方为8,254,655,261,计算器运行一次大概需要一秒钟,除去八位数,后面是多少就是多少秒,所以

预估时间:82秒,可以接受

那用邻接表还是邻接矩阵存储图呢?

稀疏图用邻接表(java可用HashMap实现)

稠密图用邻接矩阵

判断:如果边数小于顶点n倍的以2为底n的对数,就是稀疏图,反之就是稠密图

a 和 b 的差的绝对值大于 21,两个点就有边相连,粗略估计是稠密图,用邻接矩阵

代码实现

Floyd-邻接矩阵-输出路径

public class Main {
    public static int mp[][]=new int[2022][2022];
    public static int path[][]=new int[2022][2022];
    //mp[i][k]+mp[k][j]相加为防溢出,所以除2
    public static int  max=Integer.MAX_VALUE/2;
    public static void main(String[] args) {
        
        //1.构建图
        for(int i=1;i<2022;i++) {
            for(int j=1;j<2022;j++) {
                if(isXiangLian(i,j)) {
                    mp[i][j]=Elength(i,j);
                    continue;
                }
                mp[i][j]=max;
            }
        }
//        //2Floyd算法求最短路径
        for(int k=1;k<2022;k++) {
            for(int i=1;i<2022;i++) {
                for(int j=1;j<2022;j++) {
//                    mp[i][j]=Math.min(mp[i][k]+mp[k][j], mp[i][j]);
                    if(mp[i][j]>mp[i][k]+mp[k][j]) {
                        
                        mp[i][j]=mp[i][k]+mp[k][j];
                        path[i][j]=k;
                    }
                }
            }
        }
       //3.输出
        System.out.println(mp[1][2021]);
        luji(1, 2021);
    }
    //输出路径
    public static void luji(int dot1,int dot2) {
        int k=path[dot1][dot2];
        if(k==0) {
            System.out.println(dot1+"->"+dot2);
            return;
        }
        luji(dot1,k);
        luji(k, dot2);
    }
    
    //判断两个点是否有边相连
    public static Boolean isXiangLian(int dot1,int dot2){
        return Math.abs(dot1-dot2)<=21;
    }
    //计算两个有边相连点的长度(权值)
    public static int Elength(int dot1,int dot2) {
        int t,t1=dot1,t2=dot2;
        while((t=t1%t2)!=0) {
            t1=t2;
            t2=t;
        }
        return dot1*dot2/t2;
    }
    

}

spfa-邻接表-输出路径

package zuiduanlujing;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;

public class Spfa {
    //判断该点是否在队列中
    public static int vic[] = new int[2022];
    //存放需要更新的点
    public static Queue<Integer> queue = new LinkedList<>();
    //记录每个点更新次数
    public static int judge[] = new int[2022];
    //存放每个点所连的边
    public static Map<Integer, List<Side>> map = new HashMap<>();
    //记录每个点到起点的最短距离
    public static int dist[] = new int[2022];
    //一个很大的数,如果直接是Integer.MAX_VALUE的话两个数相加会溢出
    public static int max = Integer.MAX_VALUE / 2;
    //存放一个点到另外一个点需要经过的中间点,没有则是1或0
    public static int[][] path=new int [2022][2022];

    public static void main(String[] args) {
        // 1.构建边
        for (int i = 1; i < 2022; i++) {
            List<Side> sides = new ArrayList<>();
            for (int j = 1; j < 2022; j++) {
                if (i == j) {
                    continue;
                }
                if (isXiangLian(i, j)) {
                    sides.add(new Side(j, Elength(i, j)));
                }
            }
            map.put(i, sides);
        }
        // 2.初始化dist
        dist[1] = 0;
        for (int i = 2; i < 2022; i++) {
            dist[i] = max;
        }
        queue.offer(1);
        vic[1] = 1;
        judge[1]++;
        // 3.spfa求最短路径
        while (!queue.isEmpty()) {
            // 队头的点出队
            int start = queue.poll();
            vic[start] = 0;
            // 获取与start相连的所有点的边
            List<Side> sides = map.get(start);
            for (Side side : sides) {
                if (dist[side.end] <= dist[start] + side.quanZhi) {
                    continue;
                }
                path[1][side.end]=start;
                dist[side.end] = dist[start] + side.quanZhi;
                // 如果该点已经在队列中了则不用再入队了
                if (vic[side.end] == 1) {
                    continue;
                }
                queue.offer(side.end);
                vic[side.end] = 1;
                judge[side.end]++;
                if (judge[side.end] > 2021) {
                    System.out.println("有负环");
                    return;
                }

            }

        }
        System.out.println(dist[2021]);
        luji(1, 2021);

    }

//    //输出路径
        public static void luji(int dot1,int dot2) {
            int k=path[dot1][dot2];
            if(k==0||k==1) {
                System.out.println(dot1+"->"+dot2);
                return;
            }
            luji(dot1,k);
            luji(k, dot2);
        }

    // 判断两个点是否有边相连
    public static Boolean isXiangLian(int dot1, int dot2) {
        return Math.abs(dot1 - dot2) <= 21;
    }

    // 计算两个有边相连点的长度(权值)
    public static int Elength(int dot1, int dot2) {
        int t, t1 = dot1, t2 = dot2;
        while ((t = t1 % t2) != 0) {
            t1 = t2;
            t2 = t;
        }
        return dot1 * dot2 / t2;
    }
}9

class Side {
    public int end;
    public int quanZhi;

    public Side() {
    }

    public Side(int end, int quanZhi) {
        this.end = end;
        this.quanZhi = quanZhi;
    }
}

Bellman-边-输出路径

package zuiduanlujing;

import java.util.ArrayList;
import java.util.List;

public class Main3 {
    public static List<SIDE> sides = new ArrayList<>();
    public static int max = Integer.MAX_VALUE / 2;
    public static int[] dist = new int[2022];
    public static int path[][] = new int[2022][2022];

    public static void main(String[] args) {
        // 1.将所有边存进eList
        for (int i = 1; i < 2022; i++) {
            for (int j = 1; j < 2022; j++) {
                if (isXiangLian(i, j)) {
                    sides.add(new SIDE(i, j, Elength(i, j)));
                }
            }
        }
        // n记录点数,m记录边数
        int n = 2021;
        int m = sides.size();
        // 2.初始化dist
        dist[1] = 0;
        for (int i = 2; i < 2022; i++) {
            dist[i] = max;
        }
        // 3.Bellman算法求最短路径,顶点1已经dist[1]=0,所以从2开始
        for (int i = 2; i <= n; i++) {
            for (int j = 0; j < m; j++) {
                SIDE side = sides.get(j);
                if (dist[side.end] > dist[side.start] + side.quanZhi) {
                    dist[side.end] = dist[side.start] + side.quanZhi;
                    path[1][side.end]=side.start;
                }
            }
        }
        // 4.判断有无负环,如果还能更新,则存在负环
        for (int j = 0; j < m; j++) {
            SIDE side = sides.get(j);
            if (dist[side.end] > dist[side.start] + side.quanZhi) {
                System.out.println("存在负环");
                return;
            }
        }

        System.out.println(dist[2021]);
        luji(1, 2021);
    }
    // 输出路径
        public static void luji(int dot1, int dot2) {
            int k = path[dot1][dot2];
            //和顶点1直接相连的点会被赋值为1,除此之外直接相连的没有赋值的点为0
            if (k == 1||k==0) {
                System.out.println(dot1 + "->" + dot2);
                return;
            }
            luji(dot1, k);
            luji(k, dot2);
        }
    // 判断两个点是否有边相连
    public static Boolean isXiangLian(int dot1, int dot2) {
        return Math.abs(dot1 - dot2) <= 21;
    }

    // 计算两个有边相连点的长度(权值)
    public static int Elength(int dot1, int dot2) {
        int t, t1 = dot1, t2 = dot2;
        while ((t = t1 % t2) != 0) {
            t1 = t2;
            t2 = t;
        }
        return dot1 * dot2 / t2;
    }
}

class SIDE {
    public int start;
    public int end;
    public int quanZhi;

    public SIDE(int start, int end, int quanZhi) {
        this.start = start;
        this.end = end;
        this.quanZhi = quanZhi;
    }
}

Dijkstra-邻接矩阵-输出路径(无法求带负权的图)

 

public class Main2 {
    public static int mp[][] = new int[2022][2022];
    public static int path[][] = new int[2022][2022];
    public static int max = Integer.MAX_VALUE / 2;
    // 记录起点到里面每个元素的距离
    public static int[] dist = new int[2022];
    public static int[] vic = new int[2022];

    public static void main(String[] args) {
        // 1.构建图
        for (int i = 1; i < 2022; i++) {
            for (int j = 1; j < 2022; j++) {
                if (isXiangLian(i, j)) {
                    mp[i][j] = Elength(i, j);
                    continue;
                }
                mp[i][j] = max;
                // 将dist所有值初始为一个很大的值
                dist[i] = max;
            }
        }

        // 2.Dijkstra算法求最短路径
        // 起点到起点为0
        dist[1] = 0;

        for (int i = 2; i < 2022; i++) {
            dist[i] = Math.min(dist[i], mp[1][i]);
        }
        vic[1] = 1;
        for (int i = 2; i < 2022; i++) {
            // 选出从起点到某点的距离最小的点开始拓展
            int min = max;
            int k = -1;
            for (int j = 1; j < 2022; j++) {
                if (vic[j] == 1) {
                    continue;
                }
                if (min > dist[j]) {
                    min = dist[j];
                    k = j;
                }
            }

            vic[k] = 1;
            // 开始从k点拓展
            for (int j = 1; j < 2022; j++) {
                if (vic[j] == 1) {
                    continue;
                }
                if (dist[j] > dist[k] + mp[k][j]) {
                    dist[j] = dist[k] + mp[k][j];
                    path[1][j] = k;

                }
            }

        }

        System.out.println(dist[2021]);

        luji(1, 2021);
    }

    // 输出路径
    public static void luji(int dot1, int dot2) {
        int k = path[dot1][dot2];
        if (k == 0) {
            System.out.println(dot1 + "->" + dot2);
            return;
        }
        luji(dot1, k);
        luji(k, dot2);
    }

    // 判断两个点是否有边相连
    public static Boolean isXiangLian(int dot1, int dot2) {
        return Math.abs(dot1 - dot2) <= 21;
    }

    // 计算两个有边相连点的长度(权值)
    public static int Elength(int dot1, int dot2) {
        int t, t1 = dot1, t2 = dot2;
        while ((t = t1 % t2) != 0) {
            t1 = t2;
            t2 = t;
        }
        return dot1 * dot2 / t2;
    }
}

 

二,最长路径问题

问题描述

给定一个有向加权无环图G,从G中找出无入度的顶点s,求从s出发到顶点E最长路径

有向无权图

 

输入样例

0 1 1
0 3 2
1 2 6
2 5 2
2 4 1
3 1 4
3 4 3
4 5 1
end

其中第一列和第二列0,1,2,3,4,5分别代表顶点SABCDE,第三列代表第一列和第二列连接的边的权值

测环数据

0 1 1
0 3 2
1 2 6
2 5 2
2 4 1
3 1 4
4 3 3
4 5 1
end

 

思路分析

没有最长路径情况

  • 没有任何一条路径可以到达终点

  • 存在环路,没经过一次环路最长路径就会加一次,最长路径会变成无穷大

肯定不能用dijkstra算法,这是因为,Dijkstra算法的大致思想是每次选择距离源点最近的结点加 入,然后更新其它结点到源点的距离,直到所有点都被加入为止。当每次选择最短的路改为每次选择最长路的时候,出现了一个问题,那就是不能保证现在加入的结 点以后是否会被更新而使得到源点的距离变得更长,而这个点一旦被选中将不再会被更新。例如这次加入结点u,最长路为10,下次有可能加入一个结点v,使得 u通过v到源点的距离大于10,但由于u在之前已经被加入到集合中,无法再更新,导致结果是不正确的。

如果取反用dijkstra求最短路径呢,记住,dijkstra不能计算有负边的情况。。。

可 以用 Bellman-Ford 算法求最长路径,只要把图中的边权改为原来的相反数即可。也可以用Floyd-Warshall 算法求每对节点之间的最长路经,因为最长路径也满足最优子结构性质,而Floyd算法的实质就是动态规划。但是,如果图中含有回路,Floyd算法并不能 判断出其中含有回路,且会求出一个错误的解;而Bellman-Ford算法则可以判断出图中是否含有回路。spfa就是Bellman算法的优化;

so

无环可用Floyd,spfa

有环 spfa

如果是有向无环图,先拓扑排序,再用动态规划求解。

代码实现

Floyd-邻接矩阵(不能判断是否有环)

package zuiChangLuJing;
import java.util.Scanner;
public class Floyd {
    public static char[] dot = "SABCDE".toCharArray();
    public static int map[][] = new int[dot.length][dot.length];
    public static int[][] path=new int[dot.length][dot.length];

    public static void main(String[] args) {
        // 1.构建图
        Scanner s = new Scanner(System.in);
        while (true) {
            String strI = s.next();
            if (strI.equals("end")) {
                break;
            }
            int i = Integer.parseInt(strI);
            int j = s.nextInt();
            int quanZhi = s.nextInt();
            map[i][j] = quanZhi;
        }
        // 2.floyd
        for (int k = 0; k < dot.length; k++) {
            for (int i = 0; i < dot.length; i++) {
                for (int j = 0; j < dot.length; j++) {
                    //ik或kj无边相连
                    if(map[i][k]==0||map[k][j]==0) {
                        continue;
                    }
                    if(map[i][j]<map[i][k]+map[k][j]) {
                        map[i][j]=map[i][k]+map[k][j];
                        path[i][j]=k;
                    }
                }
            }
        }
       System.out.println(map[0][5]);
       luji(0,5);
    }
    //输出路径
        public static void luji(int dot1,int dot2) {
            int k=path[dot1][dot2];
            if(k==0) {
                System.out.println(dot[dot1]+"->"+dot[dot2]);
                return;
            }
            luji(dot1,k);
            luji(k, dot2);
        }
}

spfa求最长路径-邻接表-输出路径

package zuiChangLuJing;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Scanner;
import java.util.concurrent.LinkedBlockingDeque;

public class Spfa {
    public static char[] dot = "SABCDE".toCharArray();
    public static Map<Integer, List<Side>> map = new HashMap<>();
    public static int[][] path = new int[dot.length][dot.length];
    public static Queue<Integer> queue = new LinkedBlockingDeque<>();
    public static int judge[] = new int[dot.length];
    public static int vic[] = new int[dot.length];
    public static int dist[] = new int[dot.length];

    public static void main(String[] args) {
        Scanner s = new Scanner(System.in);
        while (true) {
            String str = s.next();
            if (str.equals("end")) {
                break;
            }
            int i = Integer.parseInt(str);
            int j = s.nextInt();
            int quanZhi = s.nextInt();
            List<Side> list = map.getOrDefault(i, new ArrayList<Side>());
            list.add(new Side(j, quanZhi));
            map.put(i, list);
        }

        // spfa
        dist[0] = 0;
        queue.offer(0);
        vic[0] = 1;
        judge[0]++;
        while (!queue.isEmpty()) {
            int from = queue.poll();
            vic[from] = 0;
            List<Side> sides = map.get(from);
            if (sides == null) {
                continue;
            }
            for (Side side : sides) {
                if (dist[side.end] >= dist[from] + side.quanZhi) {
                    continue;
                }
                dist[side.end] = dist[from] + side.quanZhi;
                path[0][side.end] = from;
                if (vic[side.end] == 1) {
                    continue;
                }
                queue.offer(side.end);
                vic[side.end] = 1;
                judge[side.end]++;
                if (judge[side.end] > dot.length) {
                    System.out.println("有环");
                    return;
                }
            }
        }

        System.out.println(dist[5]);
        luji(0, 5);
    }

    public static void luji(int dot1, int dot2) {
        int k = path[dot1][dot2];
        if (k == 0) {
            System.out.println(dot[dot1] + "->" + dot[dot2]);
            return;
        }
        luji(dot1, k);
        luji(k, dot2);
    }
}

class Side {
    public int end;
    public int quanZhi;

    public Side(int end, int quanZhi) {
        this.end = end;
        this.quanZhi = quanZhi;
    }
}

三,拓扑排序

问题描述

以leecode课程表为例

现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。

例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。

 示例

示例 1:

输入:numCourses = 2, prerequisites = [[1,0]]
输出:[0,1]
解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。

示例 2:

输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
输出:[0,2,1,3]
解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。

示例 3:

输入:numCourses = 1, prerequisites = []
输出:[0]

提示:

1 <= numCourses <= 2000
0 <= prerequisites.length <= numCourses * (numCourses - 1)
prerequisites[i].length == 2
0 <= ai, bi < numCourses
ai != bi
所有[ai, bi] 互不相同

参考代码

DFS

class Solution {
    public Boolean flag = true;
    public List<List<Integer>> courseMap = new ArrayList<>();
    public int[] vic = null;
    public int count=0;
    public int order[]=null;
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        vic = new int[numCourses];
        order=new int[numCourses];
        for (int i = 0; i < numCourses; i++) {
            courseMap.add(new ArrayList<Integer>());
        }
        for (int[] p : prerequisites) {
            courseMap.get(p[0]).add(p[1]);
        }

        for (int i = 0; i < numCourses; i++) {
            if (vic[i] == 0 && flag == true) {
                dfs(i);
            }
        }
        if(count!=order.length) {
            return new int[0];
        }

        return order;
    }

    private void dfs(int course) {
        vic[course] = 1;
        // 前置课程列表
        List<Integer> courList = courseMap.get(course);

        for (int qzCourse : courList) {
            if (vic[qzCourse] == 1) {
                flag = false;
                return;
            }
            if (flag == false) {
                return;
            }
            if (vic[qzCourse] == 2) {
                continue;
            }
            dfs(qzCourse);
        }

        vic[course] = 2;
        order[count++]=course;
    }
}

 

BFS

class Solution {
    public List<List<Integer>> courseMap = new ArrayList<>();

    public int[] findOrder(int numCourses, int[][] prerequisites) {
        int[] order = new int[numCourses];
        int[] du = new int[numCourses];
        int count = 0;
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            courseMap.add(new ArrayList<Integer>());
        }
        for (int[] p : prerequisites) {
            courseMap.get(p[1]).add(p[0]);
            du[p[0]]++;
        }

        for (int i = 0; i < numCourses; i++) {
            if (du[i] == 0) {
                queue.offer(i);
            }
        }
        while (!queue.isEmpty()) {
            int course = queue.poll();
            order[count++] = course;
            // 获取该课程完成后其后置课程列表
            List<Integer> couresList = courseMap.get(course);
            for (Integer i : couresList) {
                du[i]--;
                if (du[i] == 0) {
                    queue.offer(i);
                }
            }
        }
        //所能学的课程数不等于要学的课程数,返回空数组
        if(count!=numCourses) {
            return new int[0];
        }

        return order;
    }

}

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/course-schedule-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

四,并查集

题目描述

以leecode题目冗余连接为例

树可以看成是一个连通且 无环 的 无向 图。

给定往一棵 n 个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1 到 n 中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edges ,edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。

思路分析

写并查集一般需要三方法,初始化方法,find方法,合并方法

参考代码

class Solution {
    public int parent[];

    public int find(int dot) {
        if (parent[dot] != dot) {
            parent[dot] = find(parent[dot]);
        }
        return parent[dot];
    }

    public void union(int dot1, int dot2) {
        int p1 = find(dot1);
        int p2 = find(dot2);
        if (p1 != p2) {
            parent[p1] = p2;
        }
    }

    public void init(int n) {
        parent = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
    }

    public int[] findRedundantConnection(int[][] edges) {
        //顶点数大于等于边数加一
       init(edges.length+1);
        for (int[] edge : edges) {
            int start = edge[0];
            int end = edge[1];
            if (find(start) != find(end)) {
                union(start, end);
                continue;
            }
            return edge;
        }

        return new int[0];

    }
}

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/redundant-connection 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

五,最小生成树

1,连接所有点的最小费用

题目描述

给你一个points 数组,表示 2D 平面上的一些点,其中 points[i] = [xi, yi] 。

连接点 [xi, yi] 和点 [xj, yj] 的费用为它们之间的 曼哈顿距离 :|xi - xj| + |yi - yj| ,其中 |val| 表示 val 的绝对值。

请你返回将所有点连接的最小总费用。只有任意两点之间 有且仅有 一条简单路径时,才认为所有点都已连接。

参考代码

prim
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;

public class Solution {

    public Queue<EDGE> priorityQueue = new PriorityQueue<EDGE>(new Comparator<EDGE>() {

        @Override
        public int compare(EDGE o1, EDGE o2) {
            // TODO Auto-generated method stub
            return o1.quanZhi - o2.quanZhi;
        }
    });

    public int getQuanZhi(int xi, int yi, int xj, int yj) {
//        |xi - xj| + |yi - yj|
        return Math.abs(xi - xj) + Math.abs(yi - yj);
    }

    public int vic[];
    // 邻接矩阵
    public EDGE edges[][];

    public int minCostConnectPoints(int[][] points) {
        vic = new int[points.length + 1];
        edges = new EDGE[points.length][points.length];
        // 生成所有可能的边
        for (int i = 0; i < points.length; i++) {
            for (int j = 0; j < points.length; j++) {
                edges[i][j] = new EDGE(i, j, getQuanZhi(points[i][0], points[i][1], points[j][0], points[j][1]));
            }
        }
        // 以节点0为起点开始搜索
        vic[0] = 1;
        int size = 1;
        int result = 0;
        // 将与节点0相连的节点加入优先级队列
        for (int i = 1; i < points.length; i++) {
            priorityQueue.offer(edges[0][i]);
        }
        while (!priorityQueue.isEmpty()) {
            EDGE edge = priorityQueue.poll();
            int end = edge.end;
            // 如果该节点以及访问过,则continue
            if (vic[end] == 1) {
                continue;
            }
            result += edge.quanZhi;
            // 节点个数加一
            size++;
            if (size == points.length) {
                break;
            }
            vic[end] = 1;
            // 以该节点来将其周围节点加入进去
            for (int i = 0; i < points.length; i++) {
                priorityQueue.offer(edges[end][i]);
            }
        }
        return result;
    }
}

class EDGE {
    public int start;
    public int end;
    public int quanZhi;

    public EDGE(int start, int end, int quanZhi) {
        this.start = start;
        this.end = end;
        this.quanZhi = quanZhi;
    }
}
Kruskal
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;

class Solution {
    // 创建优先级队列
    public Queue<EDGE> prioritYqueue = new PriorityQueue<EDGE>(new Comparator<EDGE>() {
        @Override
        public int compare(EDGE o1, EDGE o2) {
            // TODO Auto-generated method stub
            return o1.quanZhi - o2.quanZhi;
        }
    });
    // 每个节点的最根源父节点
    public int parent[];

    // 并查集的查找方法
    public int find(int dot) {
        if (parent[dot] != dot) {
            parent[dot] = find(parent[dot]);
        }
        return parent[dot];
    }

    // 并查集的合并方法
    public void union(int dot1, int dot2) {
        // 主要,合并判断的是p1和p2最上面的父节点,而不是他的直接父节点,所以不是
        // int p1 = parent(dot1);
        // int p2 = parent(dot2);
        // if (p1 != p2) {
        // parent[dot] = p2;
        // }
        int p1 = find(dot1);
        int p2 = find(dot2);
        if (p1 != p2) {
            parent[p1] = p2;
        }
    }

    // 并查集的初始化方法
    public void init(int n) {
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
    }

    // 返回两个节点的权值
    public int getQuanZhi(int xi, int yi, int xj, int yj) {
//        |xi - xj| + |yi - yj|
        return Math.abs(xi - xj) + Math.abs(yi - yj);
    }

    // main方法
    public static void main(String[] args) {
        int[][] points = { { -14, -14 }, { -18, 5 }, { 18, -10 }, { 18, 18 }, { 10, -2 } };
        Solution solution = new Solution();
        int result = solution.minCostConnectPoints(points);
        System.out.println(result);
    }

    // 求连通所有节点的最小权值
    public int minCostConnectPoints(int[][] points) {
        int result = 0;
        int size = 0;
        parent = new int[points.length];
        init(points.length);
        // 将所有可能的边加入优先级队列
        for (int i = 0; i < points.length; i++) {
            for (int j = i + 1; j < points.length; j++) {
                prioritYqueue.offer(new EDGE(i, j, getQuanZhi(points[i][0], points[i][1], points[j][0], points[j][1])));
            }
        }

        while (!prioritYqueue.isEmpty()) {
            EDGE edge = prioritYqueue.poll();
            if (find(edge.start) != find(edge.end)) {
                union(edge.start, edge.end);
                result += edge.quanZhi;
                // n个点,只需更新n-1条边(即使没有size也没有问题,因为当所有的点都连通之后,再有边就必定形成环,进不了上层if (find(edge.start)
                // != find(edge.end)))
                size++;
                if (size + 1 >= points.length) {
                    break;
                }
            }

        }

        return result;
    }

}

class EDGE {
    int start;
    int end;
    int quanZhi;

    public EDGE(int start, int end, int quanZhi) {
        this.start = start;
        this.end = end;
        this.quanZhi = quanZhi;
    }

}

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/min-cost-to-connect-all-points

2,城市建设(蓝桥杯)

题目描述

栋栋居住在一个繁华的C市中,然而,这个城市的道路大都年久失修。市长准备重新修一些路以方便市民,于是找到了栋栋,希望栋栋能帮助他。

C市中有n个比较重要的地点,市长希望这些地点重点被考虑。现在可以修一些道路来连接其中的一些地点,每条道路可以连接其中的两个地点。另外由于C市有一条河从中穿过,也可以在其中的一些地点建设码头,所有建了码头的地点可以通过河道连接。

栋栋拿到了允许建设的道路的信息,包括每条可以建设的道路的花费,以及哪些地点可以建设码头和建设码头的花费。

市长希望栋栋给出一个方案,使得任意两个地点能只通过新修的路或者河道互达,同时花费尽量小。

样例说明 建设第2、3、4条道路,在地点4、5建设码头,总的花费为9。

输入

输入的第一行包含两个整数n, m,分别表示C市中重要地点的个数和可以建设的道路条数。所有地点从1到n依次编号。 接下来m行,每行三个整数a, b, c,表示可以建设一条从地点a到地点b的道路,花费为c。若c为正,表示建设是花钱的,如果c为负,则表示建设了道路后还可以赚钱(比如建设收费道路)。 接下来一行,包含n个整数w_1, w_2, …, w_n。如果w_i为正数,则表示在地点i建设码头的花费,如果w_i为-1,则表示地点i无法建设码头。

输入保证至少存在一个方法使得任意两个地点能只通过新修的路或者河道互达。

 

数据规模和约定 对于100%的数据,1 < = n < = 10000,1 < = m < = 100000,-1000< =c< =1000,-1< =w_i< =1000,w_i≠0。

输出

输出一行,包含一个整数,表示使得所有地点通过新修道路或者码头连接的最小花费。如果满足条件的情况下还能赚钱,那么你应该输出一个负数。

样例输入

5 5
1 2 4
1 3 -1
2 3 3
2 4 5
4 5 10
-1 10 10 1 1

样例输出

9

思路分析

根据题意可以知道,这就是连接所有点,花费最小,但是需要注意的是,点与点之间可以通过道路连接,也可以通过码头连接,也就是假如

A修建一条码头,B也修建一条码头,那么A可以到B,他们的权值为修建A码头的费用加上修建B码头的费用,但问题的关键在于,如果要修建码头,那么那些地方修呢?对此,我们可以分解一下,引入中间点0,既A修建一条码头,相当于A到中间点0,权值修建A的费用,B修建码头,那么AB就通过中间点0连接起来了,假如还有点c,c也修建码头,那么ABC就通过中间点0连通了

还有可能存在这样的问题,只通过路,无法将全部的点连接起来,所以我们需要统计一下更新的点

还有可能存在这样的问题,只通过路比修码头费用要少

测试用例 5 10 5 2 8 4 5 8 2 5 10 1 4 4 5 3 5 1 2 9 1 5 8 5 1 2 1 3 5 4 3 12 -1 -1 -1 18 11 建立码头 5->1 2 1->4 4 5->3 5 5->2 8 5->0 11 30 不建码头 5->1 2 1->4 4 5->3 5 5->2 8 19

修码头比只修路费用要少

测试用例2 5 5 1 2 4 1 3 -1 2 3 3 2 4 5 4 5 10 -1 10 10 1 1 建立码头 1->3 -1 4->0 1 5->0 1 2->3 3 2->4 5 不建码头 1->3 -1 2->3 3 2->4 5 4->5 10

所以两种情况费用我们都要计算,最后取最小值

还有的路不仅不费钱,还赚钱,所以不管是否连通,我们都要修

 

可能会想到思路二,那就是有码头的和有码头的直接算成一条边,权值为他们的和,但是这样有一个问题

假如有三个点123中间一条河,123之间没有路,每个点修建码头的费用依次为f1,f2,f3,那么他们连通的费用为f1+f2+f3

如果直接枚举12连成一条边,13连成一条边,23连成一条边,那么连通他们的最小代价必定是这三条边中两条边的权值相加,结果必定大于f1+f2+f3,所以该思路不正确,所以这种问题还是需要引入中间点0

代码实现

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Scanner;

public class Main {
    public int[] parent = null;

    public static void main(String[] args) {
        Scanner sca = new Scanner(System.in);
        // 将所有可能的边加入优先级队列
        Queue<EDGE> queue = new PriorityQueue<>(new Comparator<EDGE>() {

            @Override
            public int compare(EDGE o1, EDGE o2) {
                // TODO Auto-generated method stub
                return o1.quanZhi - o2.quanZhi;
            }
        });

        int n = sca.nextInt();
        int m = sca.nextInt();
        for (int i = 0; i < m; i++) {
            queue.offer(new EDGE(sca.nextInt(), sca.nextInt(), sca.nextInt()));
        }
        // 由于能建码头的点没有终点,我们将0作为临时点,费用为建码头的费用
        for (int i = 1; i <= n; i++) {
            int t = sca.nextInt();
            if (t != -1) {
                queue.offer(new EDGE(i, 0, t));
            }
        }
        Main main = new Main();
        // 2.并查集三方法
        main.init(n);
        //创建一个新队列并将旧队列中的元素添加进新队列,旧队列用于不计算码头,新队列用于计算码头
        Queue<EDGE> queue2 = new PriorityQueue<>(queue);
        // 3.使用最小生成树算法
        // 不计算码头
        int result1 = main.Kruskal(queue, false, n);
        // 计算码头
        //再次使用,需要初始化parent,
        main.init(n);
        int result2 = main.Kruskal(queue2, true, n);
        System.out.println(Math.min(result1, result2));

    }

    private int Kruskal(Queue<EDGE> queue, boolean flag, int n) {
        // TODO Auto-generated method stub
        int result = 0;
        // 记录连通了的边
        int count=0;
        List<EDGE>list=new ArrayList<>();
        while (!queue.isEmpty()) {
            EDGE edge = queue.poll();
            if (edge.end == 0 && flag == false) {
                continue;
            }
            if (find(edge.start) != find(edge.end)) {
                union(edge.start, edge.end);
                result += edge.quanZhi;
                list.add(new EDGE(edge.start,edge.end,edge.quanZhi));
                count++;
                continue;
            }
            // 如果能赚钱,即使连通也要建
            if (edge.quanZhi < 0) {
                result += edge.quanZhi;
                list.add(new EDGE(edge.start,edge.end,edge.quanZhi));
            }

        }
        //如果不建码头,也就是无中间点,也就是只用更新n个点,n-1条边,如果更新低于这些,说明无法连通
        if(count!=n-1&&flag==false) {
            return Integer.MAX_VALUE;
        }
//        //如果建码头,也就是有中间点,也就是只用更新n+1个点,n条边,如果更新低于这些,说明无法连通
//        if(count!=n&&flag==true) {
//            return Integer.MAX_VALUE;
//        }
        return result;
    }

    private void union(int dot1, int dot2) {
        // TODO Auto-generated method stub
        int p1 = find(dot1);
        int p2 = find(dot2);
        if (p1 != p2) {
            parent[p1] = p2;
        }

    }

    private int find(int dot) {
        // TODO Auto-generated method stub
        int p = parent[dot];
        if (dot == p) {
            return p;
        }
        return parent[dot] = find(parent[dot]);
    }

    private void init(int n) {
        // TODO Auto-generated method stub
        // 临时点0到n(包括n)
        parent = new int[n + 1];
        // 点0初始点已经为0了
        for (int i = 1; i <= n; i++) {
            parent[i] = i;
        }

    }

}

class EDGE {
    public int start;
    public int end;
    public int quanZhi;

    public EDGE() {

    }

    public EDGE(int start, int end, int quanZhi) {
        this.start = start;
        this.end = end;
        this.quanZhi = quanZhi;
    }

}

六,二分图

1,染色法判定二分图

题目描述

给定一个 nn 个点 mm 条边的无向图,图中可能存在重边和自环。

请你判断这个图是否是二分图。

输入格式

第一行包含两个整数 nn 和 mm。

接下来 mm 行,每行包含两个整数 uu 和 vv,表示点 uu 和点 vv 之间存在一条边。

输出格式

如果给定图是二分图,则输出 Yes,否则输出 No

数据范围

1≤n,m≤10的五次方 1≤n,m≤10的五次方

输入样例:

4 4
1 3
1 4
2 3
2 4

输出样例:

Yes

参考代码

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.StringTokenizer;

public class Main {
    static BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
    static StringTokenizer strTonken = new StringTokenizer("");

    public static String nextLine() throws IOException {
        return bufferedReader.readLine();
    }

    public static String next() throws IOException {
        while (!strTonken.hasMoreTokens()) {
            strTonken = new StringTokenizer(bufferedReader.readLine());
        }
        return strTonken.nextToken();
    }

    public static int nextInt() throws NumberFormatException, IOException {
        return Integer.parseInt(next());
    }

    public static void main(String[] args) throws NumberFormatException, IOException {
        int n = nextInt();
        //1≤n,m≤10的五次方,二维矩阵内存会超
        Map<Integer, List<Integer>>map=new HashMap<>();
        int scs[] = new int[n + 1];
        int m = nextInt();
        for (int i = 0; i < m; i++) {
            int u = nextInt();
            int v = nextInt();
            // 无向边
            List<Integer>list1=map.getOrDefault(u, new ArrayList<>());
            list1.add(v);
            map.put(u, list1);
            
            List<Integer>list2=map.getOrDefault(v, new ArrayList<>());
            list2.add(u);
            map.put(v, list2);
        }
        // 枚举每个点
        for (int i = 1; i < n + 1; i++) {
            if (scs[i] != 0) {
                continue;
            }
            if (!check(i, scs, map)) {
                System.out.println("No");
                return;
            }
        }
        System.out.println("Yes");

    }

    // 染色法判定二分图
    private static boolean check(int dot, int[] scs, Map<Integer, List<Integer>>map) {
        // TODO Auto-generated method stub
        Queue<Integer> queue = new LinkedList<>();
        // 1红2绿
        scs[dot] = 1;
        queue.offer(dot);
        while (!queue.isEmpty()) {
            int dotTemp = queue.poll();
            int sc = scs[dotTemp] == 1 ? 2 : 1;
            List<Integer>list=map.get(dotTemp);
            if(list==null) {
                continue;
            }
            for(Integer i:list) {
                if (scs[i] == 0) {
                    scs[i] = sc;
                    queue.offer(i);
                    continue;
                }
                if (scs[i] != sc) {
                    return false;
                }
            }
        }
        return true;
    }

}

2,二分图的最大匹配

给定一个二分图,其中左半部包含 n1n1 个点(编号 1∼n11∼n1),右半部包含 n2n2 个点(编号 1∼n21∼n2),二分图共包含 mm 条边。

数据保证任意一条边的两个端点都不可能在同一部分中。

请你求出二分图的最大匹配数。

二分图的匹配:给定一个二分图 GG,在 GG 的一个子图 MM 中,MM 的边集 {E}{E} 中的任意两条边都不依附于同一个顶点,则称 MM 是一个匹配。

二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。

输入格式

第一行包含三个整数 n1n1、 n2n2 和 mm。

接下来 mm 行,每行包含两个整数 uu 和 vv,表示左半部点集中的点 uu 和右半部点集中的点 vv 之间存在一条边。

输出格式

输出一个整数,表示二分图的最大匹配数。

数据范围

1≤n1,n2≤500, 1≤u≤n1 1≤v≤n2 1≤m≤10的五次方

输入样例:

2 2 4
1 1
1 2
2 1
2 2

输出样例:

2

参考代码

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

public class Main {
    static BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
    static StringTokenizer strTonken = new StringTokenizer("");

    public static String nextLine() throws IOException {
        return bufferedReader.readLine();
    }

    public static String next() throws IOException {
        while (!strTonken.hasMoreTokens()) {
            strTonken = new StringTokenizer(bufferedReader.readLine());
        }
        return strTonken.nextToken();
    }

    public static int nextInt() throws NumberFormatException, IOException {
        return Integer.parseInt(next());
    }

    public static void main(String[] args) throws NumberFormatException, IOException {
        Map<Integer, List<Integer>> map = new HashMap<>();
        int n1 = nextInt();
        int n2 = nextInt();
        int m = nextInt();
        int vic[] = new int[n2 + 1];
        int pip[] = new int[n2 + 1];
        for (int i = 0; i < m; i++) {
            int u = nextInt();
            int v = nextInt();
            List<Integer> list = map.getOrDefault(u, new ArrayList<>());
            list.add(v);
            map.put(u, list);
        }
        int count = 0;
        // 遍历所有男生
        for (int i = 1; i < n1 + 1; i++) {
            // 将上一个男生考虑过的情况清空
            Arrays.fill(vic, 0);
            if (check(i, vic, map, pip)) {
                count++;
            }
        }
        System.out.println(count);

    }

    private static boolean check(int u, int[] vic, Map<Integer, List<Integer>> map, int[] pip) {
        // TODO Auto-generated method stub
        List<Integer> list = map.get(u);
        if (list == null) {
            return false;
        }
        for (Integer v : list) {
            // 如果这个男生已经考虑过这个女生
            if (vic[v] != 0) {
                continue;
            }
            // 标记为已经考虑过
            vic[v] = 1;
            // 如果该女生没有匹配或者心有所属但是那个男生有其它选择
            if (pip[v] == 0 || check(pip[v], vic, map, pip)) {
                pip[v] = u;
                return true;
            }
        }
        return false;
    }

}

 

posted @ 2022-04-13 14:59  lzstar-A2  阅读(36)  评论(0编辑  收藏  举报