一、图的遍历
1.1 图的广度优先遍历
这里我用的上一章节中图的邻接矩阵的结构,进行遍历的。

这里其实跟二叉树中,层序遍历思想差不多,都是前一个节点带后面的节点(A带B,C,D之后依次类推)当然这里有一个难点就是当我们遍历到B的时候我们插入队列的时候有AEC,但是我们只想要E,因为A已经pop掉了,C在队列中。我们该如何操作?用个数组标记一下全部元素,当前要插入队列中的元素是否用过,也就是说当我们元素插入到队列的时候标记一下。
代码理解一下
void BFS(const V& src)
{
size_t srci = GetVertexIndex(src);
queue<size_t> q;
// 标记数组
vector<bool> visited(_vertexs.size(), false);
q.push(srci);
visited[srci] = true;
size_t levelSize = 1;
while (!q.empty())
{
while (levelSize--)
{
size_t front = q.front();
q.pop();
cout << front << ":" << _vertexs[front];
// 把front顶点的邻接顶点入队列
for (size_t i = 0; i < _vertexs.size(); ++i)
{
// 过滤掉已经入队的
if (_matrix[front][i] != MAX_W && visited[i] == false)
{
q.push(i);
visited[i] = true;
}
}
}
cout << endl;
// 更新levelSize
levelSize = q.size();
}
}
测试结果

1.2 图的深度优先遍历

这种就是递归,不用管上图标注的对不对,get到思想即可。
void _DFS(size_t srci, vector<bool>& visited)
{
// 遍历当前结点并标记
cout << srci << ":" << _vertexs[srci] << endl;
visited[srci] = true;
// 寻找当前节点没有被访问过的邻接顶点,继续递归往深度遍历
for (size_t i = 0; i < _vertexs.size(); i++)
{
if (_matrix[srci][i] != MAX_W && visited[i] == false)
{
_DFS(i, visited);
}
}
}
void DFS(const V& src)
{
size_t srci = GetVertexIndex(src);
// 标记数组
vector<bool> visited(_vertexs.size(), false);
_DFS(srci, visited);
}
二、最小生成树
构造最小生成树的方法: Kruskal 算法 和 Prim 算法 。这两个算法都采用了 逐步求解的贪心策略 。
在学习 Kruskal****算法和Prim****算法的时候要先了解一下什么是并查集。
并查集
在一些应用问题中,需要 将 n 个不同的元素划分成一些不相交的集合 。 开始时,每个元素自成一个
单元素集合,然后按一定的规律将归于同一组元素的集合合并 。在此过程中 要反复用到查询某一
个元素归属于那个集合的运算 。适合于描述这类问题的抽象数据类型称为 并查集 (union-find
set) 。
比如:某公司今年校招全国总共招生 10 人,西安招 4 人,成都招 3 人,武汉招 3 人, 10 个人来自不
同的学校,起先互不相识,每个学生都是一个独立的小团体,现给这些学生进行编号: {0, 1, 2, 3,
4, 5, 6, 7, 8, 9}; 给以下数组用来存储该小集体,数组中的数字代表:该小集体中具有成员的个数。( 负号下文解释 )
毕业后,学生们要去公司上班,每个地方的学生自发组织成小分队一起上路,于是:
西安学生小分队 s1={0,6,7,8} ,成都学生小分队 s2={1,4,9} ,武汉学生小分队 s3={2,3,5} 就相互认识
了, 10 个人形成了三个小团体。假设右三个群主 0,1,2 担任队长,负责大家的出行。

