1. 有权图的表示方式
- 有权图需要自定义以下Edge类
public class Edge {
private int a;
private int b;
// 权值,可用泛型来扩展
private int weight;
public Edge(int a, int b, int weight) {
this.a = a;
this.b = b;
this.weight = weight;
}
// 得到与v相连的另外一个顶点
public int getOther(int v) {
return a == v ? b : a;
}
// 所有属性的get方法……
}
- 有权图一般用邻接表的形式表示
// graph.get(i) = [{"a":i, "b":1, "weight":10},
// {"a":i, "b":2, "weight":20},
// {"a":i, "b":3, "weight":30}]
// 表示顶点i和顶点1,2,3相连,权值分别是10,20,30
List<List<Edge>> graph;
2. 有权图的相关操作
2.1 最小生成树
-
切分:把图中的节点分成两部分
-
横切边:如果一条边的两端顶点属于切分的不同部分,把这种边称为横切边。如下图的蓝色边
-
切分定理:给定任何切分,横切边中权值最小的边必定属于最小生成树
2.1.2 Prim算法
-
算法思路:以某顶点作为起始顶点,标记它(即把它加入到标记阵营中),把该顶点和其他顶点看成两部分(通过是否标记来区分两个阵营),从所有横切边中选出权值最小的边,并标记该边的相邻顶点,把它加入到标记阵营中。再从新的所有横切边中选出权值最小的边,把该边的相邻顶点加入到标记阵营中。不断重复上述步骤
-
时间复杂度:O(E㏒V)
-
代码逻辑如下图所示,具体代码需要使用索引堆数据结构
public class PrimMST {
public List<Edge> getMST(List<List<Edge>> graph) {
if (graph == null || graph.size() == 0) {
return new ArrayList<>();
}
int size = graph.size();
// 值为true的顶点理解成被标记了,与没有被标记的顶点看成两部分
boolean[] marked = new boolean[size];
// 索引堆
IndexHeap<Edge> indexHeap = new IndexHeap<>(size, (o1, o2) -> o2.getWeight() - o1.getWeight());
List<Edge> mst = new ArrayList<>();
visit(0, graph, marked, indexHeap);
while (!indexHeap.isEmpty()) {
int nextV = indexHeap.peekIndex();
mst.add(indexHeap.poll());
visit(nextV, graph, marked, indexHeap);
}
return mst;
}
private void visit(int v, List<List<Edge>> graph, boolean[] marked, IndexHeap<Edge> indexHeap) {
if (marked[v]) {
return;
}
marked[v] = true;
for (Edge adjEdge : graph.get(v)) {
int otherV = adjEdge.getOther(v);
if (marked[otherV]) {
continue;
}
if (indexHeap.indexOf(otherV) == null) {
indexHeap.offer(otherV, adjEdge);
} else if (adjEdge.getWeight() < indexHeap.indexOf(otherV).getWeight()) {
// 如果对应位置已有元素,并且新元素的权值更小,则替换
indexHeap.replace(otherV, adjEdge);
}
}
}
}
2.1.3 Kruskal算法
- 算法思路:把所有边按权值从小到大排序,从权值小的边开始遍历,若遍历到的边不构成环,则放入到结果集中,直到拿到 v - 1 条边为止
- 时间复杂度:O(E㏒E),效率比Prim算法低
- 代码实现如下,判断是否构成环需要使用并查集数据结构
public class KruskalMST {
public List<Edge> getMST(List<List<Edge>> graph) {
if (graph == null || graph.size() == 0) {
return new ArrayList<>();
}
// 收集所有边,按权值从小到大排序
List<Edge> allEdge = new ArrayList<>();
graph.forEach(item -> item.forEach(e -> {
// 无向图的时候,不收集重复的边
if (e.getA() < e.getB()) {
allEdge.add(e);
}
}));
allEdge.sort(Comparator.comparing(Edge::getWeight));
// 并查集
UnionFind unionFind = new UnionFind(graph.size());
List<Edge> mst = new ArrayList<>();
int a, b;
for (Edge edge : allEdge) {
a = edge.getA();
b = edge.getB();
// 如果新加入的边会构成环,则跳过
if (unionFind.isConnected(a, b)) {
continue;
}
unionFind.union(a, b);
mst.add(edge);
}
return mst;
}
}
2.2 最短路径
- 最短路径树:又称为单源最短路径树,是指从图中一个顶点出发,到其他任意顶点的最短路径构成树的集合
- 下面介绍的算法都是得到最短路径树
2.2.1 Dijkstra算法
-
该算法的前提:图中不能有负权值的边
-
时间复杂度:O(E㏒V)
-
该算法主要是得出从某个起始顶点到达其他顶点的最短路径的权值,有向图与无向图的逻辑通用
-
代码逻辑如下图所示,实际得到的是一棵最短路径树,具体代码需要使用索引堆数据结构
public class Dijkstra {
public List<String> getShortestPath(List<List<Edge>> graph, int source) {
if (graph == null || graph.size() == 0) {
return new ArrayList<>();
}
int size = graph.size();
// 若marked[i] = true,则说明找到了从顶点source到顶点i的最短路径
boolean[] marked = new boolean[size];
// path[i]表示从顶点source到顶点i的最短路径的权值
int[] path = new int[size];
// from[i] = j,表示在最短路径树中,i的上一个节点是j
int[] from = new int[size];
// source是最短路径树的根节点
from[source] = -1;
// 索引堆
// 索引:顶点编号
// 具体元素:从source到该顶点的最短路径的权值
IndexHeap<Integer> indexHeap = new IndexHeap<>(size, (w1, w2) -> w2 - w1);
indexHeap.offer(source, 0);
while (!indexHeap.isEmpty()) {
// 当前权值最小的顶点,每次出堆都说明找到了从source到curV的最短路径
int curV = indexHeap.peekIndex();
Integer curWeight = indexHeap.poll();
marked[curV] = true;
path[curV] = curWeight;
for (Edge adjE : graph.get(curV)) {
int nextV = adjE.getOther(curV);
if (marked[nextV]) {
continue;
}
// 累加后新的权值
int newWeight = curWeight + adjE.getWeight();
if (indexHeap.indexOf(nextV) == null) {
indexHeap.offer(nextV, newWeight);
} else if (newWeight < indexHeap.indexOf(nextV)) {
indexHeap.replace(nextV, newWeight);
}
from[nextV] = curV;
}
}
// 汇总最短路径树
return translateShortestPathTree(source, marked, path, from);
}
private List<String> translateShortestPathTree(int source, boolean[] marked, int[] path, int[] from) {
List<String> ret = new ArrayList<>();
StringBuilder sb = new StringBuilder();
int temp;
for (int v = 0; v < marked.length; v++) {
if (v == source) {
continue;
}
if (!marked[v]) {
ret.add("不存在从" + source + "到" + v + "的路径");
continue;
}
temp = v;
while (temp != -1) {
sb.insert(0, temp);
temp = from[temp];
if (temp != -1) {
sb.insert(0, " -> ");
}
}
ret.add(source + "到" + v + "的最短路径:" + sb.toString() + " 总权值为" + path[v]);
sb.setLength(0);
}
return ret;
}
}
2.2.2 SPFA算法
-
该算法是Bellman-Ford算法的优化,支持负权边,但是图里不能有负权环:假设图有负权环,那么每绕一圈,路径总权值就会更小,这样永远也找不到最短路径
-
该算法一般用于有向图中,需要用上队列数据结构。在无向图中,假设存在负权边,由于无向图的特性就相当于拥有负权环了
-
时间复杂度:O(kE),k是每个顶点的平均入队次数,一般在区间[2, 3]中
-
算法逻辑如下,可参考这篇文章
public class SPFA {
public List<String> getShortestPath(List<List<Edge>> graph, int source) {
if (graph == null || graph.size() == 0) {
return new ArrayList<>();
}
int size = graph.size();
// path[i]表示从顶点source到顶点i的最短路径的权值
int[] path = new int[size];
Arrays.fill(path, Integer.MAX_VALUE);
path[source] = 0;
// from[i] = j,表示在最短路径树中,i的上一个节点是j
int[] from = new int[size];
Arrays.fill(from, -1);
// 顶点i是否在队列中
boolean[] inQueue = new boolean[size];
inQueue[source] = true;
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(source);
while (!queue.isEmpty()) {
Integer curV = queue.poll();
inQueue[curV] = false;
for (Edge adjE : graph.get(curV)) {
int nextV = adjE.getOther(curV);
int newWeight = path[curV] + adjE.getWeight();
if (newWeight < path[nextV]) {
path[nextV] = newWeight;
from[nextV] = curV;
if (!inQueue[nextV]) {
queue.offer(nextV);
inQueue[nextV] = true;
}
}
}
}
// 汇总最短路径树
return translateShortestPathTree(source, path, from);
}
private List<String> translateShortestPathTree(int source, int[] path, int[] from) {
List<String> ret = new ArrayList<>();
StringBuilder sb = new StringBuilder();
int temp;
for (int v = 0; v < path.length; v++) {
if (v == source) {
continue;
}
if (from[v] == -1) {
ret.add("不存在从" + source + "到" + v + "的路径");
continue;
}
temp = v;
while (temp != -1) {
sb.insert(0, temp);
temp = from[temp];
if (temp != -1) {
sb.insert(0, " -> ");
}
}
ret.add(source + "到" + v + "的最短路径:" + sb.toString() + " 总权值为" + path[v]);
sb.setLength(0);
}
return ret;
}
}