图论算法之 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;
}

总结

以上都是本人在学习数据结构的过程中记录下来的。图论的知识很广泛,这些还只是最浅显的部分,期待在将来能够学习更多有用的知识吧。

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