演算法之經典圖演算法
圖介紹
圖:是一個頂點集合加上一個連線不同頂點對的邊的集合組成。定義規定不允許出現重複邊(平行邊)、連線到頂點自身的邊(自環),定義了一個簡單圖。
自環:連線到頂點自身的邊。
平行邊:連線同一對頂點的兩條邊,含有平行邊的圖稱為多重圖。
頂點的度數:依附於它的邊的總數。
頂點連通:兩個頂點之間存在一條連線雙方的路徑。
連通圖:任意一個頂點都存在一條路徑到達另一個任意頂點。
極大連通子圖:一個非連通的圖由若干個連通圖組成,這些連通的圖都是極大連通子圖。
圖的密度:已經連線的頂點對佔所有可能被連線頂點對的比例,稀疏圖被連線的頂點對較少,稠密圖沒有被連線的頂點對很少。
在許多計算機應用中,由相連節點表示的模型很重要。沿著這些連線,能否從一個節點到達另外一個節點?有多少節點和指定的節點相連?兩個節點之間最短的連結是哪一條?這些都可以通過圖模型解決。這裡學習的都是已知的最優美和最有意思的演算法。下面圖片說明了圖應用的廣泛性。
表示圖的資料結構
1、鄰接矩陣
當圖含有百萬個頂點時候,陣列需要V*V儲存位,並且需要V*V步初始化。在稠密圖中,這種方式可以承受,因為和鄰接表需要差不多的儲存空間,但是對於很多頂點的稀疏圖,那麼就使用鄰接表比較合適。
2、鄰接表
將每個頂點的所有相連頂點都儲存在該頂點對應的元素所指向的一張連結串列中。
這種結構可以滿足典型應用,所以後面實現的各種圖資料結構,都是基於鄰接表。優點在於總是使用與V+E成正比空間。
圖的兩種搜尋方式
1、深度優先搜尋(depth-first-search DFS)
基本思想:是從起始頂點開始,沿著一條路一直走到底,如果發現不能到達目標解,那就返回到上一個節點,然後從另一條路開始走到底,這種儘量往深處走的概念即是深度優先的概念。
實現要訣:訪問一個頂點並將其標記為訪問,然後遞迴地訪問所有與該頂點鄰接且未標記的頂點,深度利用了可以下壓的棧(遞迴支援)。
深度優先搜尋軌跡:
深度優先搜尋動畫:
其中:搜尋結果是以起點為根節點的樹,edgeTo是由父連結表示的樹,動畫顯示的抽象的遞迴過程。
2、廣度優先搜尋(breadth-first-search BFS)
基本思想:廣度優先搜尋類似於二叉樹的層序遍歷,它的基本思想就是:首先訪問起始頂點v,接著由v出發,依次訪問v的各個未訪問過的鄰接頂點w1,w2,…,wi,然後再依次訪問w1,w2,…,wi的所有未被訪問過的鄰接頂點;再從這些訪問過的頂點出發,再訪問它們所有未被訪問過的鄰接頂點……依次類推,直到圖中所有頂點都被訪問過為止。
實現:為了實現逐層的訪問,演算法必須藉助一個輔助佇列,來儲存所有已經被標記過但是其鄰接表還未被檢查過的頂點。先將起點加入佇列,然後重複呼叫即可。
廣度優先搜尋軌跡:
廣度優先搜尋動畫:
其中:搜尋結果是以起點為根節點的樹,這個過程不包含遞迴過程,直到fifo為空,edgeTo是由父連結表示的樹,動畫顯示的抽象的過程。
DFS可以處理問題
1、單點連通性(兩個頂點是否連通?)及單點路徑(給定一幅圖和一個起點s,那麼在s和v之間是否存在一條路徑?):
通過dfs呼叫,可以得出與起點連通的路徑,並且將路徑記錄父連結樹中就可以回答上述兩個問題,下面程式碼裡面實現了。
2、一幅圖中連通分量:
一次dfs可以找出含有起點的某個連通圖,如果對所有的頂點進行dfs就可以找出全部連通圖並且由於遍歷過的點都標記了,所以不會重複以其為起點進行dfs。
3、檢測環:
4、雙色問題:
BFS可以處理問題
單點最短路徑
graph.h
#ifndef GRAPH_H
#define GRAPH_H
#include "stdio.h"
#include "stdlib.h"
#include "queue.h"
typedef struct Graphnode* Node;
struct Graphnode{
int v;
Node next;
};//鄰接表節點
typedef struct{
int v;
int w;
}Edge;//邊結構體
typedef struct graph* Graph;
struct graph {
int v;
int E;
Node *adj;
};//表結構體,含有頂點個數,邊個數,指標陣列
/******************************無向圖操作*******************************/
Graph GraphInit(int v);//初始化儲存空間
void addEdge(Graph G ,Edge E);//新增邊
void GraphShow(Graph G);//顯示鄰接表
void DFSPaths(Graph G , int s);//給定圖和起點s,給出連通圖深度搜索路徑
void BFSPaths(Graph G , int s);//給定圖和起點s,給出連通圖廣度搜索路徑
void DFSCC(Graph G);//給定圖,求連通分量
/**********************************************************************/
#endif // GRAPH_H
graph.c
#include "graph.h"
#include <memory.h>
static Node NewNode( Node head , int w )//元素插入連結串列前面
{
Node x = (Node)malloc(sizeof(struct Graphnode));
x->v = w;
x->next = head;
return x;
}
Graph GraphInit(int v)//依據頂點個數,初始化鄰接表
{
int i = 0;
Graph G = (Graph)malloc(sizeof(struct graph));//分配graph變數地址
G->v = v;
G->E = 0;
G->adj = (Node *)malloc( v * sizeof(Node));//分配指標陣列
for(i = 0 ; i < v ; i++){//初始化指標陣列全為空
G->adj[i] = NULL;//這裡將G->adj[v]越界訪問,直接破壞了堆,所以後面分配報錯。
}
return G;//將整個分配好的空間返回呼叫者
}
void addEdge(Graph G ,Edge E)//傳指標,既可以減少棧開銷也可以修改傳入引數值。
{
G->adj[E.v] = NewNode(G->adj[E.v] , E.w);//插入連結串列
G->adj[E.w] = NewNode(G->adj[E.w] , E.v);//插入連結串列
G->E++;//邊數目加1
}
void GraphShow(Graph G)//顯示鄰接表
{
int i = 0;
Node t;
printf("%d vertices , %d edges\n" , G->v , G->E);
for(i = 0 ; i < G->v ; i++){
printf("%d: " , i);
for(t = G->adj[i] ; t != NULL ; t = t->next)
printf("%2d ",t->v);
printf("\n");
}
}
static int marked[20];//標記頂點是否被訪問
static int edgeTo[20];
static int id[20];//同一連通分量頂點與對應的識別符號連結起來
static int CC_Count = 0;
bool hasPathTo(int v)//是否存在起點s到v的路徑
{
return marked[v];
}
static void dfs(Graph G , int s)//深度優先搜尋
{
Node t;
marked[s] = 1;//訪問了
id[s] = CC_Count;
for(t = G->adj[s] ; t!=NULL ; t=t->next){
if(marked[t->v] == 0){//沒有標記
edgeTo[t->v] = s;//記錄在父連結樹
dfs(G , t->v);//遞迴繼續訪問,模仿棧
}
}
}
void DFSPaths(Graph G , int s)//在G中找出所有起點為s的路徑
{
int i,j;
memset(marked , 0 , sizeof(marked));//BFS和DFS共同陣列部分清0
memset(edgeTo , 0 , sizeof(edgeTo));
dfs(G , s);
for(i = 0 ; i < G->v ; i++){
if(hasPathTo(i)){//如果到起點有路徑
printf("%d to %d :" , s , i);
for(j = i ; j != s ; j = edgeTo[j])
printf("%2d " , j);
printf("%2d " , s);
printf("\n");
}
}
}
static void bfs(Graph G , int s)//廣度優先搜尋
{
Node t;
int v;
marked[s] = 1;//標記起點
QueuePut(s);//將起點加入佇列
while( !QueueEmpty()){
v = QueueGet();
for(t = G->adj[v] ; t!=NULL ; t=t->next){
if(marked[t->v] == 0){//沒有標記
edgeTo[t->v] = v;//記錄在父連結樹
marked[t->v] = 1;//標記已知最短
QueuePut(t->v);//新增佇列
}
}
}
}
void BFSPaths(Graph G , int s)//在G中找出所有起點為s的路徑
{
int i,j;
memset(marked , 0 , sizeof(marked));
memset(edgeTo , 0 , sizeof(edgeTo));
bfs(G , s);
for(i = 0 ; i < G->v ; i++){
if(hasPathTo(i)){//如果到起點有路徑
printf("%d to %d :" , s , i);
for(j = i ; j != s ; j = edgeTo[j])
printf("%2d " , j);
printf("%2d " , s);
printf("\n");
}
}
}
//Connected Components.
void DFSCC(Graph G)//輸入圖得出連通量,一次深度搜索找出一個連通量,那麼進行v次搜尋即可找出所有連通量
{
int i , t;
for(t = 0 ; t <G->v ; t++){//所有頂點遍歷一次
if(marked[t] == 0){//沒有標記,防止重複遍歷
dfs(G , t);//遞迴繼續訪問,模仿棧
CC_Count++;//當dfs遞迴完成,那麼一個連同分量遍歷完成
}
}
printf("All Components is %d\n" , CC_Count );
for(i = 0 ; i < CC_Count ; i++){
printf("%d Components vertex:\n" , i+1);
for(t = 0 ; t <G->v ; t++){
if(id[t] == i)
printf("%d " , t);
}
printf("\n");
}
}
main.c
#include "graph.h"
Edge EdgestinyG[13] = {//連線描述陣列
{0,5},
{4,3},
{0,1},
{9,12},
{6,4},
{5,4},
{0,2},
{11,12},
{9,10},
{0,6},
{7,8},
{9,11},
{5,3},
};
Edge EdgestinyGG[8] = {//連線描述陣列
{0,5},
{2,4},
{2,3},
{1,2},
{0,1},
{3,4},
{3,5},
{0,2},
};
#define EDGES 13
int main(void)
{
/********* 測試BFS和DFS***************/
Graph G;
int i;
QueueInit(15);
G = GraphInit(6);
for(i = 0; i < 8 ; i++)
addEdge(G , EdgestinyGG[i]);
GraphShow(G);
DFSPaths( G , 0 );
printf("\n");
BFSPaths( G , 0 );
/*********測試連通圖個數***************/
// Graph G;
// int i;
// G = GraphInit(13);
// for(i = 0; i < EDGES ; i++)
// addEdge(G , EdgestinyG[i]);
// GraphShow(G);
// DFSCC(G);
return 0;
}
以下是兩種測試的結果:
如果碰到一個圖中含有多個連通圖,一般的思路就是分別處理子圖即可。
有向圖
最小生成樹
最短路徑
經典圖演算法必須時刻記住並且已經在很多領域應用了很久了,是經過實際驗證的演算法設計。