文章目录
- 数据结构表示图
- 最小生成树
- Kruskal
- Prim
- 最短路径
- Dijkstra
- Bellman-Ford算法
- 多源最短路径:FloydWarshall
- 总结
数据结构表示图
首先节点的存取,V是节点key,vector<pair<V,V>> map;其实已经能表达一个图了,但是这样保存节点对我们使用来说会导致复杂度高。
常用保存节点的方式,有矩阵和邻接表。
矩阵的优点:O(1) 时间找到两点是否相连以及他们的权值。
矩阵的缺点:找一点相邻的所有节点的时候是O(N)的,即会遍历到不相连的两两节点。
图还分有向图和无向图,一般来说有向图存出边即可。
template<class V, class W, bool Direction = false> V表示节点的类型,W表示权值的类型,Direction表示是否是无向图。
图一般提供的函数,构造函数,将图的每一个节点的距离初始化;
AddEdge函数:将两个节点建立联系,附上权值;
GetVertexIndex: 将对应的图的节点转化为下标
template <class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
public:
typedef Graph<V, W, MAX_W, Direction> Self;
Graph() = default;
Graph(const V *vertexs, size_t n)
{
_vertexs.reserve(n);
for (size_t i = 0; i < n; ++i)
{
_vertexs.push_back(vertexs[i]);
_vIndexMap[vertexs[i]] = i;
}
// MAX_W 作为不存在边的标识值
_matrix.resize(n);
for (auto &e : _matrix)
{
e.resize(n, MAX_W);
}
}
size_t GetVertexIndex(const V &v)
{
auto ret = _vIndexMap.find(v);
if (ret != _vIndexMap.end())
{
return ret->second;
}
else
{
throw invalid_argument("不存在的顶点");
return -1;
}
}
void _AddEdge(size_t srci, size_t dsti, const W &w)
{
_matrix[srci][dsti] = w;
if (Direction == false)
{
_matrix[dsti][srci] = w;
}
}
void AddEdge(const V &src, const V &dst, const W &w)
{
size_t srci = GetVertexIndex(src);
size_t dsti = GetVertexIndex(dst);
_AddEdge(srci, dsti, w);
}
void Print()
{
// 打印顶点和下标映射关系
for (size_t i = 0; i < _vertexs.size(); ++i)
{
cout << _vertexs[i] << "-" << i << " ";
}
cout << endl
<< endl;
cout << " ";
for (size_t i = 0; i < _vertexs.size(); ++i)
{
cout << i << " ";
}
cout << endl;
// 打印矩阵
for (size_t i = 0; i < _matrix.size(); ++i)
{
cout << i << " ";
for (size_t j = 0; j < _matrix[i].size(); ++j)
{
if (_matrix[i][j] != MAX_W)
cout << _matrix[i][j] << " ";
else
cout << "#"
<< " ";
}
cout << endl;
}
cout << endl
<< endl;
// 打印所有的边
for (size_t i = 0; i < _matrix.size(); ++i)
{
for (size_t j = 0; j < _matrix[i].size(); ++j)
{
if (i < j && _matrix[i][j] != MAX_W)
{
cout << _vertexs[i] << "-" << _vertexs[j] << ":" << _matrix[i][j] << endl;
}
}
}
}
private : map<V, size_t> _vIndexMap;
vector<V> _vertexs; // 顶点集合
vector<vector<W>> _matrix;
// 存储边集合的矩阵
};
void TestGraph()
{
Graph<char, int, INT_MAX, true> g("0123", 4);
g.AddEdge('0', '1', 1);
g.AddEdge('0', '3', 4);
g.AddEdge('1', '3', 2);
g.AddEdge('1', '2', 9);
g.AddEdge('2', '3', 8);
g.AddEdge('2', '1', 5);
g.AddEdge('2', '0', 3);
g.AddEdge('3', '2', 6);
g.Print();
}
}
namespace LinkTable
{
template <class W>
struct LinkEdge
{
int _srcIndex;
int _dstIndex;
W _w;
LinkEdge<W> *_next;
LinkEdge(const W &w)
: _srcIndex(-1), _dstIndex(-1), _w(w), _next(nullptr)
{
}
};
template <class V, class W, bool Direction = false>
class Graph
{
typedef LinkEdge<W> Edge;
public:
Graph(const V *vertexs, size_t n)
{
_vertexs.reserve(n);
for (size_t i = 0; i < n; ++i)
{
_vertexs.push_back(vertexs[i]);
_vIndexMap[vertexs[i]] = i;
}
_linkTable.resize(n, nullptr);
}
size_t GetVertexIndex(const V &v)
{
auto ret = _vIndexMap.find(v);
if (ret != _vIndexMap.end())
{
return ret->second;
}
else
{
throw invalid_argument("不存在的顶点");
return -1;
}
}
void AddEdge(const V &src, const V &dst, const W &w)
{
size_t srcindex = GetVertexIndex(src);
size_t dstindex = GetVertexIndex(dst);
// 0 1
Edge *sd_edge = new Edge(w);
sd_edge->_srcIndex = srcindex;
sd_edge->_dstIndex = dstindex;
sd_edge->_next = _linkTable[srcindex];
_linkTable[srcindex] = sd_edge;
// 1 0
// 无向图
if (Direction == false)
{
Edge *ds_edge = new Edge(w);
ds_edge->_srcIndex = dstindex;
ds_edge->_dstIndex = srcindex;
ds_edge->_next = _linkTable[dstindex];
_linkTable[dstindex] = ds_edge;
}
}
private:
map<string, int> _vIndexMap;
vector<V> _vertexs; // 顶点集合
vector<Edge *> _linkTable; // 边的集合的临接表
};
void TestGraph()
{
string a[] = {"张三", "李四", "王五", "赵六"};
Graph<string, int> g1(a, 4);
g1.AddEdge("张三", "李四", 100);
g1.AddEdge("张三", "王五", 200);
g1.AddEdge("王五", "赵六", 30);
}
}
最小生成树
什么是最小生成树
在给定一张无向图,如果在它的子图中,任意两个顶点都是互相连通,并且是一个树结构,那么这棵树叫做生成树。当连接顶点之间的图有权重时,权重之和最小的树结构为最小生成树!
Kruskal
- 选最短的边,且不能构成回路,会用到并查集。因为需要找两个点是否已经存在路径当中。
- AddEdge可以重载,但是由于V可能就是int,就会造成错误,所以采用换个函数名的方式达到复用的效果。
- 总之细节很多,实现完看看代码。有不是连通图的情况也需要判断。
//传入参数为一个图,是一个输入型参数,最终图的数值会保存在该图当中。
//返回一个最终的权值总和
int Kruskal(Self& minTree)
{
//首先初始化最小生成树
minTree._vertex = _vertex;
minTree._vertex2index = _vertex2index;
size_t len = _weightmatrix.size();
minTree._weightmatrix.resize(len, vector<W>(len, MaxSize));
//Kruskal算法关注的是边的关系,需要每次取出权重最小的边
priority_queue<Edge,vector<Edge>,greater<Edge>> pq;
//初始化优先级队列
for (int i = 0; i < len; ++i)
{
for (int j = 0; j < len; ++j)
{
//这个点存在有效权值则入pq,注意这里只用入矩阵的一半即可
if (i < j && _weightmatrix[i][j] != MaxSize)
{
pq.push(Edge(i, j, _weightmatrix[i][j]));
}
}
}
//此时就有了一个优先级队列保存最小的节点。
//需要拿出优先级队列的节点并且需要是不能构成环的
//全局贪心,每次找最小的边进行合并,需要注意不能在一个集合当中,所以可以使用并查集来,且需要保存的是点
//将所有找到的边合并起来就是答案
UnionFindSet unionset(_vertex.size());
W weight = W();
int i = 1;//边是比点少一条的。
while (!pq.empty())
{
Edge edge = pq.top();
pq.pop();
if (unionset.FindRoot(edge._srci) == unionset.FindRoot(edge._desti))
{
cout << "构成环: " << _vertex[edge._srci] << "->" << _vertex[edge._desti] << endl;
continue;
}
i++;
cout << "Add:" << edge._srci << "-> " << edge._desti << endl;
minTree.Add2Weight(_vertex[edge._srci], _vertex[edge._desti], edge._weight);
unionset.Union(edge._srci, edge._desti);
cout << _vertex[edge._srci] << "->" << _vertex[edge._desti] << endl;
weight += edge._weight;
}
if (i != _vertex.size())
{
//这个时候表示不是一个连通图
cout << "不是连通图" << endl;
}
return weight;
}
Prim
步骤:
- 先加入
const V& v
四周围的边入优先级队列(小堆),每次从头部获取一个最短的边,记录为访问,添加新增节点的四周围的边。 - 为了防止添加的边有重复,我们采用
vector<bool>
来记录每一个顶点,记录每一个添加进来的顶点。
当然选的边不同,结果也有可能相同。
//Prim算法是基于局部的贪心
int Prim(Self& minTree, const V& v)
{
//初始化最小生成树
minTree._vertex = _vertex;
minTree._vertex2index = _vertex2index;
int len = _vertex.size();
minTree._weightmatrix.resize(len, vector<W>(len, MaxSize));
//初始化优先级队列,只不过入一个节点周围的
priority_queue<Edge,vector<Edge>,greater<Edge>> pq;
int srci = VertexToIndex(v);
for (int i = 0; i < len; ++i)
{
if (_weightmatrix[srci][i] != MaxSize)
pq.push(Edge(srci, i, _weightmatrix[srci][i]));
}
//初始化访问的列表,这个可以用作区分集合,出边不在visited就不会构成环
vector<bool> visited(len, false);
visited[srci] = true;
W weight = W();//默认权值
int size = 0;//记录边数
//从优先级队列取出一个点作为最小的值,这个位置被认为是总体权值最小所加的一条边。
while (!pq.empty())
{
Edge indexEdge = pq.top();
pq.pop();
//如果该边终点已经访问过,那么就一定会构成环。
if (visited[indexEdge._desti])
{
cout << "形成回路: " << _vertex[indexEdge._srci] << "->" << _vertex[indexEdge._desti] << endl;
//该点若已经访问过,说明是入边,此时加入该点会造成环
continue;
}
cout << "加入集合: " << _vertex[indexEdge._srci] << "->" << _vertex[indexEdge._desti] << endl;
weight += _weightmatrix[indexEdge._srci][indexEdge._desti];
//入四周围的边
for (int i = 0; i < _weightmatrix[indexEdge._desti].size(); ++i)
{
//该点没有访问过并且是可以到达的则入集合
if (_weightmatrix[indexEdge._desti][i] != MaxSize && visited[i] == false)
{
pq.push(Edge(indexEdge._desti, i, _weightmatrix[indexEdge._desti][i]));
cout << "加入" << _vertex[indexEdge._desti] << " 后新增了" << _vertex[i] << endl;
}
}
minTree.Add2Weight(_vertex[indexEdge._srci], _vertex[indexEdge._desti], indexEdge._weight);
visited[indexEdge._desti] = true;
size++;
}
//若size最终为边数说明边都入进来了。
if (size == _vertex.size()-1)
{
//表明了到达这里的时候没有找到一个点
cout << "是连通图" << endl;
return weight;
}
else
{
cout << "不是联通图" << endl;
return W();
}
return weight;
}
最短路径
从A点到其他点的最短距离。
Dijkstra无法解决带负权值的,后续的算法无法解决带负权回路。
Dijkstra
找的起始边,用邻接表和邻接矩阵的有各自的优缺点。
每次用最短的路径更新周围的节点进行松弛操作。
每一次选取一个点,作为到达该点的最短点,由该点更新周围其他点。
缺点:有负权边用Dijkstra会出错。
//Dist数组就是告知从src到对应下标所要的代价(权值),path就是该节点父亲节点的下标
void Dijkstra(const V& src, vector<int>& dist, vector<int>& path)
{
int index = VertexToIndex(src);
size_t n = _vertex.size();
dist.resize(n, MaxSize);
path.resize(n, -1);
path[index] = index;//自己就是自己的父节点
dist[index] = 0;
int len = path.size();
vector<bool> visited(len, false);
int size = len;
while (size--)
{
//用index去跟新其他节点的路径
for (int i = 0; i < n; ++i)
{
if (_weightmatrix[index][i] != MaxSize && dist[i] > dist[index] + _weightmatrix[index][i])
{
//更新其他路径需要保证更新完更小
dist[i] = dist[index] + _weightmatrix[index][i];
//更新父亲
path[i] = index;
}
}
//更新index
visited[index] = true;
int minNum = INT_MAX;
for (int i = 0; i < n; ++i)
{
if (!visited[i] && dist[i] < minNum)
{
index = i;
minNum = dist[i];
}
}
if (minNum == INT_MAX)
{
break;
}
}
if (size != 0)
{
cout << "不是连通图" << endl;
}
}
Bellman-Ford算法
Dijkstra算法只能用来解决正权图的单源最短路径问题,但有些题目会出现负权图。这时这个算法
就不能帮助我们解决问题了,而bellman—ford算法可以解决负权图的单源最短路径问题。它的
优点是可以解决有负权边的单源最短路径问题,而且可以用来判断是否有负权回路。它也有明显
的缺点,它的时间复杂度 O(N*E) (N是点数,E是边数)普遍是要高于Dijkstra算法O(N²)的。像这里
如果我们使用邻接矩阵实现,那么遍历所有边的数量的时间复杂度就是O(N^3),这里也可以看出
来Bellman-Ford就是一种暴力求解更新。
- 优化:第一个轮次进行更新的边才会影响其他路径,只需要拿这些边去更新其他边(SPFA优化,队列优化)。但是对于时间复杂度并没有得出一个结论。
最坏情况O(N^3),最好情况O(N^2)
。 - 循环提前跳出优化
可以用SPFA优化,采用队列优化。
用 (i,j)存在权值的情况去更新其他边。由于每次遍历整个邻接矩阵就好了。
但是需要遍历n回,n是点的个数。
注意每一个点dist[到自己] = 0,一开始可以设置为无穷大,根据给的顶点进行更新就可以了。dist[index] = W();//自己到自己的权值为0
。
a->b假设需要更新,最多用len条边更新。
// 时间复杂度:O(N^3) 空间复杂度:O(N)
bool BellmanFord(const V& src, vector<W>& dist, vector<int>& pPath)
{
//从src更新其他节点,首先看src能够更新哪些节点
int len = _vertex.size();
dist.resize(len, MaxSize);
pPath.resize(len,-1);
int index = VertexToIndex(src);
dist[index] = W();//自己到自己的权值为0
//如果只更新一次,可能出现后续的节点影响了前面的节点的情况,一个节点最多经过k-2个节点,需要更新k次
for (int k = 0; k < len; ++k)
{
bool flag = true;//如果不需要跟新就可以跳出来
cout << "更新第:" << k << "轮" << endl;
for (int i = 0; i < len; ++i)
{
//更新已经能够到达的路径周围的节点
if (dist[i] == MaxSize)
continue;
//这里的i就是需要更新周围节点
for (int j = 0; j < len; ++j)
{
if (i != j && _weightmatrix[i][j] != MaxSize)
{
//表示这个点是有联系的
if (dist[j] > dist[i] + _weightmatrix[i][j])
{
cout << _vertex[i] << "->" << _vertex[j] << ":" << _weightmatrix[i][j] << endl;
flag = false;
//就可以更新
dist[j] = dist[i] + _weightmatrix[i][j];
//更新父路径实际上是从i这个节点的父路径来的,观察这里
pPath[j] = i;
}
}
}
}
if (flag)
break;
}
//检测是否有负权回路,检测方法为再更新一次即可。
bool flag = false;
for (int i = 0; i < len; ++i)
{
//更新已经能够到达的路径周围的节点
if (dist[i] == MaxSize)
continue;
//这里的i就是需要更新周围节点
for (int j = 0; j < len; ++j)
{
if (i != j && _weightmatrix[i][j] != MaxSize)
{
//表示这个点是有联系的
if (dist[j] > dist[i] + _weightmatrix[i][j])
{
flag = true;
break;
}
}
}
if (flag)
return false;//有负权回路
return true;
}
}
多源最短路径:FloydWarshall
dist数组和pPath数组得是二维的,就能记录任意两个点。结构和前面的有所不同。 距离矩阵能存储任意两个点的距离。
dist数组在前面都是以一个特定的点出发,是我们传参决定的。
FloydWarshall 是取中间点进行更新。本质是用了动态规划。
不需要像Dijstra,Bellman-Ford一个在距离矩阵取,一个在邻接矩阵取。而是都在距离矩阵dist取即可。
三维当中的k是经过多少个点的意思。情况就是上述的两种。他虽然是三维空间来表示,但是实现的时候由于中间节点k也可以查二维矩阵就可以了,所以就用二维的矩阵表示。
若是不带负权回路,用Dijstra遍历每一个顶点也是O(N^3),但是FloydWarshall是可以解决负权回路的。
而Bellman-Ford算法的话效率太低了。所以多源最短路径横空出世。
Floyd-Warshall算法是解决任意两点间的最短路径的一种算法。
Floyd算法考虑的是一条最短路径的中间节点,即简单路径p={v1,v2,…,vn}上除v1和vn的任意节
点。
设k是p的一个中间节点,那么从i到j的最短路径p就被分成i到k和k到j的两段最短路径p1,p2。p1
是从i到k且中间节点属于{1,2,…,k-1}取得的一条最短路径。p2是从k到j且中间节点属于{1, 2,…,k-1}取得的一条最短路径。
左边是距离矩阵,右边是父路径矩阵。
- 初始化
- 直接相连的边更新,自己到自己初始化成0
- 注意父路径
vvpPath[i][j]
若是经过了k,则是k,所以需要改变,并且即使是vvDist[i][j] + vvDist[k][j]途中经过了其他的节点
,因为我们要找的与j相连的上一个邻接点。k->j不一定是直接相连,若是则是k,否则就是其他。 - 由于起始点和终点都有可能变,所以中间节点都是需要遍历的,也就是
for(int k = 0;k < len;++k)
。如同i->j最多经过n-2个点。
void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath)
{
vvDist = _weightmatrix;
int len = vvDist.size();
//vvDist是权值矩阵
//vvpPath用来表示到达i,j位置的父亲
vvpPath.resize(len, vector<int>(len, -1));
// 直接相连的边更新一下
for (size_t i = 0; i < len; ++i)
{
for (size_t j = 0; j < len; ++j)
{
if (_weightmatrix[i][j] != MaxSize)
{
vvpPath[i][j] = i;
}
if (i == j)
{
vvDist[i][j] = W();
}
}
}
for (int k = 0; k < len; ++k)
{
for (int i = 0; i < len; ++i)
{
//每次更新从i出去的点
for (int j = 0; j < len; ++j)
{
// k 作为的中间点尝试去更新i->j的路径
if (vvDist[i][k] != MaxSize && vvDist[k][j] != MaxSize
&& vvDist[i][k] + vvDist[k][j] < vvDist[i][j])
{
vvDist[i][j] = vvDist[i][k] + vvDist[k][j];
vvpPath[i][j] = vvpPath[k][j];
}
}
}
}
// 打印权值和路径矩阵观察数据
for (size_t i = 0; i < len; ++i)
{
for (size_t j = 0; j < len; ++j)
{
if (vvDist[i][j] == MaxSize)
{
//cout << "*" << " ";
printf("%3c", '*');
}
else
{
//cout << vvDist[i][j] << " ";
printf("%3d", vvDist[i][j]);
}
}
cout << endl;
}
cout << endl;
for (size_t i = 0; i < len; ++i)
{
for (size_t j = 0; j < len; ++j)
{
//cout << vvParentPath[i][j] << " ";
printf("%3d", vvpPath[i][j]);
}
cout << endl;
}
cout << "=================================" << endl;
}
总结
图论到此为止~
- 喜欢就收藏
- 认同就点赞
- 支持就关注
- 疑问就评论