前言:图论的主要问题,如最小生成树、最短路径、拓扑排序、关键路径、查并集
提示:
图论
查并集
注:用来判断环,为了后续的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(弗洛伊德)