圖論算法之 Dijkstra單源最短路、歐拉回路、最小生成樹、Fold-Fulkerson最大流

基本圖算法的思想與實現

整理總結了一些學習數據結構時常用的基礎算法,其中比較簡單的BFS、DFS、染色和Floyd算法等這裏就不再多說,這篇文章主要介紹一些做OJ和考試時很重點的算法。

Dijkstra最短路徑

無向圖算法的代表應該就是Dijkstra算法了,它可以用平方時間複雜度求解圖中所有點到某個頂點的最短路徑,是很實用的解題算法。
其思路是貪心思路,主要思想有二
1、如果某個頂點到終點最短路徑的所有局部最優解都被找到,那麼到達該頂點的最短路徑就是這些局部最優解中的最短路。
2、如果v->…->destination是v到終點的最短路徑,那麼u->v->…->destination是u到終點最短路徑的所有局部最優解。
憑藉這兩個思想,我們從終點(或起點,無向圖中這兩者可以替換)開始尋找到達它的所有局部最優路徑,就能遞歸地求解所有頂點的最短路。
找最短路可以線性查找,可以排序,但最有效的一種方法是使用優先隊列,這樣我們的循環次數沒有改變,但是每次找最短操作的代價減小到了對數級。
C++實現如下

#include <iostream>
#include <map>
#include <vector>
#include <string>
#include <queue>

#define MAXN 205
#define INF 100000000

using namespace std;



struct edge
{
	int dest;
	int val;
	edge(int d, int v) :dest(d), val(v) {}
};


vector<edge> graph[MAXN];
int dist[MAXN];
int N,cnt =0;
bool visited[MAXN];

class cmp
{
public:
	bool operator()(const edge &a, const edge &b) const { return a.val > b.val; } //優先隊列
};




int dijkstra(int start, int end)
{
	priority_queue<edge, vector<edge>, cmp> q;
	for (int i = 0;i < cnt;i++) dist[i] = (i == end ? 0 : INF);
	fill(visited, visited + MAXN, 0);
	q.push(edge(end,0));
	while (!q.empty())
	{
		edge short_path = q.top();
		int vex = short_path.dest;
		q.pop();
		cout << "Pop " << vex << " length:" << short_path.val << endl;
		if (visited[vex])
			continue;
		visited[vex] = 1;
		for (edge e : graph[short_path.dest])
		{
			if (visited[e.dest])
				continue;
			if (dist[e.dest] > (dist[vex] + e.val))
			{
				//更新dist並在heap中push e.dest
				dist[e.dest] = dist[vex] + e.val;
				//插入更短的路徑,不用在意原先已經插入的路徑,因爲它再也不會進入這層判斷
				q.push(edge(e.dest, dist[e.dest]));
			}
		}
	}

	if (dist[start] != INF) return dist[start];
	return -1;
}

/*
測試例數目
1
12
0 1 4
0 2 6
0 3 6
1 2 1
1 4 7
2 3 2
2 4 6
2 5 4
3 5 5
4 5 1
4 6 6
5 6 8

最短路起點和終點
0 6

*/


int main()
{
	int t;

	cin >> t;
	while (t--)
	{
		int n, j, dis;
		string x, y;
		map<string, int> p;
		cnt = 0;
		for (int i = 0;i < MAXN;i++)
			graph[i].clear();
		cin >> N;
		while (N--)
		{
			cin >> x >> y >> dis;//輸入數據的處理
			if (!p.count(x)) p[x] = cnt++;
			if (!p.count(y)) p[y] = cnt++;

			graph[p[x]].push_back(edge(p[y],dis ));
			graph[p[y]].push_back(edge(p[x], dis));
		}

		cin >> x >> y;
		if (x == y) cout << 0 << endl; 
		else if (!p.count(x) || !p.count(y)) cout << -1 << endl;
		else cout << dijkstra(p[x], p[y]) << endl;
	}

	

	return 0;
}

