有向图的可达性(遍历)
与无向图不同,有向图的遍历(或者叫排列)基于DFS,分为以下三种:
前序遍历:根最先
后序遍历:根最后
逆后序遍历(拓扑排序)
- 拓扑排序是将DAG中所有顶点排成一个线性序列,使得图中任意一对顶点 u 和 v,若边
<u,v>∈E(G)
,则 u 在线性序列中出现在 v 之前; - 定理1:当且仅当一幅有向图是无环有向图(DAG)时它才能进行拓扑排序(即拓扑有序);
- 定理2:一幅有向无环图的拓扑顺序即为所有顶点的逆后续排列;
- 注意:在拓扑排序之前必须检查图中有没有环,如果有则返回null
最短路径树(SPT)
单源最短路径算法(既不能处理负权边,更不能处理负权环):Dijkstra
-
Dij算法的思想其实跟prim算法类似,两种贪心算法都会用添加边的方式构造一棵树:Prim算法每次添加的是离树最近的的非树顶点,Dijkstra算法每次添加的都是离起点最近的非树顶点。同样对应于Prim算法的延时实现和即时实现,Dijkstra算法也有延时(优先队列)和即时(索引优先队列)两种实现,他们代码中的唯一区别就是Dijkstra多了一个松弛的操作。
-
Dij算法的基础在于
松弛
,根据距离起点的远近依次检查路径,即假设添加一条边总会使路径变得更长,而负权边的存在破坏了这个基础,所以Dij算法不能处理负权边。 -
Dij的算法复杂度:在一幅含有V个顶点,E条边的加权有向图中,使用Dij算法所需空间与
V
成正比,所需时间与ElogV
成正比。
单源最短路径算法(即可以处理负权边,也可以处理负权环):Bellman-Ford
-
如果
s -> v
的有向路径上的任意一点m处于负权环上,则讨论s -> v
的最短路径没有意义,因为m -> m
可以构造任意小的路径;只有加权有向图中至少存在一条s -> v
的有向路径并且所有s -> v
的有向路径上的任意顶点都不存在于任何负权环中,s -> v
的最短路径才存在。 -
规定如下:
- 对于起点不可达的顶点,最短路径为
$ + \infty$
- 对于起点可达但路径上的某点属于一个负权环的顶点,最短路径为
$ - \infty$
- 对于其他顶点,计算最短路径的权重以及最短路径树
- 对于起点不可达的顶点,最短路径为
-
换言之,只有存在最短路径BF算法才会得出解,即给定源点 s,s无法到达任何负权环才能有最短路径解,否则检测到可达的负权环则程序终止,避免陷入死循环。
-
对于一个含有 V 个顶点 E 条边的加权有向图以任意顺序放松有向图的所有边,重复 V 轮;在最坏情况下基于队列的Bellman-Ford算法解决最短路径问题(或者找到从源点可达的负权环),所需的时间与
EV
成正比,空间和V
成正比。
完全最短路径算法(可以处理负权边,但不能处理负权环):Flyod
一种基于动态规划的多源最短路算法,其核心代码就是三层循环。算法复杂度较高,对于一个含有N个点的图,时间复杂度与$N^3$
成正比。
补充:加权有向无环图的最优SPT算法:拓扑排序+松弛操作
- 相比Dij算法,AcyclicSP算法只需要按照拓扑顺序依次松弛每个点,每个点只会被松弛一次,因为当 from 被放松后,一定成立
distTo[from] + edge.getWeight() >= distTo[to]
,在算法结束前都成立;因为 from 松弛后, distTo[from] 就不会变了,而 distTo[to] 只会变小,所以当按照拓扑顺序松弛完所有的点后,最短路径就生成了。 - 这个算法的复杂度是线性的
E+V
。
实现
/*
加权有向图的实现比加权无向图更加简单一些,区别就是每个点只存储以该点为起点的边
*/
import java.util.*;
public class DirectedGraph {
// 这里为了简单,用0开始连续的数字代表顶点(顶点ID),实际的实现中应该设计成类 Node 的形式
private final int V; // 顶点数目
private int E; // 边的数目
private boolean[] marked; // 用于遍历时标记用
private HashMap<Integer, TreeSet<DirectedEdge>> adj; // 邻接表用Hash表实现,key=Node_ID, value=相邻的边组成的链表(这里的实现是红黑二叉树)
public DirectedGraph(int v) {
this.V = v;
this.E = 0;
this.adj = new HashMap<>();
// 初始化哈希表
for (int i = 0; i < V; i++) {
adj.put(i,new TreeSet<>());
}
}
public int getV() {
return V;
}
public int getE() {
return E;
}
// 通过起点和终点返回边
public DirectedEdge getEdge(int from, int to) {
for (DirectedEdge e : adj.get(from))
if (e.to == to)
return e;
return null;
}
// 只有这里与无向图不同
public void addEdge(DirectedEdge e) {
adj.get(e.from).add(e); // 只添加作为起点的边
E++;
}
// 返回 v 点所有的连接边,Iterable<Edge>方便遍历
Iterable<DirectedEdge> adj(int v) {
return adj.get(v);
}
// 返回加权无向图中所有的边
public Iterable<DirectedEdge> edges() {
TreeSet<DirectedEdge> edges = new TreeSet<>();
for (Map.Entry<Integer,TreeSet<DirectedEdge>> entry : adj.entrySet())
edges.addAll(entry.getValue());
return edges;
}
/*
有向图的可达性:与无向图不同,有向图的遍历(或者叫排列),基于无向图的DFS,分为以下三种
*/
// DFS
// 1. preOrder 前序遍历(DFS):递归调用之前将顶点加入队列(先入先出,为了正序输出)
LinkedList<Integer> preQueue;
// 2. postOrder 后序遍历:递归调用之后将顶点加入队列(先入先出,为了正序输出)
LinkedList<Integer> postQueue;
// 3. reversePostOrder(topologicalOrder) 逆后序遍历:递归调用之后将顶点压入栈(先入后出,为了逆序输出)
LinkedList<Integer> reversePostStack;
/*
4. TopologicalOrder 拓扑排序:是将DAG中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前
定理1:当且仅当一幅有向图是无环有向图(DAG)时它才能进行拓扑排序(即拓扑有序)
定理2:一幅有向无环图的拓扑顺序即为所有顶点的逆后续排列
所以在拓扑排序之前必须检查图中有没有环,如果有则返回null
*/
public void depthFirstSearch() {
marked = new boolean[V]; // 每次遍历之前懒惰初始化,每次都保证初始全是false
preQueue = new LinkedList<>();
postQueue = new LinkedList<>();
reversePostStack = new LinkedList<>(); // 无环时是拓扑排序,当有环时返回null
dfs(0);
// 判断有没有环,有环返回null,但实际中应该先判断有没有环,这里只是为了方便一起写
EdgeWeightedDirectedCycle cycle = new EdgeWeightedDirectedCycle(this); // 注意后面的判断环的dfs方法里加了负权重环的判断,在这里应该删除
if (cycle.hasCycle())
reversePostStack = null;
}
private void dfs(int root) {
preQueue.offer(root);
marked[root] = true;
for (DirectedEdge edge : this.adj(root)) {
if (!marked[edge.to])
dfs(edge.to);
}
postQueue.offer(root);
reversePostStack.push(root);
}
@Override
public String toString() {
return "UndirectedGraph{" +
"V=" + V +
", E=" + E +
", adj=" + adj +
'}';
}
}
class DirectedEdge implements Comparable<DirectedEdge> {
int from;
int to;
double weight;
public DirectedEdge(int from, int to, double weight) {
this.from = from;
this.to = to;
this.weight = weight;
}
public int getFrom() {
return from;
}
public int getTo() {
return to;
}
public double getWeight() {
return weight;
}
// 这里定义大小关系,平行边定义为相等,因为前面用的set存邻接边,即不允许添加平行边
@Override
public int compareTo(DirectedEdge that) {
if (this.from == that.from && this.to == that.to)
return 0;
else if (this.weight < that.weight)
return -1;
else if (this.weight > that.weight)
return 1;
else
return 1;
}
@Override
public String toString() {
return "DirectedEdge{" +
"from=" + from +
", to=" + to +
", weight=" + weight +
'}';
}
}
/**
* 有向加权图最短路径树(SPT):
* 1. 单源最短路径:Dijkstra
* 2. 单源最短路径:Bellman-Ford
* 3. 完全最短路径:Flyod
* 4. 补充:加权有向无环图的最优SPT算法:拓扑排序+松弛操作
*/
/**
* Dij算法的思想其实跟prim算法类似,两种算法都会用添加边的方式构造一棵树:Prim算法每次添加的是离树最近的的非树顶点,
* Dijkstra算法每次添加的都是离起点最近的非树顶点。同样对应于Prim算法的延时实现和即时实现,Dijkstra算法也有延时(优先队列)
* 和即时(索引优先队列)两种实现,他们代码中的唯一区别就是Dijkstra多了一个松弛的操作。
*/
class Dijkstra {
private DirectedEdge[] edgeTo; // 到此点路径(指向此点的边),为了记录路径
private double[] distTo; // 到此点的路径权重
private IndexMinPriorityQueue indexMinPq; // 索引优先队列,将顶点ID和到此点的路径权重关联(即时实现)
public Dijkstra(DirectedGraph graph, int s) {
edgeTo = new DirectedEdge[graph.getV()];
distTo = new double[graph.getV()];
indexMinPq = new IndexMinPriorityQueue<>(graph.getV());
for (int v = 0; v < graph.getV(); v++)
distTo[v] = Double.MAX_VALUE; // 先将最短距离置为最大值
distTo[s] = 0.0; // 设置源点距离为0
indexMinPq.insert(s ,0.0); // 用顶点s和权重0初始化indexPq
while (!indexMinPq.isEmpty())
relax(graph, indexMinPq.delMin());
}
// 工具函数:松弛操作,并更新索引优先队列
private void relax(DirectedGraph graph, int v) {
for (DirectedEdge edge : graph.adj(v)) { // 遍历所有邻接边
int w = edge.to;
if (distTo[edge.from] + edge.getWeight() < distTo[w]) { // 这里多加了edge.getWeight()进行松弛
edgeTo[w] = edge;
distTo[w] = distTo[edge.from] + edge.getWeight();
if (indexMinPq.contains(w)) // 找到非树上的点离源点更近的点,更新或添加
indexMinPq.change(w,distTo[w]); // 更新
else
indexMinPq.insert(w,distTo[w]); // 添加
}
}
}
// 查询最短路径长度
public double distTo(int v) {
return distTo[v];
}
// 查询是否存在最短路径
public boolean hasPathTo(int v) {
return distTo[v] < Double.MAX_VALUE;
}
// 查询最短路径
public Iterable<DirectedEdge> pathTo(int v) {
if (!hasPathTo(v)) return null;
LinkedList<DirectedEdge> pathStack = new LinkedList<>();
for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from])
pathStack.push(e);
return pathStack;
}
}
/**
* AcyclicSP算法(仅适用于加权DAG);
* 相比Dijkstra算法,只需要按照拓扑顺序依次松弛每个点,每个点只会被松弛一次,因为当from被放松后,一定成立 distTo[from] + edge.getWeight() >= distTo[to];
* 在算法结束前都成立,因为from松弛后,distTo[from]就不会变了,而distTo[to]只会变小,所以当按照拓扑顺序松弛完所有的点后,最短路径就生成了。
*/
class AcyclicSP {
private DirectedEdge[] edgeTo; // 到此点路径(指向此点的边)
private double[] distTo; // 到此点的路径权重
/*
这里是寻找最短路径的实现,寻找最长路径只需要把distTo[]初始化为Double.MIN_VALUE,并改变relax()中不等式的方向即可
*/
public AcyclicSP(DirectedGraph graph, int s) {
edgeTo = new DirectedEdge[graph.getV()];
distTo = new double[graph.getV()];
for (int v = 0; v < graph.getV(); v++)
distTo[v] = Double.MAX_VALUE; // 先将最短距离置为最大值
distTo[s] = 0.0; // 设置源点距离为0
graph.depthFirstSearch();
LinkedList<Integer> topological = graph.reversePostStack; // 获取拓扑排列
for (int v : topological) // 按照拓扑顺序遍历所有的点,每个点只遍历一次
relax(graph,v);
}
// 工具函数:松弛操作
private void relax(DirectedGraph graph, int v) {
for (DirectedEdge edge : graph.adj(v)) { // 遍历所有邻接边
int w = edge.to;
if (distTo[edge.from] + edge.getWeight() < distTo[w]) { // 找到非树上的点离源点更近的点,更新或添加
edgeTo[w] = edge;
distTo[w] = distTo[edge.from] + edge.getWeight();
}
}
}
// 查询最短路径长度
public double distTo(int v) {
return distTo[v];
}
// 查询是否存在最短路径
public boolean hasPathTo(int v) {
return distTo[v] < Double.MAX_VALUE;
}
// 查询最短路径
public Iterable<DirectedEdge> pathTo(int v) {
if (!hasPathTo(v)) return null;
LinkedList<DirectedEdge> pathStack = new LinkedList<>();
for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from])
pathStack.push(e);
return pathStack;
}
}
/**
* BF算法
*/
class BellmanFordSP {
private DirectedEdge[] edgeTo; // 到此点路径(指向此点的边)
private double[] distTo; // 到此点的路径权重
private LinkedList<Integer> queue; // 正在被放松的顶点队列
private boolean[] onQueue; // 该顶点是否在队列中
private int cost; // relax()被调用的次数
private Iterable<DirectedEdge> cycle; // edgeTo[]中的边是否形成负权重环
public BellmanFordSP(DirectedGraph graph, int s) {
edgeTo = new DirectedEdge[graph.getV()];
distTo = new double[graph.getV()];
onQueue = new boolean[graph.getV()];
for (int v = 0; v < graph.getV(); v++)
distTo[v] = Double.POSITIVE_INFINITY; // 正无穷
distTo[s] = 0.0;
// Bellman-Ford algorithm:按任意顺序松弛有向图的所有边
queue = new LinkedList<>();
queue.offer(s);
onQueue[s] = true;
while (!queue.isEmpty() && !hasNegativeCycle()) { // 队列为空代表成功找到了最短路径,如果在V轮后队列非空则一定存在负权环,循环终止
int v = queue.poll();
onQueue[v] = false;
relax(graph, v);
}
}
// 松弛操作
private void relax(DirectedGraph graph, int v) {
for (DirectedEdge e : graph.adj(v)) {
int w = e.to;
if (distTo[w] > distTo[v] + e.getWeight()) {
distTo[w] = distTo[v] + e.getWeight();
edgeTo[w] = e;
if (!onQueue[w]) { // 保证队列中不含重复的点,并且只将成功放松的边指向的所有顶点加入队列
queue.offer(w);
onQueue[w] = true;
}
}
if (++cost % graph.getV() == 0) { // 周期性的检查edgeTo[]表示的子图中是否存在负权重环
findNegativeCycle();
if (hasNegativeCycle()) return; // 如果存在环直接返回
}
}
}
// 查询最短路径长度
public double distTo(int v) {
return distTo[v];
}
// 查询是否存在最短路径
public boolean hasPathTo(int v) {
return distTo[v] < Double.MAX_VALUE;
}
// 查询最短路径
public Iterable<DirectedEdge> pathTo(int v) {
if (!hasPathTo(v)) return null;
LinkedList<DirectedEdge> pathStack = new LinkedList<>();
for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from])
pathStack.push(e);
return pathStack;
}
public boolean hasNegativeCycle() {
return cycle != null;
}
public Iterable<DirectedEdge> negativeCycle() {
return cycle;
}
// 用edgeTo[]中的边表示一个有向加权图,检查edgeTo[]表示的子图中是否存在负权重环
private void findNegativeCycle() {
int V = edgeTo.length;
DirectedGraph spt = new DirectedGraph(V);
for (int v = 0; v < V; v++) // 构造一个图
if (edgeTo[v] != null)
spt.addEdge(edgeTo[v]);
EdgeWeightedDirectedCycle finder = new EdgeWeightedDirectedCycle(spt);
cycle = finder.cycle();
}
}
/**
* 暴力 Dijkstra 算法也可以求任意两点间的最短路径
*/
class ViolentDijkstra {
private DirectedEdge[][] edgeTo; // edgeTo[w][v]: w指向v经过的最后一条边,为了记录路径
private double[][] distTo; // distTo[w][v]: w到v点的路径权重
public ViolentDijkstra(DirectedGraph graph) {
edgeTo = new DirectedEdge[graph.getV()][graph.getV()];
distTo = new double[graph.getV()][graph.getV()];
for (int w = 0; w < graph.getV(); w++)
for (int v = 0; v < graph.getV(); v++) {
if (w == v)
distTo[w][v] = 0; // 自己指向自己设置为0,为了下面relax()中初始化
else
distTo[w][v] = Double.MAX_VALUE;
}
// Violent Dijkstra algorithm
for (int w = 0; w < graph.getV(); w++)
for (int v = 0; v < graph.getV(); v++) {
relax(graph,w,v);
}
}
// 工具函数:松弛操作
private void relax(DirectedGraph graph, int w, int v) {
for (DirectedEdge edge : graph.adj(v)) { // 遍历所有邻接边
int to = edge.to;
if (distTo[w][v] + edge.getWeight() < distTo[w][to]) {
edgeTo[w][to] = edge;
distTo[w][to] = distTo[w][v] + edge.getWeight();
}
}
}
// 查询最短路径长度
public double distTo(int w, int v) {
return distTo[w][v];
}
// 查询是否存在最短路径
public boolean hasPathTo(int w,int v) {
return distTo[w][v] < Double.MAX_VALUE;
}
// 查询最短路径
public Iterable<DirectedEdge> pathTo(int w, int v) {
if (!hasPathTo(w, v)) return null;
LinkedList<DirectedEdge> pathStack = new LinkedList<>();
for (DirectedEdge e = edgeTo[w][v]; e != null; e = edgeTo[w][e.from])
pathStack.push(e);
return pathStack;
}
}
/**
* Flyod算法: 动态规划的思想,由于不是基于松弛操作的,所以可以处理负权边
*/
class Flyod {
private DirectedEdge[][] edgeTo; // edgeTo[w][v]: w指向v经过的最后一条边,为了记录路径
private double[][] distTo; // distTo[w][v]: w到v点的路径权重
public Flyod(DirectedGraph graph) {
edgeTo = new DirectedEdge[graph.getV()][graph.getV()];
distTo = new double[graph.getV()][graph.getV()];
// 初始化数组
for (int i = 0; i < graph.getV(); i++)
for (int j = 0; j < graph.getV(); j++) {
if (i == j)
distTo[i][j] = 0;
else
distTo[i][j] = Double.MAX_VALUE;
}
for (DirectedEdge e : graph.edges()) {
distTo[e.from][e.to] = e.weight;
distTo[e.to][e.from] = e.weight;
edgeTo[e.from][e.to] = e;
}
// Flyod algorithm
for (int k = 0; k < graph.getV(); k++)
for (int w = 0; w < graph.getV(); w++)
for (int v = 0; v < graph.getV(); v++)
if (distTo[w][v] > distTo[w][k] + distTo[k][v]) {
distTo[w][v] = distTo[w][k] + distTo[k][v];
edgeTo[w][v] = graph.getEdge(k,v); // 记录边
}
}
// 查询最短路径长度
public double distTo(int w, int v) {
return distTo[w][v];
}
// 查询是否存在最短路径
public boolean hasPathTo(int w,int v) {
return distTo[w][v] < Double.MAX_VALUE;
}
// 查询最短路径
public Iterable<DirectedEdge> pathTo(int w, int v) {
if (!hasPathTo(w, v)) return null;
LinkedList<DirectedEdge> pathStack = new LinkedList<>();
for (DirectedEdge e = edgeTo[w][v]; e != null; e = edgeTo[w][e.from])
pathStack.push(e);
return pathStack;
}
}
// 测试类
class TestDirectedGraph {
// test UndirectedGraph
public static void main(String[] args) {
DirectedGraph directedGraph = new DirectedGraph(8); // 添加6个点,ID:0-5
directedGraph.addEdge(new DirectedEdge(0,1,10));
directedGraph.addEdge(new DirectedEdge(0,2,30));
directedGraph.addEdge(new DirectedEdge(0,5,100));
directedGraph.addEdge(new DirectedEdge(1,2,50));
directedGraph.addEdge(new DirectedEdge(1,3,20));
directedGraph.addEdge(new DirectedEdge(2,4,10));
directedGraph.addEdge(new DirectedEdge(3,4,60));
directedGraph.addEdge(new DirectedEdge(4,5,10));
// System.out.println(directedGraph);
// test Dfs(只添加了6个点)
directedGraph.depthFirstSearch();
System.out.println(directedGraph.preQueue);
System.out.println(directedGraph.postQueue);
System.out.println(directedGraph.reversePostStack);
// test Dijkstra
Dijkstra dijkstra = new Dijkstra(directedGraph,0);
System.out.println(dijkstra.pathTo(5));
// test AcyclicSP
AcyclicSP acyclicSP = new AcyclicSP(directedGraph,0);
System.out.println(acyclicSP.pathTo(5));
// test Violent Dijkstra
ViolentDijkstra violentDijkstra = new ViolentDijkstra(directedGraph);
System.out.println(violentDijkstra.pathTo(0,5));
// test Flyod
Flyod flyod = new Flyod(directedGraph);
System.out.println(flyod.pathTo(0,5));
// test BF
directedGraph.addEdge(new DirectedEdge(2,6,10));
directedGraph.addEdge(new DirectedEdge(6,7,-60));
directedGraph.addEdge(new DirectedEdge(7,6,10));
BellmanFordSP bellmanFordSP = new BellmanFordSP(directedGraph,0);
System.out.println(bellmanFordSP.pathTo(5));
}
}
/* Output:
[0, 1, 3, 4, 5, 2]
[5, 4, 3, 2, 1, 0]
[0, 1, 2, 3, 4, 5]
[DirectedEdge{from=0, to=2, weight=30.0}, DirectedEdge{from=2, to=4, weight=10.0}, DirectedEdge{from=4, to=5, weight=10.0}]
[DirectedEdge{from=0, to=2, weight=30.0}, DirectedEdge{from=2, to=4, weight=10.0}, DirectedEdge{from=4, to=5, weight=10.0}]
[DirectedEdge{from=0, to=2, weight=30.0}, DirectedEdge{from=2, to=4, weight=10.0}, DirectedEdge{from=4, to=5, weight=10.0}]
[DirectedEdge{from=0, to=2, weight=30.0}, DirectedEdge{from=2, to=4, weight=10.0}, DirectedEdge{from=4, to=5, weight=10.0}]
[DirectedEdge{from=0, to=2, weight=30.0}, DirectedEdge{from=2, to=4, weight=10.0}, DirectedEdge{from=4, to=5, weight=10.0}]
*/
// 工具类
/**
* 索引优先队列
* @param <T>
*/
class IndexMinPriorityQueue<T extends Comparable<T>> {
private T[] elements;
private int[] indexPq;
private int[] reIndexQp;
private int N = 0;
public IndexMinPriorityQueue(int maxN) {
elements = (T[]) new Comparable[maxN + 1]; // elements这里可以不加1,没影响,但可能有其他用途
indexPq = new int[maxN + 1];
reIndexQp = new int[maxN +1];
for (int i = 0; i <= maxN; i++)
reIndexQp[i] = -1;
}
public boolean isEmpty() {
return N == 0;
}
public int size() {
return N;
}
public boolean contains(int k) {
return reIndexQp[k] != -1;
}
// 插入:在k位置插入元素,位置k并不代表任何含义,只是存储在elements数组的索引位置;注意:这里k可以从索引0开始,没有任何影响
public void insert(int k, T value) {
N++;
elements[k] = value; // 放入索引k
indexPq[N] = k; // 记录此元素所在索引位置(k)
reIndexQp[k] = N; // 记录indexPq数组中哪个位置(N)存储着此元素的索引
swim(N); // 从堆底加入并上浮,维护indexPq 和 reIndexQp
}
public T min() {
return elements[indexPq[1]];
}
public int minIndex() {
return indexPq[1];
}
// 删除最小值,并返回其索引
public int delMin() {
int indexOfMax = indexPq[1];
if (elements[indexOfMax] == null) // 已为空,返回-1
return -1;
exch(1,N--); // 把最后一个元素(最小元素)放在顶端,然后N--(堆的大小-1)
sink(1); // 让“最后”一个元素下沉
elements[indexPq[N+1]] = null; // 将垃圾(删除的最小值)清空
reIndexQp[indexPq[N+1]] = -1; // 更新对应reIndexQp为-1
indexPq[N+1] = 0; // 更新最后一位删除的indexPq为0
return indexOfMax;
}
// 删除索引k位置的元素,与删除最小值类似
public void delete(int k) {
int indexOfPq = reIndexQp[k];
exch(indexOfPq,N--);
swim(indexOfPq);
sink(indexOfPq);
elements[k] = null;
reIndexQp[k] = -1;
indexPq[N+1] = 0;
}
// 更新值
public void change(int k, T newValue) {
elements[k] = newValue;
// 更新值后,可能出现三种情况:
// 1. 比父节点小:需要上浮
// 2. 比子节点大:需要下沉
// 3. 大小在父节点和子节点之间:不执行任何操作
// 所以此处采取的策略是先上浮在下沉(或先下沉再上浮)
swim(reIndexQp[k]); // 上浮
sink(reIndexQp[k]); // 下沉
}
// 用于堆实现的比较方法:这里怎么设计关乎着是大堆顶(<0)还是小堆顶(>0)
private boolean greater(int i, int j) {
return elements[indexPq[i]].compareTo(elements[indexPq[j]]) > 0;
}
// 用于堆实现的交换方法:交换indexPq[i]、indexPq[j] 和 reIndexPq[i]、reIndexPq[j]
private void exch(int i, int j) {
int tempPq = indexPq[i];
indexPq[i] = indexPq[j];
indexPq[j] = tempPq;
reIndexQp[indexPq[i]] = i;
reIndexQp[indexPq[j]] = j;
}
// 上浮
private void swim(int k) {
while(k > 1 && greater(k/2,k)) {
exch(k/2,k); // k/2默认向下取整
k = k/2;
}
}
// 下沉
private void sink(int k) {
while (2*k <= N) {
int j = 2*k;
if (j < N && greater(j,j+1)) // 找到较小的子节点,并将j指向它
j++;
if (!greater(k,j)) // 此时j一定指向较小的子节点,如果elements[indexPq[k]] <= elements[indexPq[j]],则下沉结束
break;
exch(k,j); // 如果没有break则说明elements[indexPq[k]] > elements[indexPq[j]],交换indexPq 和 reIndexQp
k = j; // 交换k、j,让k始终指向下沉的元素
}
}
@Override
public String toString() {
return " indexPq " +
Arrays.toString(indexPq) + "\n" +
" reIndexQp " +
Arrays.toString(reIndexQp) + "\n" +
"PriorityQueue " +
Arrays.toString(elements);
}
}
/**
* 负权重环检测(环检测只需要把dfs里的weight删除)
*/
class EdgeWeightedDirectedCycle {
private boolean[] marked; // 是否已经被访问
private DirectedEdge[] edgeTo; // 到达edgeTo[v]代表达到v的边
private boolean[] onStack; // 递归调用栈上的所有顶点
private Stack<DirectedEdge> cycle; // 有向环上的所有顶点(如果存在)
/*
判断edgeTo[]表示的加权有向图graph是否有环,如果是有,返回环。
*/
public EdgeWeightedDirectedCycle(DirectedGraph graph) {
marked = new boolean[graph.getV()];
onStack = new boolean[graph.getV()];
edgeTo = new DirectedEdge[graph.getV()];
for (int v = 0; v < graph.getV(); v++)
if (!marked[v]) dfs(graph, v);
}
// dfs查找环
private void dfs(DirectedGraph graph, int v) {
onStack[v] = true; // 标记已经在栈上了
marked[v] = true;
for (DirectedEdge e : graph.adj(v)) {
int w = e.to;
if (cycle != null) return; // 已经有环了,返回
else if (!marked[w]) { // 找到没有被访问的点,递归 dfs
edgeTo[w] = e;
dfs(graph, w);
}else if (onStack[w]) { // 如果相邻点已经在同一个栈上了,则说明存在环,并记录这个环
cycle = new Stack<>();
double weight = 0; // 计算环的权值(前面如果是拓扑排序请把这部分删除)
DirectedEdge f = e;
while (f.from != w) { // 循环遍历这个环,加入cycle中
cycle.push(f);
weight += f.getWeight();
f = edgeTo[f.from];
}
cycle.push(f);
weight = weight + f.getWeight();
if (weight > 0) // 正权值环
cycle = null;
return; // 添加完环返回
}
}
onStack[v] = false; // 递归结束要出栈,将入栈标记标为 false
}
public boolean hasCycle() {
return cycle != null;
}
public Iterable<DirectedEdge> cycle() {
return cycle;
}
}