基本图算法的思想与实现
整理总结了一些学习数据结构时常用的基础算法,其中比较简单的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;
}
总结
以上都是本人在学习数据结构的过程中记录下来的。图论的知识很广泛,这些还只是最浅显的部分,期待在将来能够学习更多有用的知识吧。