一趟火车之旅后,每个小分队成员就互相熟悉,称为了一个朋友圈。
从上图可以看出:编号 6,7,8 同学属于 0 号小分队,该小分队中有 4 人 ( 包含队长 0) ;编号为 4 和 9 的同
学属于 1 号小分队,该小分队有 3 人 ( 包含队长 1) ,编号为 3 和 5 的同学属于 2 号小分队,该小分队有 3
个人 ( 包含队长 1) 。
仔细观察数组中内融化,可以得出以下结论:
- 数组的下标对应集合中元素的编号
- 数组中如果为负数,负号代表根,数字代表该集合中元素个数
- 数组中如果为非负数,代表该元素双亲在数组中的下标
并查集一般可以解决以下问题:
- 查找元素属于哪个集合
沿着数组表示树形关系往上一直找到根( 即:树中中元素为负数的位置 )- 查看两个元素是否属于同一个集合
沿着数组表示的树形关系往上一直找到树的根,如果根相同表明在同一个集合,否则不在- 将两个集合归并成一个集合
将两个集合中的元素合并
将一个集合名称改成另一个集合的名称- 集合的个数
遍历数组,数组中元素为负数的个数即为集合的个数。
代码实现一下
class UnionFindSet
{
public:
UnionFindSet(int n)
:_ufs(n, -1)
{}
int FindRoot(int x)
{
int root = x;
while (_ufs[root] >= 0)
root = _ufs[root];
//路径压缩
while (_ufs[x] >= 0)
{
int parent = _ufs[x];
_ufs[x] = root;
x = parent;
}
return root;
}
bool Union(int x, int y)
{
int root1 = FindRoot(x);
int root2 = FindRoot(y);
if (root1 == root2)
{
return false;
}
// -20 -100
if (_ufs[root1] > _ufs[root2])
{
swap(root1, root2);
}
_ufs[root1] += _ufs[root2];
_ufs[root2] = root1;
return true;
}
bool InSet(int x1, int x2)
{
return FindRoot(x1) == FindRoot(x2);
}
int SetSize()
{
int count = 0;
for (auto e : _ufs)
{
if (e < 0)
++count;
}
return count;
}
private:
vector<int> _ufs;
};
2.1、Kruskal算法
克鲁斯卡尔算法
步骤:找最小的边 ,不能成环

定义一个边的类
struct Edge
{
size_t _srci;
size_t _dsti;
W _w;
Edge(size_t srci, size_t dsti, const W& w)
:_srci(srci)
,_dsti(dsti)
,_w(w)
{}
bool operator > (const Edge& e) const
{
return _w > e._w;
}
};
这里就是先找最小边,并且节点相连的边不为环 ,如何判断相连的边不为环用并查集,比如我们看图f和图g,为什么最后选了7没选6,因为选6这个边构成环了,怎么判断构成的环?这个边的i节点与g节点他们能找到共同的根节点。
代码实现一下
W Kruskal(Self& minTree)
{
size_t n = _vertexs.size();
minTree._vertexs = _vertexs;
minTree._indexMap = _indexMap;
minTree._matrix.resize(n);
for (size_t i = 0; i < n; ++i)
{
minTree._matrix[i].resize(n, MAX_W);
}
priority_queue<Edge, vector<Edge>, greater<Edge>> minque;
for (size_t i = 0; i < n; ++i)
{
for (size_t j = 0; j < n; ++j)
{
if (i < j && _matrix[i][j] != MAX_W)
{
minque.push(Edge(i, j, _matrix[i][j]));
}
}
}
// 依次从priority_queue中选最小n-1条边连接minTree即可(判断构成环的不能选)
int size = 0;
UnionFindSet ufs(n);
W totalW = W();
while (!minque.empty())
{
Edge minEg = minque.top();
minque.pop();
if (!ufs.InSet(minEg._srci, minEg._dsti))
{
cout << _vertexs[minEg._srci] << "->" << _vertexs[minEg._dsti] << ":" << minEg._w << endl;
minTree._AddEdge(minEg._srci, minEg._dsti, minEg._w);
ufs.Union(minEg._srci, minEg._dsti);
++size;
totalW += minEg._w;
}
else
{
cout << " 构成环 : ";
cout << _vertexs[minEg._srci] << "->" << _vertexs[minEg._dsti] << ":" << minEg._w << endl;
}
}
if (size == n - 1)
{
return totalW;
}
else
{
return W();
}
}
验证一下

2.2、Prim****算法
普利姆算法
步骤:通过选中的节点,找未选中的节点的边,且不能成环

