上次寫了篇圖的基本構造方法,運用圖這種強大的數據結構結構,還能解決實際應用中的許多問題,今天這篇就主要整理一些常見的應用
一、路徑問題
路徑問題在圖的處理領域是非常重要的。如我們最常見的走迷宮,就是典型的尋路問題。這里主要運用深度優先和廣度優先算法兩種方式來進行路徑尋找,這2種搜索算法在很多數據結構中都有重要的運用,之前寫的一篇二叉查找樹中的層序遍歷就用到了廣度優先算法,這里就詳細的介紹一下。
1.深度優先尋找路徑
首先是深度優先,為了更加形象,直接看下圖
![][1]
這里以頂點1為出發點,4為終點。假設一個人要走到終點,從1出發有三條路徑,首先沿著往5的路徑進行遍歷,依次1 -> 5 -> 9 -> 8,然后發現沒路了,就返回上一頂點,到頂點5這里發現還有一條路就繼續沿著這條路走,5->3->6,結果又沒路了,就繼續返回到起點,沿著另一條路行走......1->2->4,一看,這下就直接到終點了,轉了幾條路終于找到了終點,那心里真是無比的興奮啊!回到正題,看看這個具體實現,可以用一個boolean類型的變量來標記是否遍歷過該頂點,用一個int型的變量表示從起點到一個頂點的已知路徑上的最后一個頂點。至于圖的基本構造可以參考我之前寫的[圖論基礎篇][2]
/**
* 圖的深度優先查找路徑
* @author Legend
*/
public class DepthFirstPaths {
private boolean[] marked; // 該頂點是否調用過dfs
private int[] edgeTo; // 從起點到一個頂點的已知路徑上的最后一個頂點
private final int s; // 起點
// 圖的初始化
public DepthFirstPaths(Graph G,int s) {
marked = new boolean[G.V()];
edgeTo = new int[G.V()];
this.s = s;
dfs(G,s);
}
// 深度優先主方法
private void dfs(Graph G,int v) {
marked[v] = true;
// 遍歷與頂點v相連的邊
for (int w : G.adj(v)) {
if (!marked[w]) {
edgeTo[w] = v;
dfs(G,w); // 繼續遞歸的進行遍歷
}
}
}
// 是否存在s到v的路徑
public boolean hasPathTo(int v) {
return marked[v];
}
// s到v的路徑
public Iterable<Integer> PathTo(int v) {
if (!hasPathTo(v)) {
return null;
}
Stack<Integer> path = new Stack<>();
for (int i = v;i != s;i = edgeTo[i]) {
path.push(i);
}
path.push(s);
return path;
}
}
因為我們要準確的知道每一條路徑,所以這里創建了一個edgeTo變量用于記錄從起點到一個頂點的已知路徑上的最后一個頂點,而edgeTo[w] = v表示的就是從w到v的路徑。再看PathTo方法,首先判斷是否有從s到v的路徑,沒有就返回null。然后實例化一個Stack類型對象path,依次遍歷,把路徑上每一個頂點都push進去,最后在push頂點,并返回path。
2.廣度優先尋找路徑
廣度優先正如其名,優先進行廣度的遍歷,整個過程呈擴散狀。這里還是用上面那張圖,為了方便查看,還是把圖片放到這里。
![][3]
[1]: http://img.mukewang.com/59fb24c20001780108340606.png
[2]: http://blog.cspojie.cn/2017/10/20/%E5%9B%BE%E8%AE%BA-%E5%9F%BA%E7%A1%80%E7%AF%87/#more,這里直接用Graph來表示,然后具體實現看看下面的代碼
[3]: http://img.mukewang.com/59fd753d0001780108340606.png
還是用之前的情景,從頂點1出發,先遍歷和1相鄰的頂點 2,5,3,然后從頂點2開始,右繼續遍歷和2相鄰的頂點,因為已經遍歷過頂點1,所以這里就只需要遍歷頂點7和頂點4,發然后現就直接到終點了,這是在特殊的情況下,如果先遍歷的是另外的頂點,那么幾乎要走完每條路才能找到終點。這里就不多說了,重點講下具體實現,這里用一個抽象數據結構---隊列來實現,首先創建一個隊列,然后把起點標記后插入隊列中去,如果隊列不為空就把當前的頂點彈出隊列,然后依次遍歷和這個頂點相鄰的頂點,并把這些頂點標記后也加入隊列,接下來用同樣的方式將下一頂點彈出隊列,遍歷和其相鄰的頂點,直到隊列為空就停止遍歷。好了,具體看下面的代碼
/**
*
* 廣度優先尋找路徑
* @author Legend
**/
public class BreadthFirstPaths {
private boolean[] marked;
private int[] edgeTo;
private final int s;
public BreadthFirstPaths(Graph G,int s) {
marked = new boolean[G.V()];
edgeTo = new int[G.V()];
this.s = s;
try {
bfs(G,s);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void bfs(Graph G,int s) throws InterruptedException {
Queue<Integer> queue = new Queue<Integer>();
marked[s] = true;
queue.enqueue(s);
while ( !queue.isEmpty() ) {
int v = queue.dequeue(); // 從隊列中彈出一個頂點
for (int w : G.adj(v)) { // 遍歷和該頂點相鄰的頂點
if ( !marked[w] ) {
edgeTo[w] = v; // 保存最短路徑的最后一條邊
marked[w] = true; // 標記它 因為最短路徑已知
queue.enqueue(w);
}
}
}
}
public boolean hasPathTo(int v) {
return marked[v];
}
public Iterable<Integer> pathTo(int v) {
if ( !hasPathTo(v) ) {
return null;
}
Stack<Integer> path = new Stack<>();
for (int i = 0;i != s;i =edgeTo[i] ){
path.push(i);
}
path.push(s);
return path;
}
}
這里同樣運用了pathTo方法,頂點與路徑的標記過程和深度優先尋找路徑是一樣的,這里就不多說了。
連通分量問題
1.介紹
連通分量也是一個比較常見的問題,主要用于判斷任意兩個結點的連接狀態,特別是用于檢測網絡連接與電路連接的問題中,運用比較廣。
2.基本實現
也沒什么好說的,這里還是運用深度優先的方法,比較簡單,就直接上代碼
/**
* 使用深度優先找出圖中的連通分量
*
* @author Legend
* @create 2017-11-01 8:23
**/
public class CC {
private int count;
private boolean marked[];
private int[] id;
public CC(Graph G) {
marked = new boolean[G.V()];
id = new int[G.V()];
for (int s = 0;s < G.V();s++) {
if (!marked[s]) {
dfs(G,s);
count++;
}
}
}
private void dfs(Graph G,int v) {
marked[v] = true;
id[v] = count;
for (int w : G.adj(v)) {
if (!marked[w]) {
dfs(G,w);
}
}
}
// 判斷兩個結點是否相連接
public boolean isConnected(int v,int w) {
return id[v] == id[w];
}
// v所在連通分量的標識符(0~count-1)
public int id(int v) {
return id[v];
}
// 連通分量的數量
public int count() {
return count;
}
}
雙色問題
1.介紹
能否用2種顏色將圖的所有頂點著色,使得任意一條邊的兩個端點的顏色都不相同,這個問題也就等價于當前的圖是不是二分圖。因為二叉樹其實就是一種比較特殊的圖,所以才有這個問題。
2.基本實現
具體實現可以用一個bool類型的變量來表示2種顏色,直接在構造方法里面進行循環,這里也是運用深度優先的方式。首先判斷當前結點是否被遍歷,沒被遍歷過就進行遍歷,也就是用bool型變量將其標記為true,然后遍歷和這個結點相連的結點,并且把這個結點和相鄰的結點涂上不同的顏色。然后進行判斷
先來看下代碼
/**
* 雙色問題
*
* @author Legend
* @create 2017-11-01 9:17
**/
public class TwoColor {
private boolean[] marked; //當前結點是否被遍歷過
private boolean[] color; // 表示不同顏色
private boolean isTwoColorable = true; // 是否能用2種顏色表示
public TwoColor(Graph G) {
marked = new boolean[G.V()];
color = new boolean[G.V()];
for (int s = 0;s < G.V();s++) {
if (!marked[s]) {
dfs(G,s);
}
}
}
private void dfs(Graph G,int v) {
marked[v] = true;
for (int w : G.adj(v)) {
if (!marked[w]) {
color[w] = !color[v];
dfs(G,w);
} else if (color[w] == color[v]) {
isTwoColorable = false;
}
}
}
public boolean isBipartite() {
return isTwoColorable;
}
}
如果這2個頂點顏色相同,則不能使得任意一條邊的2個端點的顏色都不相同,則這個圖不是二分圖,試想一下,如果是二分圖,任意一條的兩個端點肯定顏色是不相同的。因為每次遍歷時都將當前頂點與連接頂點標記了2種不同的顏色,如果這個頂點有多個相鄰頂點,并且這些相鄰頂點又有邊相連,這必然會造成2個顏色相同,這樣的圖自然也不可能是二分圖了。
因為這篇博客主要是整理圖論的一些應用的問題,所以對于這些問題的優化這里不是重點,有興趣的可以自己去查查資料,那圖論應用篇就暫時到這里了。
ps:該blog首發lenged's blog