【C++】圖論總結

圖論在信息學競賽中佔了很大部分,很多實際問題可以用圖論來解決。

定義

  • 什麼叫圖論?

  • 研究圖的問題一門高深的學科。

  • 什麼是圖?

  • 就是由點和線組成的圖形
    G=<V,E>
    G=graph V=vertex E=edge

圖的描述:

來自百度地圖,侵刪

圖的表示:

來自維基百科,侵刪在這裏插入圖片描述

分類

  • 有向圖和無向圖
    有向圖就是邊有方向的
    無向圖就是可以兩邊走的
  • 混合圖: 既有有向又有無向邊
  • 簡單圖:沒有重邊和自環的圖
  • 完全圖:任意兩個點直接都有一條邊
    比如n個點完全圖有C(n,2)條邊
  • 稀疏圖和稠密圖
    稀疏圖: 邊的數量相對點來說很少
    稠密圖 : 邊的數量接近於完全圖
  • 連通圖
    連通圖: 在無向圖如果任意兩個之間都可以相互到達,就是連通圖。
    n個點連通圖最少需要n-1條邊。
    強連通圖: 在有向圖裏面,任意兩點可以相互到達,就是強連通。
    n個點的強連通圖最少需要n條。
    弱連通圖:有向圖中,任意兩個點,至少有一個點可以到達另外一個點。

頂點的度

頂點的度: 和點相連的邊的數量。
有向圖的度可以分爲入度和出度,A的入度1,A的出度2,A度3。
在這裏插入圖片描述
定理:1 任何一個圖裏面頂點的度之和一定是邊的數量的2倍
2 有向圖中所有頂點的入度之和是等於所有頂點的出度之和。
3 任意一個無向圖一定有偶數個奇點.

例題1:

  • 一個無向圖有16條邊(每個點的度至少是2),其中4個度爲3,3個度爲4,求這個無向圖最多有幾個點 ?(2003年普及組問題求解)
  • 答案是11

例題2:

  • 一個無向圖有4個結點,其中3個的度數爲2,3,3,則第4個結點的度數不可能是___________
    A. 0 B. 1 C. 2 D. 4
  • 答案是B

例題3:

  • 假設我們用d=(a1,a2,….a5)表示無向無自環圖G的5個頂點的度數,下面給出的哪組值是可能的?
    A.{3,4,4,3,1}
    B.{4,2,2,1,1}
    C.{3,3,3,2,2}
    D.{3,4,3,2,1}
  • 答案是B

另外,樹一個特殊的圖,n個點,n-1條邊。

圖裏面邊的存儲方法

一 相鄰矩陣

int a[10][10];
用a[i][j]>0 表示i到j有邊
a[i][j]==k 表示i到j的邊長

優點:寫法簡單,能在O(1)得出任意兩個點是否有邊和邊長。
缺點:在稀疏圖的時候空間浪費太大,找和i相鄰的點需要O(n)的時間。

二 數組模擬鄰接表(邊表)

int a[10][10];
用a[i][0]表示和i相連的點有幾個
a[i][j]表示和i相連的第j個點的編號。

讀入:

while(m--) {
	int x,y;
	cin >> x >> y;
	a[x][++a[x][0]]=y;
	a[y][++a[y][0]]=x;
}

查找於x相連的點:

for(int i=1; i<=a[x][0]; i++) cout<<a[x][i];

優點: 查詢與x相鄰的點時間複雜是O(k) 。(k是相鄰的點的數量)
缺點: 空間還是需要很大,需要在O(k)時間知道i和j是否有邊。

三 利用stl標準模板庫裏的動態數組vector(前向星)

定義:

vector<int> a;           //a一維數組動態數組
vector<vector<int> > a;  //a二維數組
vector<int> a[100];      //定義了100個一維

注意:第二種定義方法中>和>間必須加空格,否則會編譯錯誤

使用如果a數組擁有第i個元素,那麼直接可以用a[i]表示第i個數,注意a數組從0開始

