基本圖算法的思想與實現
整理總結了一些學習數據結構時常用的基礎算法,其中比較簡單的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;
}
總結
以上都是本人在學習數據結構的過程中記錄下來的。圖論的知識很廣泛,這些還只是最淺顯的部分,期待在將來能夠學習更多有用的知識吧。