這裏使用了一個結構體表示路徑,另外新建了一個用於優先隊列排序的cmp類。

C++的優先隊列沒有自帶的decrease key方法,所以這裏就沒有在每次插入新路徑時,刪除之前的更長的路徑(如果用斐波那契堆去這樣做會更高效),而是不管之前的路徑,用一個visited數組表示某個頂點是否已經尋得最優解,來讓之前push進的更長的路徑以後不會發揮作用。

一般來說,鄰接表比鄰接矩陣更高效,尤其是用於稀疏圖時。

Python的話大同小異,可以用集合

def Dijkstra(Map,i):
    #對給定的鄰接矩陣,考慮第i個地點和其他位置的最小距離
    #首先獲取該地點和其他地點的初始距離
    initdist = {}
    for key in range(len(Map)):
        initdist[key] = Map[i][key]
    dist = [{},initdist]
    while (len(dist[1])):
        idx = list(dist[1].keys())[0]
        #算法核心1,貪心,已知的最短路即是該點最優解,因爲所有正權重的邊,對權重最小的邊,比起直接到達,繞路永遠更遠
        for key in dist[1]:
            if dist[1][key]<dist[1][idx]:
                idx = key
        dist[0][idx] = dist[1][idx]
        #算法核心2,把Floyd的二維數組更迭變爲一維數組更迭,依然考慮經過第i(i不按順序遍歷)個結點後最短路徑可能的更新
        for key in dist[1]:
            dist[1][key] = min(dist[1][idx]+Map[idx][key],dist[1][key])
        dist[1].pop(idx)
    return dist[0]

歐拉回路

歐拉回路是著名的“七橋問題”數學模型,它期望在無向圖中,找到一條不重複地走過所有邊的路徑。這裏介紹一種基礎的算法,Fleury算法。
首先我們有歐拉回路的存在的充分必要條件,每個頂點的度必須是偶數。也因此,七橋問題被證實無解。
Fleury算法的思想是很簡單的,它基於割邊的概念進行。如果G-e(圖去除邊e)有兩個連通分支,則e是G中的橋;判斷某條邊是不是橋可以藉助連通性搜索實現。
Fleury算法從任意頂點開始搜索,算法從當前頂點嘗試經由未遍歷過的邊,走到下一個頂點。選擇邊的優先級爲 非割邊>割邊
所以Fleury算法也是一種貪心算法,如果算法執行到最後,即所有邊都被遍歷過一次,那麼找到的迴路一定是歐拉回路。
C++實現如下

#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
/*
弗羅萊算法
*/
 
int stk[1005];
int top;
int N, M, ss, tt;
int mp[1005][1005];
 
void dfs(int x) {
    stk[top++] = x;
    for (int i = 1; i <= N; ++i) {
        if (mp[x][i]) {
            mp[x][i] = mp[i][x] = 0; // 刪除此邊
            dfs(i);
            break;
        }   
    }
}
/*
9 12
1 5
1 9
5 3
5 4
5 8
2 3
2 4
4 6
6 7
6 8
7 8
8 9
 
path:
5 8 7 6 8 9 1 5 3 2 4 6
*/
 
void fleury(int ss) {
    int brige;
    top = 0;
    stk[top++] = ss; // 將起點放入Euler路徑中
    while (top > 0) {
        brige = 1;
        for (int i = 1; i <= N; ++i) { // 試圖搜索一條邊不是割邊(橋)
            if (mp[stk[top-1]][i]) {
                brige = 0;
                break;
            }
        }
        if (brige) { // 如果沒有點可以擴展,輸出並出棧
            printf("%d ", stk[--top]);
        } else { // 否則繼續搜索歐拉路徑
            dfs(stk[--top]);
        }
    }
}
 