存放(把x放到a數組的最後):

a.push_back(x);   //把x放到a數組的最後

查找所有和x相鄰的點:

for(int i=0; i<a[x].size(); i++) cout << a[x][i];

整合:

int n,m;
vector<int> edge[N];

void init() {
    cin >> n >> m;
    for(int i=0; i<m; i++) {
        int x,y;
        cin >> x >> y;         //邊連接的兩個頂點
        edge[x].push_back(y);  //添邊x->y
        edge[y].push_back(x);  //添邊y->x
    }
}

優點 :節省空間,找x相鄰的需要O(k)的複雜度
缺點: 判斷i和j是否有邊需要O(k),比自己寫的鄰接表要慢一些。

四 前向星鄰接表(鏈式前向星)

定義:

struct edge{
	int to,nt;     //to是邊的終點,nt(next)是下一條邊的序號
} e[邊的數量];

int h[N],cnt;      // h[i]表示i的第一條在e裏面序號,cnt是邊的總數

建邊:

inline void add(int a,int b){
	e[++cnt].to=b; 
	e[cnt].nt=h[a]; 
	h[a]=cnt;
} 

讀入:

while(m--) {
	int x,y;
	scanf(%d%d”,&x,&y);
	add(x,y);
	add(y,x);
}

枚舉與x的相鄰的所有點:

for(int i=h[x]; i; i=e[i].nt) cout<<e[i].to;

整合:

struct edge{
	int to,nt; 
} e[邊的數量];

int h[N],cnt;

void add(int a,int b) {
	e[++cnt].to=b;
	e[cnt].nt=h[a];
	h[a]=cnt;
}

int main() {
	while (m--) {
		int x,y;
		scanf(%d%d”,&x,&y);
		add(x,y);
		add(y,x);
	}
	for(int i=h[x]; i; i=e[i].nt) cout << e[i].to;
}

圖的遍歷問題

圖的遍歷問題是搜索圖。
圖的搜索分爲深度優先搜索和寬度優先搜索兩種方法。

深度優先搜索

對下圖進行深度優先搜索,寫出搜索結果。注意:從A出發。
在這裏插入圖片描述
從頂點A出發,進行深度優先搜索的結果爲:A,B,C,D,E。

對於一個連通圖,深度優先遍歷的遞歸過程如下:

void dfs(int i) { //圖用鄰接矩陣存儲
	//訪問頂點i;
	visited[i]=1;
	for(int j=1; j<=n; j++)
		if(!visited[j] && a[i][j]) dfs(j)}

以上dfs(i)的時間複雜度爲O(n^2)。
對於一個非連通圖,調用一次dfs(i),即按深度優先順序依次訪問了頂點i所在的(強)連通分支,所以只要在主程序中加上:

for(int i=1; i<=n; i++)   //深度優先搜索每一個未被訪問過的頂點
	if(!visited[i]) dfs(i); 

廣度優先搜索(寬度優先搜索)

對下圖從A出發進行寬度優先搜索,寫出搜索結果。
在這裏插入圖片描述
從頂點A出發,進行寬度優先遍歷的結果爲: A,B,C,D,E 。

void bfs(int i) { //寬度優先遍歷,圖用鄰接矩陣表示
	queue<int> q;
	i=q.pop();
	visited[i]=true;
	q.push(i);
	while(!q.empty()) {
		v=q.front();
		q.pop();
		for(int j=1; j<=n; j++) {
			if(!visited[j]) {
  				visited[j]=1;
				q.push(j);
			}
		}
	}
}

時間複雜度是O(n^2).
BFS與DFS的總結:

  • DFS:類似回溯,利用堆棧進行搜索
    BFS:類似樹的層次遍歷,利用隊列進行搜索
  • DFS:儘可能地走“頂點表”
    BFS:儘可能地沿着頂點的“邊”進行訪問
  • DFS:容易記錄訪問過的路徑
    BFS:不易記錄訪問過的路徑,需要開闢另外的存儲空間進行保存路徑

