【每日算法】图算法(遍历&MST&最短路径&拓扑排序)
图有邻接矩阵和邻接表两种存储方法,邻接矩阵非常easy,这里不讨论,以下我们先看看经常使用的邻接表表示方法。
邻接表经常使用表示方法
指针表示法
指针表示法一共须要两个结构体:
struct ArcNode //定义边表结点
{
int adjvex; //邻接点域
ArcNode* next;
};
struct VertexNode //定义顶点表结点
{
int vertex;
ArcNode* firstedge;
};
每一个节点相应一个VertexNode,其firstedge指向边表(与当前节点邻接的表构成的链表)的头节点。
vector表示法
使用vector模拟邻接表,十分方便简洁,v是一个数组,相应于上面的顶点表,v[i]是一个vector。存放邻接顶点的下标。
这个方案的缺陷是:要求顶点的编号从0~MAXN-1。
vector<int> v[MAXN];
数组表示法
head[i]:指向编号为i的顶点的邻接表中第一条边的序号;
h:边的序号;
e:边表,每条边相应一个序号,由h给出;
int head[MAXN]; //初始化为-1
int h = 0;
struct Edge
{
int adjvex;
int next;
};
Edge e[MAXN<<1];
void add(int a, int b) //a与b邻接
{
e[h].adjvex = b; //当前边的序号为h,
e[h].next = head[a]; //头插法
head[a] = h++;
}
深度优先遍历
图的深度优先遍历(DFS)相似于树的前序遍历:
訪问顶点v。visited[v] = 1;
w = 顶点v的第一个邻接点。
while(w存在)
if(w未被訪问) 从顶点w出发递归运行DFS;
w = 顶点v的下一个邻接点;
广度优先遍历
图的广度优先遍历(BFS)相似于树的层次遍历:
初始化队列Q;
訪问顶点v;visited[v] = 1;顶点v入队列Q;
while(队列非空)
v = 队列Q的队头元素出队。
w = 顶点v的第一个邻接点。
while (w存在)
假设w未被訪问。訪问w;visited[w] = 1;顶点w入队列Q;
w = 顶点v的下一个邻接点;
注意,我们这里的队列用来存储已被訪问的顶点,即对于每一个顶点。我们是先訪问,再入队,这样能够避免顶点的反复入队。
最小生成树(MST)
最小生成树算法以Prim算法和Kruskal算法最为经典。
Prim算法
初始化:一个顶点。空边集。
其基本思想是。寻找这种边:满足“一个点在生成树中,一个点不在生成树中”的边中权值最小的一条边。
将找到的边增加边集中,顶点加到顶点集中。当全部顶点都增加进来时,算法结束。
初始化: U = {v0}; TE = {};
while (U != V)
{
在E中寻找最短边(u, v),且满足 u∈U。v∈V-U;
U = U + {v}。
TE = TE + {(u, v)};
}
因为Prim须要不断读取随意两个顶点之间边的权值,所以适合用邻接矩阵存储。
为找出最短边,我们定义一个候选最短边的集合。用来存放候选的边(一个点在生成树中,一个点不在生成树中)。我们使用数组shortEdge[n]来表示这个集合。数组元素包括adjvex和lowcost两个域,分别表示邻接点和权值。比方shortEdge[i].adjvex = k, shrotEdge[i].lowcost = w表示下标为i和下标为k的顶点邻接,边的权值为w。
void Prim(Graph G)
{
for (int i = 1; i < G.vertexNum; ++i) //初始化辅助数组
{
shortEdge[i].lowcost = G.arc[0][i];
shortEdge[i].adjvex = 0;
}
shortEdge[0].lowcost = 0; //将顶点0增加集合U
for (int i = 1; i < G.vertexNum; ++i)
{
int k = MinEdge(shortEdge, G.vertexNum); //寻找最短边的邻接点k;
cout << '(' << k << ',' << shortEdge[k].adjvex << ')' << endl;
shorEdge[k].lowcost = 0; //将k增加集合U
for (int j = 1; j < G.vertexNum; ++j) //更新数组shortEdge
{
//k新增加U,更新与k邻接的点的lowcost。
if (G.arc[k][j] < shortEdge[j].lowcost)
{
shortEdge[j].lowcost = G.arc[k][j];
shortEdge[j].adjvex = k;
}
}
}
}
Prim算法的时间复杂度为O(n^2)。与图中的边数无关。适合求稠密图的最小生成树。
Kruskal算法
初始化:n个顶点(即n个连通分量)。空边集。
其基本思想是:每次从未标记的边中选取最小权值的边,假设该边的两个顶点位于两个不同的连通分量。则将该边增加最小生成树。合并两个连通分量,并标记该边。否则,位于同一个连通分量,则去掉该边(相同标记就可以),避免造成回路。
可见,此算法的关键是:怎样考察两个顶点是否位于两个不同连通分量。
最简单的做法是:使用并查集。
此算法须要对边进行操作,所以我们用边集数组存储图的边,为提高最短边查找速度。能够先按权值排序。
const int MaxVertex = 10;
const int MaxEdge = 100;
struct EdgeType
{
int from, to;
int weight;
};
template <class DataType>
struct EdgeGraph
{
DataType vertex[MaxVertex];
EdgeType edge[MaxEdge];
int vertexNum, edgeNum;
};
void Kruskal(EdgeGraph G)
{
for (int i = 0; i < G.vertexNum; ++i)
parent[i] = -1;
for (int num = 0, i = 0; i < G.edgeNum; ++i)
{
v1 = FindRoot(partent, G.edge[i].from);
v2 = FindRoot(partent, G.edge[i].to);
if (v1 != v2) //不同连通分量
{
cout << '(' << v1 << ',' << v2 << ')' << endl;
parent[v2] = v1; //合并
num++;
if (num == n-1) return;
}
}
}
int FindRoot(int parent[], int v)
{
int t = v;
if (parent[t] != -1) t= parent[t]; //一直往上找到根节点
return t;
}
最短路径
最短路径经典的算法有Dijkstra算法(单源最短路,不能处理负权),SPFA算法(单源最短路,可处理负权)。Floyd算法(任一对顶点间的最短路)。
Dijkstra算法
此算法使用贪心策略,这里作为复习。仅仅讲一讲实现。不再讨论原理了。以下贴出一张图。方便大家回顾。
当中黑色顶点之间的灰色边为已选的边,黑色与灰色顶点之间的灰色边为候选边。
每一次增加一条边就更新顶点的最短路径值,贪心策略为每次选取值最小的点。
因为此算法须要高速求得随意两顶点之间边的权值,所以用邻接矩阵存储。
为记录每一个顶点的最短路径值。须要辅助数组dist[n]:dist[i]表示当前所找到的从源点v到终点vi的最短路径长度。
初始值:若从v到vi有弧,则dist[i]为弧上的权值。否则为正无穷。
为记录路径,须要辅助数组path[n]:path[i]为一个字符串。表示当前所找到的从源点v到终点vi的最短路径。
初始值:若从v到vi有弧,则path[i]为“v vi”。否则为“”。
数组s[n]:存放源点和已经生成的终点。
伪代码:
初始化数组dist,path和s
while (s中元素个数 < n)
{
在dist[n]中找最小值。其编号为k;
输出dist[k]和path[k];
改动数组dist和path;
将顶点k增加到数组s中。
}
C++实现:
void Dijkstra(MGraph G, int v)
{
for (int i = 0; i < G.vertexNum; ++i)
{
s[i] = 1;
dist[i] = G.arc[v][i];
if (path[i] != INF)
path[i] = G.vertex[v]+G.vertex[i];
else
path[i] = "";
}
s[v] = 0; //将源点增加集合S中
dist[v] = 0;
int num = 1;
while (num < G.vertexNum)
{
int k = 0;
for (int i = 0; i < G.vertexNum; ++i)
if (s[i] && (dist[i] < dist[k]))
k = i;
cout << dist[k] << ' ' << path[k] <<endl;
s[k] = 0;
++num;
for (int i = 0; i < G.vertexNum; ++i)
if (dist[i] > dist[k] + G.arc[k][i])
{
dist[i] = dist[k] + G.arc[k][i];
path[i] = path[k] + G.vertex[i];
}
}
}
时间复杂度为O(n^2)。
Floyd算法
Floyd算法用于求每一对顶点之间的最短路径问题。其算法复杂度为O(n^3)。
void Floyd(MGraph G)
{
for (int i = 0; i < G.vertexNum; ++i)
for (int j = 0; j < G.vertexNum; ++j)
{
dist[i][j] = G.arc[i][j];
if (dist[i][j] != INF)
path[i][j] = G.vertex[i]+G.vertex[j];
else
path[i][j] = "";
}
for (int k = 0; k < G.vertexNum; ++k)
for (int i = 0; i < G.vertexNum; ++i)
for (int j = 0; j < G.vertexNum; ++j)
{
if (dist[i][k]+dist[k][j] < dist[i][j])
{
dist[i][j] = dist[i][k]+dist[k][j];
path[i][j] = path[i][k] + path[k][j];
}
}
}
注意k要放在最外层。因为dist[i][j] 依赖于 dist[i][k]和dist[k][j], 所以k小的须要先计算。
SPFA
SPFA(Shortest Path Faster Algorithm)(队列优化)算法是求单源最短路径的一种算法,它另一个重要的功能是判负环(在差分约束系统中会得以体现),在Bellman-ford算法的基础上加上一个队列优化,降低了冗余的松弛操作,是一种高效的最短路算法。
bool SPFA(MGraph G, int s)
{
for (int i = 0; i < G.vertexNum; ++i)
{
dist[i] = G.arc[v][i];
}
dist[v] = 0;
memset(vis, 0, sizeof(vis));
deque<int> q;
q.push_back(s);
vis[s] = true;
while (!q.empty())
{
int k = q.front();
q.pop_front();
vis[k] = false;
for (int i = 0; i < G.vertexNum; ++i)//存在负权的话。就须要创建一个COUNT数组,当某点的入队次数超过vertexNum(顶点数)返回。
{
if (dist[i] > dist[k] + G.arc[k][i])
{
dist[i] = dist[k] + G.arc[k][i];
if (!vis[i])
{
q.push_back(i);
vis[i] = true;
}
}
}
}
}
拓扑排序
參加另一篇博文:
结语
从前阵子開始,我就打算好好梳理一下学过的基础算法。一来为接下来的面试做准备,二来能够跟大家分享自己学到的知识。
最近比較忙。所以说好的【每日算法】没能做到每日更新。
今天匆匆忙忙将几个图算法回顾了一遍。上面的代码也大多数是伪代码。或者是用C++实现的大体思路(没有经过測试,请见谅。)。
接下来可能临时停更,等到过阵子忙完再好好写几篇质量高一点的博文~
每天进步一点点,Come on!
(●’◡’●)
本人水平有限,如文章内容有错漏之处,敬请各位读者指出,谢谢。