int main() {
    int x, y, deg, num;
    while (scanf("%d %d", &N, &M) != EOF) {
        memset(mp, 0, sizeof (mp));
        for (int i = 0; i < M; ++i) {
            scanf("%d %d", &x, &y);
            mp[x][y] = mp[y][x] = 1;
        }
        for (int i = 1; i <= N; ++i) {
            deg = num = 0;
            for (int j = 1; j <= N; ++j) {
                deg += mp[i][j];   
            }
            if (deg % 2 == 1) {
                ss = i, ++num;
                printf("%d\n", i);
            }
        }
        if (num == 0 || num == 2) {
            fleury(ss);
        } else {
            puts("No Euler path");
        }
    }
    return 0;   
}

Python實現如下

def Connected_num(Map):
    L = len(Map)
    cnt = 0
    visited = [0 for _ in range(L)]
    for i in range(L):
        if (not visited[i]):
            cnt += 1
            q = queue.Queue()
            q.put(i)
            visited[i] = 1
            while(q.qsize()):
                v = q.get()
                for j in range(L):
                    if (Map[v][j] and (not visited[j])):
                        visited[j] = 1
                        q.put(j)
    return cnt

#生成歐拉回路的Fleury算法
from copy import deepcopy


def Fleury(Map):#該函數僅限於無向圖鄰接矩陣
    L = len(Map)
    #首先確定是否存在歐拉回路, 即每個頂點的度是否爲偶數
    for i in range(L):
        if (sum(Map[i])%2!=0):
            print("No Euler circuit exist.")
            return

    
    
    #從0頂點開始尋找歐拉回路
    cur_vertice = 0
    euler_circuit = []
    while sum(sum(Map[i]) for i in range(L))>0:#原圖中還有邊存在
        bridge = []
        nor_bridge = []
        
        for i in range(L):
            if (Map[cur_vertice][i]):   #確定每條邊是否爲橋
                #這裏需要計算去除前和去除後的孤立島數量變化
                
                M = deepcopy(Map)
                pre_iso = Connected_num(M)
                M[cur_vertice][i] = 0;
                M[i][cur_vertice] = 0;
                post_iso = Connected_num(M)
                if post_iso-pre_iso>=1:
                    bridge.append(i)
                else:
                    nor_bridge.append(i)
        
        
        
        if len(nor_bridge):
            next_vertice = nor_bridge[0]
        elif len(bridge):
            next_vertice = bridge[0]
        else:
            print("No Euler circuit exist.")
            return
        Map[cur_vertice][next_vertice] = 0
        Map[next_vertice][cur_vertice] = 0
        euler_circuit.append((cur_vertice,next_vertice))
        cur_vertice = next_vertice
    return euler_circuit

最小生成樹

最小生成樹也是無向圖的代表算法,一般出現在算法題和計算機網絡中,代表性的算法是Prim和Kruskal算法。兩種算法前者是基於頂點,後者基於邊。
Prim算法從某個頂點出發,自身構成一個圖,然後不斷尋找最短的,連接該圖和外界的最短邊。找到這個最短邊後,就把該邊和另一端的頂點都加入當前圖,然後繼續尋找最短邊。直到找到了N-1條邊爲止。(N爲頂點數)
Kruskal算法從邊出發,不斷連接距離最短的,不構成迴路的兩個頂點。直到找到了N-1條邊爲止。
容易看出,Prim算法類似Dijkstra,可以用優先隊列優化。而Kruskal算法可以先對所有邊進行排序,然後基於並查集運行。
一般情況下,Kruskal算法的效率高於Prim算法(如果優先隊列的性能較高則不然),而對稠密圖,Prim算法的表現優於Kruskal算法。
Prim算法實現

#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
#include <cstdio>
using namespace std;

struct edge
{
	int val;
	int start;
	int end;
};


bool cmp(edge e1, edge e2)
{
	return e1.val < e2.val;
}

int inf = 99999999, N, M;
int Graph[1005][1005];
int MST[1005][1005];
bool visited[1005];