圖的最短路徑算法

分類:

  • 多源最短路徑算法:求任意兩點之間的最短距離。
    Floyd算法
  • 單源最短路徑算法:求一個點到其他所有點的最短路徑
    Dijkstra算法,Spfa算法,Bellman-ford算法

Floyd算法

時間複雜度O(n^3)
本質上是一個動態規劃
f[i][j]表示i到j最短路徑長度
開始的時候,如果i到j有邊,那麼f[i][j]就是直接的邊長,如果沒邊,f[i][j]就是無窮大。

for(int k=1; k<=n; k++) 
	for(int i=1; i<=n; i++) 
		for(int j=1; j<=n; j++)
			f[i][j]=min(f[i][j],f[i][k]+f[k][j]); 

爲什麼k循環要寫在最外面?

這個狀態數組本來是3維的。
f[i][j][0]表示i到j的最短路徑中間經過了0個點。
f[i][j][0]=edge[i][j] edge[i][j]表示i到j直接的邊長
如果i到j邊不存在,f[i][j][0]=無窮大。
f[i][j][k]表示i到j的最短路徑中間最多經過了1到k這些點
答案就是f[i][j][n]
f[i][j][k]=min(一定沒有經過k,一定經過k)
=min(f[i][j][k-1],f[i][k][k-1] + f[k][j][k-1])
把一維舍掉:
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);

Dijkstra算法

思想 :貪心的思想
步驟:
1 標記所有的點都沒有求得最短路徑,所有的d[i]=無窮大,除了起點的d值是0。
2 循環n次,每次從沒有求得最短路徑的點裏面找出一個d值最小的點,把他標記,用這個點去更新其他沒有求得最短路徑的點。

void dijkstra() {
	memset(vis,0,sizeof(vis));
	for(int i=1;i<=n;i++) d[i]=inf;
	d[s]=0;
	for(int i=1;i<=n;i++) {
		int k=-1;
		for(int j=1;j<=n;j++)
			if(!vis[j] && (k==-1 || d[k]>d[j])) k=j;
		vis[k]=1;
		for(int j=1;j<=n;j++)
			if(!vis[j] && d[k]+edge[k][j]<d[j])
				d[j]=d[k]+edge[k][j];
	}
}

Spfa算法

設dist代表s到i點的當前最短距離,fa代表s到i的當前最短路徑中i點之前的一個點的編號。開始時dist全部爲+∞,只有dist[s]=0,fa全部爲0。
維護一個隊列,裏面存放所有需要進行迭代的點。初始時隊列中只有一個點S。用一個布爾數組記錄每個點是否處在隊列中。
每次迭代,取出隊頭的點v,依次枚舉從v出發的邊v->u,設邊的長度爲len,判斷dist[v]+len是否小於dist[u],若小於則改進dist[u],將fa[u]記爲v,並且由於s到u的最短距離變小了,有可能u可以改進其它的點,所以若u不在隊列中,就將它放入隊尾。這樣一直迭代下去直到隊列變空,也就是S到所有的最短距離都確定下來,結束算法。

int const oo=1e9;
vector<int> a[N],b[N];
queue<int> q;
int s,t;
int v[N],d[N];

int spfa() {
	for(int i=1; i<=n; i++) d[i]=oo;
	q.push(s);
	v[s]=1;
	d[s]=0;
	while(!q.empty()) {
		int x=q.front();
		q.pop();
		v[x]=0;
		for(int i=0; i<a[x].size(); i++) {
			int tp=a[x][i];
			if(d[tp]>d[x]+b[x][i]) {
				d[tp]=d[x]+b[x][i];
				if(!v[tp]) {
					q.push(tp);
					v[tp]=1;
				}
			}
		}
	}
	if(d[t]==oo) d[t]=-1;
	return d[t];
}

未完待續……

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章