这里我借助了,两个数组X,Y标记该节点是否插入。X表示选中的边,Y表示未选中的边
刚开始我们选了a为节点,这里我们用堆,把a的两个边插入到堆中,并在X中标注a已经选中,在Y中标注为已经选过了。我们这里通过堆排序筛选出了ab这个边最小又因为b不在X中,这里我们在把与b相连的边添加到堆中,这里我们要注意一下重复边ba是不能插入的,因为这个边我们已经选过了,如何判断ba是不能插入的呢?这里我们用到了Y数组我们只要与b相连且未被选中的节点我们就可以插入到堆中。按照这个思路以此类推
代码实现一下:
W Prim(Self& minTree, const W& src)
{
size_t srci = GetVertexIndex(src);
size_t n = _vertexs.size();
minTree._vertexs = _vertexs;
minTree._indexMap = _indexMap;
minTree._matrix.resize(n);
for (size_t i = 0; i < n; ++i)
{
minTree._matrix[i].resize(n, MAX_W);
}
vector<bool> X(n, false);
vector<bool> Y(n, true);
X[srci] = true;
Y[srci] = false;
priority_queue<Edge, vector<Edge>, greater<Edge>> minq;
for (size_t i = 0; i < n; ++i)
{
if (_matrix[srci][i] != MAX_W)
{
minq.push(Edge(srci, i, _matrix[srci][i]));
}
}
// 选最小
size_t size = 0;
W totalW = W();
while (!minq.empty())
{
Edge minEg = minq.top();
minq.pop();
// 最小边的目标点也在X集合,则构成环
if (X[minEg._dsti])
{
// 构成环
cout << "构成环: " << minEg._srci << "->" << minEg._dsti << endl;
}
else
{
cout << "加边: " << minEg._srci << "->" << minEg._dsti << " [" <<minEg._w<<"]" << endl;
minTree._AddEdge(minEg._srci, minEg._dsti, minEg._w);
X[minEg._dsti] = true;
Y[minEg._srci] = false;
++size;
totalW += minEg._w;
if (size == n - 1)
break;
for (size_t i = 0; i < n; ++i)
{
if (_matrix[minEg._dsti][i] != MAX_W && Y[i])
{
minq.push(Edge(minEg._dsti, i, _matrix[minEg._dsti][i]));
}
}
}
}
// 判断是否只有一个树
if (size == n - 1)
{
return totalW;
}
else
{
return W();
}
}
验证一下:

三、最短路径
最短路径问题:从在带权有向图 G 中的某一顶点出发,找出一条通往另一顶点的最短路径,最短也就是沿路径各边的权值总和达到最小。
3.1、Dijkstra****算法
迪杰斯特拉算法
Dijkstra 算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路
径的最短路径。
步骤:以s为起点,更新相连的节点,选出相连最小的边y,然后更新一下y到其他节点的边,更新相连的节点为最小值。以此类推....

这里我定义了一个s,dist,pPath数组。
s数组表示没有选中过的节点,dist表示起始节点到该下标节点的最短路径,pPath数组这里存储他们的父节点。
这里的算法思想:我要找到s数组中没有选中的节点且dist最小,之后进行松弛更新,更新一下该节点到邻接点的距离,之后把选中的节点标注为true表示选过了,之后再在未选中的节点中找dist中最小的值。
// 顶点个数是N -> 时间复杂度:O(N^2)空间复杂度:O(N)
void Dijkstra(const V& src, vector<int>& pPath, vector<W>& dist)
{
// 初始化一下记录路径和权值(距离)的数组
size_t srci = GetVertexIndex(src);
size_t n = _vertexs.size();
pPath.resize(n, -1);
dist.resize(n, MAX_W);
// 集合S为已确定最短路径的顶点集合
vector<bool> s(n, false);
pPath[srci] = srci;
dist[srci] = 0;
for (size_t i = 0; i < n; i++)
{
// 选最短路径顶点且不在S更新其他路径
int u = 0; // u为找到最小路径的下标
W minDist = MAX_W;
for (size_t j = 0; j < n; j++)
{
if (s[i] == false && dist[i] < minDist)
{
u = i;
minDist = dist[i];
}
}
s[u] = true;
// 松弛更新,更新起点到它相邻点的距离
for (size_t v = 0; v < n; v++)
{
if (s[v] == false && _matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v])
{
dist[v] = dist[u] + _matrix[u][v];
pPath[v] = u;
}
}
}
}
验证一下:

3.2、Bellman-Ford****算法
贝尔曼-福特算法
bellman---ford 算法可以解决负权图的单源最短路径问题。
先解释一下为什么迪杰斯特拉算法 解决不了负权图的单源最短路径问题,正因为没有负权值导致所有的节点只走一遍,如果遇到负权值把原来,原点到节点的距离进行修改而这个节点并且已经被标注为true了表示选中过的节点。那么与这个节点相连的节点的值全部都要进行修改,因为原点到这个节点的距离进行了修改。
步骤:以s为起始节点,对与s相连的节点进行松弛,把t,y节点进行更新,之后在依次更新与t,y相连的节点....以此类推....
这里我们还是采用了三个数组s,dist,pPath
这里我们采用暴力搜索的算法,先初始化节点s,然后更新与之相连的边t,y,这次我们不像迪杰斯特拉算法找最小边,而是更新所有t,y的边,然后找到t,y相连的所有节点,再依次更新与之相连的所有边。
// 解决负权值
bool BellmanFord(const V& src, vector<W>& dist, vector<int>& pPath)
{
size_t n = _vertexs.size();
size_t srci = GetVertexIndex(src);
// vector<W> dist,记录srci-其他顶点最短路径权值数组
dist.resize(n, MAX_W);
// vector<int> pPath 记录srci-其他顶点最短路径父顶点数组
pPath.resize(n, -1);
// 先更新srci->srci为缺省值
dist[srci] = W();
cout << "映射关系: " << endl;
for (size_t i = 0; i < _vertexs.size(); ++i)
{
cout << "[" << i << "]" << "->" << _vertexs[i] << endl;
}
cout << endl;
// 总体最多更新n轮
for (size_t k = 0; k < n; ++k)
{
// i->j 更新松弛
bool update = false;
cout << "更新第:" << k << "轮" << endl;
for (size_t i = 0; i < n; ++i)
{
for (size_t j = 0; j < n; ++j)
{
if (_matrix[i][j] != MAX_W && dist[i] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
{
update = true;
dist[j] = dist[i] + _matrix[i][j];
pPath[j] = i;
}
}
}
if (update == false)
{
break;
}
}
// 还能更新就是带负权回路
for (size_t i = 0; i < n; ++i)
{
for (size_t j = 0; j < n; ++j)
{
// srci -> i + i ->j
if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
{
return false;
}
}
}
cout << "dist:" << endl;
for (size_t i = 0; i < n; i++)
{
cout << "[" << 0 << "]->[" << i << "]->" << dist[i] << endl;
}
return true;
}
效果展示一下:

3.3、Floyd-Warshall 算法
弗洛伊德算法是一种解决多源最短路径问题(任意两点间的最短路径)的算法。
Di,j,k表示从i到j的最短路径,该路径经过的中间结点是剩余的结点组成的集合中的结点,假设经过k个结点,编号为1...k,然后这里就分为了两种情况:
1.如果路径经过了结点k,那么ij的距离就等于ik的距离加上kj的距离,然后剩余就经过k-1个点
2.如果不经过结点k,那ij的距离就等于i到j经过k-1个点(不包括k)的距离


void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath)
{
size_t n = _vertexs.size();
vvDist.resize(n);
vvpPath.resize(n);
// 初始化权值和路径矩阵
for (size_t i = 0; i < n; ++i)
{
vvDist[i].resize(n, MAX_W);
vvpPath[i].resize(n, -1);
}
// 直接相连的边更新一下
for (size_t i = 0; i < n; ++i)
{
for (size_t j = 0; j < n; ++j)
{
if (_matrix[i][j] != MAX_W)
{
vvDist[i][j] = _matrix[i][j];
vvpPath[i][j] = i;
}
if (i == j)
{
vvDist[i][j] = W();
}
}
}
// 最短路径的更新 i-> {其他顶点} -> j
for (size_t k = 0; k < n; ++k)
{
for (size_t i = 0; i < n; ++i)
{
for (size_t j = 0; j < n; ++j)
{
// k 作为的中间点尝试更新i->j的路径
if (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W
&& vvDist[i][k] + vvDist[k][j] < vvDist[i][j])
{
vvDist[i][j] = vvDist[i][k] + vvDist[k][j];
// 找跟j相连的上一个邻接顶点
// 如果k->j 直接相连,上一个点就k,vvpPath[k][j]存就是k
// 如果k->j 没有直接相连,k->...->x->j,vvpPath[k][j]存就是x
vvpPath[i][j] = vvpPath[k][j];
}
}
}
}
}