void Prim()//鄰接矩陣規模
{
	fill(visited, visited + 1005, 0);
	//讀取所有邊,存儲在vector中
	vector<edge> Edges;
	for (int i = 0;i < N;i++)
		for (int j = 0;j < N;j++)
			if (Graph[i][j] != inf)
				Edges.push_back({ Graph[i][j],i,j });
	sort(Edges.begin(), Edges.end(), cmp);
	visited[0] = 1;//生成樹的起點從0開始
	bool flag = 1;//判斷算法正常運行與否
	for (int T = 0;T < N - 1;T++)
	{
		flag = 0;
		for (edge e : Edges)
		{
			if (visited[e.start] and !visited[e.end])
			{
				MST[e.start][e.end] = e.val;
				MST[e.end][e.start] = e.val;
				cout << e.start << " " << e.end << endl;
				visited[e.end] = 1;
				flag = 1;
				break;
			}
		}
		if (!flag)
		{
			cout << "Generate fail!" << endl;
			return;
		}

	}
}

//kruskal算法使用連通性查詢,可以用並查集更快實現
int father[1005];

int getF(int x)
{
	if (father[x] == -1)
		return x;
	else
		return getF(father[x]);
}

void Union(int x, int y)
{
	int fa = getF(y);
	father[fa] = getF(x);
}


bool connect(int x, int y)
{
	return (getF(x) == getF(y));
}


void Kruskal()//鄰接矩陣規模
{
	fill(father, father + 1005, -1);
	vector<edge> Edges;
	for (int i = 0;i < N;i++)
		for (int j = 0;j < N;j++)
			if (Graph[i][j] != inf)
				Edges.push_back({ Graph[i][j],i,j });
	sort(Edges.begin(), Edges.end(), cmp);
	int cnt = 0;
	for (edge e : Edges)
	{
		if (cnt >= N - 1)
			break;
		if (!connect(e.start, e.end))
		{
			MST[e.start][e.end] = e.val;
			MST[e.end][e.start] = e.val;
			//cout << e.start << " " << e.end << endl;
			Union(e.start, e.end);
			cnt++;
		}

	}
	if (cnt<N-1)
	{
		cout << "Generate fail!" << endl;
		return;
	}
	

}
/*
測試例
5 10
0 1 2200
0 2 700
0 3 800
0 4 1400
1 2 1200
1 3 2000
1 4 900
2 3 1000
2 4 1300
3 4 1600
*/



int main()
{
	freopen("input.txt", "r", stdin);
	cin >> N;
	for (int i = 0;i < N;i++)
		for (int j = 0;j < N;j++) {
			Graph[i][j] = inf;
			MST[i][j] = inf;
		}

	cin >> M;
	for (int i = 0;i < M;i++)
	{
		int s, e, v;
		cin >> s >> e >> v;
		Graph[s][e] = v;
		Graph[e][s] = v;
	}
	Prim();
	//Kruskal();
}

最大流問題

最大流問題是在有向圖中討論的,我們想求一個網絡中的可行流,使得流量達到最大。
最大流問題有很多解決方案,我們要介紹的是其中最樸素的解決方案,Fold-Fulkerson算法。它基於增廣路徑思想;增廣路徑就是一條從起點到終點,路徑上的邊權值有效的路徑。我們每次尋找一條增廣路徑,並把該路徑的最大流計入反向邊:每次尋找到一條增廣路徑都有可能阻塞一條更好的通路,所以我們設計一個“後悔”的可能性如果我們在路徑中選擇了w(u,v)-delta,那麼w(v,u)就被+delta。這樣下次選路徑的時候,就可以選出“借用”之前的流的通路不斷進行DFS尋路,直到再也找不到增廣路徑,我們就求得了最大流。

因爲每次操作,總最大流至少增加1(整數有向圖邊權值最小爲1),所以複雜度爲O(fE)。
C++實現如下

