有向图
相较于无向图,有向图的边是带有方向性的。v→w,那么v的邻接点链表中会有w,但是w的邻接点链表不存在v。因此就邻接表来说,体现有向性是通过链表中的节点有无来实现的。
public class Digraph {
private final int V;
private int E;
// 边的有向性体现在结点添加时,不像之前无向图,两端结点可以互达。
// 有向图的节点是不一定可互达的
private Bag<Integer>[] adj;
public Digraph(int V) {
this.V = V;
this.E = 0;
adj = (Bag<Integer>[]) new Bag[V];
for (int i = 0; i < V; i++) {
adj[V] = new Bag<>();
}
}
public int V() {
return V;
}
public int E() {
return E;
}
public void addEdge(int v, int w) {
adj[v].add(w);
E++;
}
public Iterable<Integer> adj(int v) {
return adj[v];
}
public Digraph reverse() {
Digraph R = new Digraph(V);
for (int i = 0; i < V; i++) {
for (int w : adj(i)) {
// 通过相反的方式添加边
R.addEdge(w, i);
}
}
return R;
}
}
有向图的可达性
public class DirectedDFS {
private boolean[] marked;
public DirectedDFS(Digraph G, int s) {
marked = new boolean[G.V()];
dfs(G, s);
}
private void dfs(Digraph G, int v) {
marked[v] = true;
for (int w : G.adj(v)) {
if (!marked[w])
dfs(G, w);
}
}
public boolean marked(int v){
return marked[v];
}
}
拓扑排序
定义:给定一幅有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素
优先级调度问题中,环的存在时不允许的,这会打破优先级熟悉怒,因此环的检测十分重要。
环检测算法及其对应的数据结构:
public class DirectedCycle {
private boolean[] marked;
private int[] edgeTo;
private Stack<Integer> cycle; // 如果有环,创建这个环
private boolean[] onStack; // 保存递归调用时,栈上的所有顶点
public DirectedCycle(Digraph G) {
onStack = new boolean[G.V()];
edgeTo = new int[G.V()];
marked = new boolean[G.V()];
for (int v = 0; v < G.V(); v++) {
if (!marked[v]) dfs(G, v);
}
}
private void dfs(Digraph G, int v) {
onStack[v] = true;
marked[v] = true;
for (int w : G.adj(v)) {
if (this.hasCycle()) return;
else if (!marked[w]) {
edgeTo[w] = v;
dfs(G, w);
}
// 如果没有环,那么一次出栈,不会再找到已在栈上的节点
// 如果找到了,那么证明图中有环的存在
else if (onStack[w]) {
cycle = new Stack<>();
// 构建这个环,依次获取DFS中顶点的上级顶点
for (int x = v; x != w; x = edgeTo[x]) {
cycle.push(x);
}
cycle.push(w);
cycle.push(v);
}
}
onStack[v] = false;
}
private boolean hasCycle() {
return cycle != null;
}
public Iterable<Integer> cycle() {
return cycle;
}
}
拓扑排序要点
- 在进行DFS时,将参数顶点保存在一个数据结构中,遍历这个数据结构实际上就能访问图中的所有顶点。
- 排序顺序(注意数据结构的使用):
- 前序:在递归调用之前将顶点加入队列
- 后序:在递归调用之后将顶点加入队列
- 逆后序:在递归调用之后将顶点压入栈
三种排序方式的数据结构 – 可类比树的遍历
public class DepthFirstOrder {
private boolean[] marked;
private Queue<Integer> pre;
private Queue<Integer> post;
private Stack<Integer> reversePost;
public DepthFirstOrder(Digraph G) {
// 用原生JDK实现
pre = new LinkedList<>();
post = new LinkedList<>();
reversePost = new Stack<>();
marked = new boolean[G.V()];
for (int v = 0; v < G.V(); v++) {
if (!marked[v]) dfs(G, v);
}
}
private void dfs(Digraph G, int v) {
// 前序
pre.offer(v);
// DFS 过程
marked[v] = true;
for (int w : G.adj(v))
if (!marked[w]) dfs(G, w);
// 逆序
post.offer(v);
// 逆后序
reversePost.push(v);
}
public Queue<Integer> pre() {
return pre;
}
public Queue<Integer> post() {
return post;
}
public Stack<Integer> reversePost() {
return reversePost;
}
}
拓扑算法实现:
public class Topological {
// 顶点的拓扑排序
private Iterable<Integer> order;
public Topological(Digraph G){
// 检测有无环
DirectedCycle cycleDetector = new DirectedCycle(G);
if(!cycleDetector.hasCycle()){
// 无环则返回节点序列
DepthFirstOrder dfs = new DepthFirstOrder(G);
// 拓扑顺序为逆后序 -- 入度小的在前面
order = dfs.reversePost();
}
}
public Iterable<Integer> order() {
return order;
}
public boolean isDAG(){
return order != null;
}
}
有向图的强连通性
-
定义:有向图的两个顶点v和w,如果是相互可达的,那么称他们强连通
-
推论:顶点强连通 当且仅当 它们都在一个普通的有向环中
-
满足离散数学推论中的:自反性、对称性、传递性
-
定义是基于点而不是边
强连通分量的计算算法 – Kosaraju算法:
这个算法的理解比较恶心,首先需要理解这个算法的目的:
- 保证在遍历强连通分量时,有一个其他连通分量的顶点,在这个强连通分量的前面
- 这样检验完一个强连通分量后,可以进入下一个强连通分量
- 强连通分量之间是可达,但不构成回路的
- 综上所述,要进行统计之前,首先要构建一个序列,将各个连通分量隔离开(内部顺序不定)
接下来是证明通过求反向图的逆后序,可以保证两个连通分量之间是隔离开的
可以看到,通过构建反向图的逆后序排列,成功将各个强连通分量隔离开,并保证可以从一个强连通分量进入下一个强连通分量
public class KosarajuSCC {
private boolean[] marked;
private int[] id;
private int count;
public KosarajuSCC(Digraph G) {
marked = new boolean[G.V()];
id = new int[G.V()];
// 核心部分 -- 其实完成了两次DFS,在创建order的时候还进行了依次dfs
DepthFirstOrder order = new DepthFirstOrder(G.reverse());
for (int s : order.reversePost()) {
if (!marked[s]) {
dfs(G, s);
count++;
}
}
}
private void dfs(Digraph G, int v) {
marked[v] = true;
id[v] = count;
for (int w : G.adj(v)) {
if (!marked[w]) dfs(G, w);
}
}
public boolean stronglyConnected(int v, int w){
// 如果所属的连通分量id相同,则证明v和w相互连通
return id[v] == id[w];
}
}