目錄
1、定義
一幅有方向性的圖(或有向圖)是由一組頂點和一組有方向的邊組成的,每條有方向的 邊都連接着有序的一對頂點。
1.1、應用
1.2、術語
一個頂點的出度爲 由該頂點指出的邊的總數;
一個頂點的入度爲指向該頂點的邊的總數。
2、數據結構
有向圖的數據結構和無向圖的數據結構基本一樣,區別在於無向圖在addEdge時會將兩個頂點互相連接,而有向圖只能按照指定方向將這兩個頂點相連。
2.1、代碼
public class Digraph {
private int V; // 頂點的總數
private int E; // 邊的總數
private Bag<Integer>[] adj; // 存放所有頂點的的鏈表,鏈表裏邊放着各自的所有相鄰頂點
public Digraph(int V) {
this.V = V;
adj = new Bag[V];
for (int i = 0; i < V; i++) {
adj[i] = new Bag<>();
}
}
public Digraph(In in) {
V = in.readInt();
adj = new Bag[V];
int E;
for (E = 0; E < this.V; ++E) {
this.adj[E] = new Bag<>();
}
E = in.readInt();
for (int i = 0; i < E; ++i) {
int v = in.readInt(); //將同一行的兩個數字 連成一條邊
int w = in.readInt();
addEdge(v, w);
}
}
public void addEdge(int v, int w) {
adj[v].add(w);
E++;
}
public int E() {
return E;
}
public int V() { return V; }
public Iterable<Integer> adj(int v) {
return adj[v];
}
/**
* 反向圖
*/
public Digraph reverse() {
Digraph D = new Digraph(V);
for (int v = 0; v < D.V; v++) {
for (int w : adj(v)) {
D.addEdge(w, v);
}
}
return D;
}
}
3、有向圖的可達性
在有向圖中,因爲邊的方向性的限制,導致我們的點只能按照指定的方向前進,我們有時會需要找出 從一個點出發,它能到達圖中所有可能到達的點。
對於這樣的需求,其實我們應用無向圖的深度優先搜索就能獲取結果,因爲每個頂點的鏈表存放着的都是它能到達的頂點,和無限圖中相鄰的頂點是一樣的。
3.1、代碼
public class DirectedDFS {
private final boolean[] marked; // 標記是否走過該點
private int s;// 起點
public DirectedDFS(Digraph D, int s) {
marked = new boolean[D.V()];
dfs(D, s);
}
public DirectedDFS(Digraph D, Iterable<Integer> sources) {
marked = new boolean[D.V()];
for (int s : sources) {
if (!marked[s]) {
dfs(D, s);
}
}
}
private void dfs(Digraph D, int v) {
marked[v] = true;
for (int w : D.adj(v)) {
if (!marked(w)) {
dfs(D, w);
}
}
}
private boolean marked(int w) {
return marked[w];
}
public static void main(String[] args) {
Digraph G = new Digraph(new In(args[0]));
Bag<Integer> sources = new Bag<>();
for (int i = 1; i < args.length; i++)
sources.add(Integer.parseInt(args[i]));
DirectedDFS reachable = new DirectedDFS(G, sources);
for (int v = 0; v < G.V(); v++) //將能到達的點都打印出來
if (reachable.marked(v))
StdOut.print(v + " ");
StdOut.println();
}
}
3.2、垃圾回收機制---有向圖的應用
多點可達性的一個重要的實際應用是在典型的內存管理系統中,包括許多 Java 的實現。
在一幅有向圖中,一個頂點表示一個對象,一條邊則表示一個對象對另一個對象的引用。這個模型很好 地表現了運行中的 Java 程序的內存使用狀況。在程序執行的任何時候都有某些對象是可以被直接 訪問的,而不能通過這些對象訪問到的所有對象都應該被回收以便釋放內存。標 記 - 清除的垃圾回收策略會爲每個對象保留一個位做垃圾收集之用。它會週期性地運行一個類似於 DirectedDFS 的有向圖可達性算法來標記所有可以被訪問到的對象,然後清理所有對象,回收沒有 被標記的對象,以騰出內存供新的對象使用。
4、有向圖的路徑
有向圖和無向圖的路徑都可以通過深度優先搜索和廣度優先搜索來查找。尋找路徑的代碼可以用來解決一下問題:
單點有向路徑。給定一幅有向圖和一個起點 s,回答“從 s 到給定目的頂點 v 是否存在一條有 向路徑?如果有,找出這條路徑。”等類似問題。
單點最短有向路徑。給定一幅有向圖和一個起點 s,回答“從 s 到給定目的頂點 v 是否存在一 條有向路徑?如果有,找出其中最短的那條(所含邊數最少)。”等類似問題。
4.1、深度優先搜索
public class DepathFirstPath {
private boolean[] marked; // 標記是否走過該點
private int edge[]; // s-w 路徑上 w 的上一個頂點
private int s;
public DepathFirstPath(Digraph D, int s) {
marked = new boolean[D.V()];
edge = new int[D.V()];
this.s = s;
dfs(D, s);
}
private void dfs(Digraph D, int v) {
marked[v] = true;
for (int w : D.adj(v)) {
if (!marked[w]) {
edge[w] = v;
dfs(D, w);
}
}
}
public boolean hasPathTo(int w) {
return marked[w];
}
@Nullable
private Iterable<Integer> pathTo(int v) {
if (!hasPathTo(v))
return null;
Stack<Integer> path = new Stack<>();
for (int w = edge[v]; w != s; w = edge[w]) {
path.push(w);
}
path.push(s);
return path;
}
}
4.1、有向圖最短路徑---廣度優先搜索
深度優先搜索就好像是一個 人在走迷宮,廣度優先搜索則好像是一組人在一起朝各個方向走這座迷宮, 每個人都有自己的繩子。當出現新的叉路時,可以假設一個探索者可以分裂 爲更多的人來搜索它們,當兩個探索者相遇時,會合二爲一(並繼續使用先 到達者的繩子)。
這樣做的目的可以是從起點出發到達每個頂點的路徑是最短的。
下圖有深度和廣度優先搜索的區別。
public class BreadthFirstPath {
private boolean[] marked;
private int[] edge;
private int s;
public BreadthFirstPath(Digraph D,int s) {
marked = new boolean[D.V()];
edge = new int[D.V()];
this.s=s;
bfs(D,s);
}
private void bfs(Digraph D,int s){
Queue<Integer> queue=new Queue<>();
queue.enqueue(s);
while (!queue.isEmpty()) {
int v=queue.dequeue();
for (int w:D.adj(v)){
if (!marked[w]){
edge[w]=v;
marked[w]=true;
queue.enqueue(w);
}
}
}
}
private boolean hasPathTo(int v){
return marked[v];
}
@Nullable
private Iterable<Integer> pathTo(int v) {
if (!hasPathTo(v))
return null;
Stack<Integer> path = new Stack<>();
for (int w = edge[v]; w != s; w = edge[w]) {
path.push(w);
}
path.push(s);
return path;
}
}
5、拓撲排序
對於有向圖,一種應用廣泛的模型是給定一組任務並安排它們的執行順序,並且這些任務是有優先級限制的。就比如任務A的優先級高於任務B,那麼我們必須在任務A完成之後才能執行任務B。
就比如學校課程的分配,每個課程也是有優先級的,而且一些基礎課程一定要放在高級課程的前面。但對於一張複雜的課程優先圖,我們怎麼來實現將所有課程的按照優先級排序呢?
拓撲排序。給定一幅有向圖,將所有的頂點排序,使得所有的 有向邊均從排在前面的元素指向排在後面的元素。
下圖就是通過拓撲排序將所有課程按照優先級的順序排列的,右下圖是拓撲排序的應用。
5.1、有向無環圖檢查
對有向圖進行拓撲排序前,我們必須確認該有向圖中是否含有環,如果存在環的話,那麼對於優先級的問題是會造成無解的情況, 你可以想象一下,任務A--->任務B---->任務C---->任務A 不斷的循環,那麼我們要把哪個任務放在第一個呢?
這時候我們就需要對有向圖進行檢測,當沒有環的時候才能進行拓撲排序,不然是無解的。
public class DirectedCycle {
private final boolean[] marked; //是否走過該點
private final boolean[] onStack; //對遞歸路徑上的點存入棧中(true),當該路徑沒有環時改爲false
private final int[] edge; // s-w 路徑上 w 的上一個頂點
private Stack<Integer> cycle; //存放有向環的所有頂點
public DirectedCycle(Digraph D) {
marked=new boolean[D.V()];
onStack=new boolean[D.V()];
edge=new int[D.V()];
for (int i=0;i<D.V();i++){
if (!marked(i)){
dfs(D,i);
}
}
}
private void dfs(Digraph D,int v){
marked[v]=true;
onStack[v]=true;
for (int w:D.adj(v)){
if (!marked(w)){
edge[w]=v;
dfs(D,w);
}else if (onStack[w]){
cycle=new Stack<>();
for (int k=edge[w];k!=w;k=edge[k]){
cycle.push(k);
}
cycle.push(w);
cycle.push(v);
}
}
onStack[v]=false; //-------記住退出遞歸之後要釋放該點,以防下條路徑有環並且含有該點
}
public boolean hasCycle(){
return cycle!=null;
}
public Iterable<Integer> cycle(){
return cycle;
}
private boolean marked(int w){
return marked[w];
}
}
5.2、基於深度優先搜索的頂點排序
在深度優先搜索的時候,我們其實會產生三種頂點的排序方式:(如果不理解,看一下代碼就能理解了)
- 前序:在遞歸調用之前將頂點加入隊列。
- 後序:在遞歸調用之後將頂點加入隊列。
- 逆後序:在遞歸調用之後將頂點壓入棧。
public class DepthFirstOrder {
private final boolean[] marked;
private final Queue<Integer> pre; //前序
private final Queue<Integer> post; //後序
private final Stack<Integer> reversePost; //逆後序
public DepthFirstOrder(Digraph D) {
marked = new boolean[D.V()];
pre = new Queue<>();
post = new Queue<>();
reversePost = new Stack<>();
for (int v = 0; v < D.V(); v++) {
if (!marked(v)) {
dfs(D,v);
}
}
}
private void dfs(Digraph D, int v) {
marked[v] = true;
pre.enqueue(v); //在遞歸調用之前將頂點加入隊列。
for (int w : D.adj(v)) {
if (!marked(w)) {
dfs(D, w);
}
}
post.enqueue(v); //在遞歸調用之後將頂點加入隊列。
reversePost.push(v); //在遞歸調用之後將頂點壓入棧。
}
private boolean marked(int w) {
return marked[w];
}
public Iterable<Integer> pre(){
return pre;
}
public Iterable<Integer> post(){
return post;
}
public Iterable<Integer> reversePost(){
return reversePost;
}
}
5.3、拓撲排序
一幅有向無環圖的拓撲順序即爲所有頂點的逆後序排列。
證明:對於任意邊 v → w,在調用 dfs(v) 時,下面三種情況必有其一成立。
dfs(w) 已經被調用過且已經返回了(w 已經被標記)。
dfs(w)還沒有被調用(w還未被標記),因此v→w會直接或間接調用並返回dfs(w),且 dfs(w) 會在 dfs(v) 返回前返回。
dfs(w) 已經被調用但還未返回。證明的關鍵在於,在有向無環圖中這種情況是不可能出現的,這是由於遞歸調用鏈意味着存在從w到v的路徑,但存在v→w則表示存在一個環。
在兩種可能的情況中,dfs(w) 都會在 dfs(v) 之前完成,因此在後序排列中 w 排在 v 之前而 在逆後序中 w 排在 v 之後。因此任意一條邊 v → w 都如我們所願地從排名較前頂點指向排名較後的頂點。
public class Topological {
private Iterable<Integer> order;
public Topological(Digraph D) {
DirectedCycle cycle = new DirectedCycle(D);
if (!cycle.hasCycle()) {
DepthFirstOrder depthFirstOrder = new DepthFirstOrder(D);
this.order = depthFirstOrder.reversePost();
}
}
public Iterable<Integer> order(){
return order;
}
/**
* 是否是有向無環圖
*/
public boolean isDAG() {
return order!=null;
}
}
6、有向圖中的強連通分量
無向圖中的連通分量:是指子圖的數量。
有向圖中:如果兩個頂點 v 和 w 是互相可達的,則稱它們爲強連通的。也就是說,既存在一條從 v 到 w 的有向路徑,也存在一條從 w 到 v 的有向路徑。如果一幅有向圖中的任意兩個頂點都是強 連通的,則稱這幅有向圖也是強連通的。
強連通分量:指的是一個有向圖中,含有這樣可以互相抵達的子集的數量。
下圖就有5個強連通分量,單個頂點也是一個強連通分量。
我們要想得到一個有向圖中的強連通分量,思路是:
1、我們要想確認這個子集是一個強連通分量,那麼我們得證明: v--->w是可達的 並且 w--->v 也是可達的
2、我們先講有向圖中的指向關係反轉 v--->w 改成 w--->v
3、然後我們將指向關係反轉的有向圖,進行深度優先搜索,並且得到它的拓撲排序後的序列a。(這樣做的目的是先證明w--->v是可達的)
4、我們按照序列a的順序對原來的有向圖進行可達性檢驗,當 v--->w 可達的話,那麼就證明 v---w屬於一個強連通分量中。
public class KosarajuSCC {
private boolean[] marked;
private int[] id;
private int count;
public KosarajuSCC(Digraph D) {
marked=new boolean[D.V()];
id=new int[D.V()];
DepthFirstOrder order=new DepthFirstOrder(D.reverse());
for (int v:order.reversePost()){
if (!marked(v)){
dfs(D,v);
count++;
}
}
}
private void dfs(Digraph D, int v) {
marked[v]=true;
id[v]=count;
for (int w:D.adj(v)){
if (!marked(w)){
dfs(D,w);
}
}
}
private boolean marked(int w) {
return marked[w];
}
public boolean stronglyConnected(int v,int w){
return id[v]==id[w];
}
public int id(int v){
return id[v];
}
private int count() {
return count;
}
}