#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
const int MAXN = 505;

int flow[MAXN][MAXN];//流圖,表示某條邊的流量剩餘
int residual[MAXN][MAXN];//反向流,表示可以讓之前選擇過的流量撤銷多少,可以當作flow使用
int N;//頂點數

struct path
{
	vector<int> vertex;//路徑上的頂點
	vector<bool> type;//流動方式(使用原有流通量或借用之前的流通量)
	int constr;//最小流量約束,約定它爲0時爲未找到有效增廣路徑
};

bool visited[MAXN];


void init()
{
	fill(flow[0], flow[0] + MAXN * MAXN, 0);
	fill(residual[0], residual[0] + MAXN * MAXN, 0);

}

void add_edge(int s, int t, int val)
{
	flow[s][t] = val;
}


void dfs(path& p, int end)
{
	int s = p.vertex[p.vertex.size() - 1];
	if (s == end) return;//當前的path就是我們要的一條有效增廣路徑
	for (int i = 0;i < N;i++)
	{
		if (visited[i]) continue;
		if (flow[s][i] > 0)//正向流過
		{
			p.vertex.push_back(i);
			p.type.push_back(1);
			visited[i] = 1;
			dfs(p, end);
		}
		else if (residual[s][i] > 0)//反向流過
		{
			p.vertex.push_back(i);
			p.type.push_back(0);
			visited[i] = 1;
			dfs(p, end);
		}
	}
	if (p.vertex[p.vertex.size() - 1] == s)
	{
		visited[s] = 0;
		p.vertex.pop_back();
		if (p.type.size()) p.type.pop_back();
	}
}

path find_path(int s, int t)//尋找增廣路,注意不同於一般的深度搜索,這裏用了回溯的思想
{
	fill(visited, visited + MAXN, 0);
	path result;
	visited[s] = 1;
	result.constr = 1000000;
	result.vertex.push_back(s);
	dfs(result,t);
	if (result.vertex.empty())
	{
		result.constr = 0;
		return result;
	}
	//確定constr的值
	for (int i = 0;i < result.vertex.size() - 1;i++) {
		result.constr = min(result.constr, result.type[i] ?
			flow[result.vertex[i]][result.vertex[i + 1]] :
			residual[result.vertex[i]][result.vertex[i + 1]]);
	}
	cout << "Find a path going though vertexs:";
	for (int v : result.vertex)
		cout << v << " ";
	cout << endl;
	cout << "Flow type:";
	for (int t : result.type)
		cout << t << " ";
	cout << endl;
	return result;
}




int Maxflow(int s, int t)
{
	int max_flow = 0;
	while (1)
	{
		path p = find_path(s, t);
		if (p.constr <= 0) break;
		int L = p.vertex.size();
		for (int i = 0;i < L - 1;i++)
		{
			if (p.type[i])
			{
				flow[p.vertex[i]][p.vertex[i + 1]] -= p.constr;
				residual[p.vertex[i+1]][p.vertex[i]] += p.constr;
			}
			else
			{
				flow[p.vertex[i]][p.vertex[i + 1]] += p.constr;
				residual[p.vertex[i+1]][p.vertex[i]] -= p.constr;
			}
		}
		max_flow += p.constr;
	}
	return max_flow;
}



int main()
{
	N = 6;
	init();
	add_edge(0, 1, 4);
	add_edge(0, 2, 2);
	add_edge(1, 2, 1);
	add_edge(1, 3, 2);
	add_edge(1, 4, 4);
	add_edge(2, 4, 2);
	add_edge(3, 5, 3);
	add_edge(4, 5, 3);
	cout << "Maxflow is "<<Maxflow(0, 5) << endl;
}

總結

以上都是本人在學習數據結構的過程中記錄下來的。圖論的知識很廣泛,這些還只是最淺顯的部分,期待在將來能夠學習更多有用的知識吧。

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