目录
一、图的基本概念
二、图的存储结构
2.1 邻接矩阵
2.2 邻接表
三、图的遍历
3.1 广度优先遍历
3.2 深度优先遍历
四、最小生成树
4.1 Kruskal算法
4.2 Prim算法
五、最短路径
5.1 单源最短路径-Dijkstra算法
5.2 单源最短路径-Bellman-Ford算法
5.3 多源最短路径-Floyd-Warshall算法
图是由顶点集合和边的集合组成的一种数据结构,记作
有向图与无向图
完全图
邻接顶点
顶点的度
路径与路径长度
带权图示例:
简单路径与回路
子图
设图 和图 ,若 且 ,则称 是 的子图
连通图和强连通图
生成树与最小生成树
图的相关应用场景
图常见的表示场景如下:
关于有向图和无向图:
图的其他相关作用:
图与树的联系与区别
图由顶点和边组成,存储图的本质就是将图中的顶点和边存储起来
邻接矩阵存储图的方式
说明:
邻接矩阵的优缺点
优点:
缺点:
邻接矩阵的实现
成员变量:
实现原理:
#include
#include
#include
using namespace std;
namespace matrix
{
template
class Graph
{
public:
Graph() = default;
Graph(const V* vertex, size_t size)
:_vertexs(vertex, vertex + size), _matrix(size, vector(size, MAX_W))
{
for (int i = 0; i < size; ++i)
_indexMap[vertex[i]] = i;
}
//获取顶点对应的下标
int GetVertexIndex(const V& v)
{
auto it = _indexMap.find(v);
if (it != _indexMap.end()) //顶点存在
return it->second;
else { //顶点不存在
throw invalid_argument("顶点不存在");
return -1;
}
}
//添加边
void AddEdge(const V& source, const V& destion, const W& weight)
{
int sourceIndex = GetVertexIndex(source), destionIndex = GetVertexIndex(destion);
_matrix[sourceIndex][destionIndex] = weight;
if (Direction == false)
_matrix[destionIndex][sourceIndex] = weight;
}
//打印顶点集合和邻接矩阵
void Print()
{
int size = _vertexs.size();
//打印顶点集合
for (int i = 0; i < size; i++)
cout << "[" << i << "]->" << _vertexs[i] << endl;
cout << endl;
//打印邻接矩阵
cout << " ";
for (int i = 0; i < size; i++)
printf("%4d", i);
cout << endl;
for (int i = 0; i < size; i++)
{
cout << i << " "; //竖下标
for (int j = 0; j < size; j++)
{
if (_matrix[i][j] == MAX_W) printf("%4c", '*');
else printf("%4d", _matrix[i][j]);
}
cout << endl;
}
cout << endl;
}
private:
vector _vertexs; //顶点集合
unordered_map _indexMap; //顶点映射下标
vector> _matrix; //邻接矩阵
};
}
邻接表存储图的方式
说明:
邻接表的优缺点
优点:
缺点:
邻接表的实现
链表结点成员变量:
namespace link_table
{
//链表结点
template
struct Edge
{
Edge(int dstI, const W& w) : _destionIndex(dstI), _weight(w), _next(nullptr) {}
//int _sourceIndex; //可选
int _destionIndex;
W _weight;
Edge* _next;
};
}
邻接表成员变量:
实现原理:
namespace link_table
{
template
class Graph
{
typedef Edge Edge;
public:
Graph() = default;
Graph(const V* vertexs, size_t size)
:_vertexs(vertexs, vertexs + size), _linkTable(size, nullptr)
{
for (int i = 0; i < size; ++i)
_indexMap[vertexs[i]] = i;
}
int GetVertexIndex(const V& vertex)
{
auto it = _indexMap.find(vertex);
if (it != _indexMap.end()) return it->second;
else {
throw invalid_argument("不存在的顶点");
return -1;
}
}
void AddEdge(const V& src, const V& dst, const W& w)
{
int srcIndex = GetVertexIndex(src), dstIndex = GetVertexIndex(dst);
Edge* newEdge = new Edge(dstIndex, w);
newEdge->_next = _linkTable[srcIndex];
_linkTable[srcIndex] = newEdge;
if (Direction == false)
{
Edge* newEdge = new Edge(srcIndex, w);
newEdge->_next = _linkTable[dstIndex];
_linkTable[dstIndex] = newEdge;
}
}
void Print()
{
int size = _vertexs.size();
//打印顶点集合
for (int i = 0; i < size; i++)
cout << "[" << i << "]->" << _vertexs[i] << " ";
cout << endl << endl;
//打印邻接表
for (int i = 0; i < size; i++)
{
Edge* cur = _linkTable[i];
cout << "[" << i << ":" << _vertexs[i] << "]->";
while (cur) {
cout << "[" << cur->_destionIndex << ":" << _vertexs[cur->_destionIndex] << ":" << cur->_weight << "]->";
cur = cur->_next;
}
cout << "nullptr" << endl;
}
}
private:
vector _vertexs; //顶点集合
unordered_map _indexMap; //映射关系
vector _linkTable; //出边表
};
}
图的遍历指的是遍历图中的顶点,主要有广度优先遍历和深度优先遍历两种方式
广度优先遍历又称BFS,类似于二叉树的层序遍历,从起始顶点开始一层一层向外进行遍历
实现原理:
void BFS(const V& source)
{
int sourceIndex = GetVertexIndex(source);
queue qe;
vector visited(_vertexs.size(), false);
qe.push(sourceIndex);
visited[sourceIndex] = true;
while (!qe.empty())
{
int front = qe.front();
qe.pop();
cout << _vertexs[front] << " ";
for (int j = 0; j < _vertexs.size(); ++j)
{
if (_matrix[front][j] != MAX_W && visited[j] == false)
{
qe.push(j);
visited[j] = true;
}
}
}
cout << endl;
}
说明:
深度优先遍历又称DFS,类似于二叉树的先序遍历,从起始顶点开始不断对顶点进行深入遍历
实现原理:
void _DFS(int srcIndex, vector& visvited)
{
cout << _vertexs[srcIndex] << " "; // 访问
visvited[srcIndex] = true;
for (int j = 0; j < _vertexs.size(); ++j)
if (_matrix[srcIndex][j] != MAX_W && visvited[j] == false)
_DFS(j, visvited);
}
void DFS(const V& source)
{
int sourceIndex = GetVertexIndex(source);
vector visvited(_vertexs.size(), false);
_DFS(sourceIndex, visvited);
cout << endl;
}
若所给图不是一个连通图,那么从一个顶点开始进行深度优先遍历,无法遍历完图中的所有顶点。这时可以遍历标记数组,查看哪些顶点还没有被访问过,对于没有被访问过的顶点,则从该顶点处继续进行深度优先遍历,直到图中所有的顶点都被访问过
认识最小生成树
说明:
构成最小生成树的准则
构造最小生成树的算法有 Kruskal(克鲁斯卡尔)算法和 Prim(普里姆)算法,这两个算法都采用了逐步求解的贪心策略
基本思想
具体实现
//添加边
void _AddEdge(int& srcIndex, int& dstIndex, const W& weight)
{
_matrix[srcIndex][dstIndex] = weight;
if (Direction == false)
_matrix[dstIndex][srcIndex] = weight;
}
void AddEdge(const V& source, const V& destion, const W& weight)
{
int sourceIndex = GetVertexIndex(source), destionIndex = GetVertexIndex(destion);
_AddEdge(sourceIndex, destionIndex, weight);
}
struct Edge
{
Edge(int srcIndex, int dstIndex, const W& weight)
:_sourceIndex(srcIndex), _destionIndex(dstIndex), _weight(weight) {}
bool operator>(const Edge& edge)const {
return _weight > edge._weight;
}
int _sourceIndex;
int _destionIndex;
W _weight;
};
W Kruskal(Graph& minTree)
{
int size = _vertexs.size();
//最小生成树初始化
minTree._vertexs = _vertexs;
minTree._indexMap = _indexMap;
minTree._matrix.resize(size, vector (size, MAX_W));
priority_queue, greater> minHeap;//小堆
for (int i = 0; i < size; ++i)
for (int j = 0; j < i; ++j)//只遍历矩阵一半,避免添加重复的边
if (_matrix[i][j] != MAX_W)
minHeap.push(Edge(i, j, _matrix[i][j]));
UnionFindSet ufs(size); //size个顶点的并查集, 用于判环
int count = 0; //已选边的数量
W totalWeight = W(); //最小生成树的总权值
while (!minHeap.empty() && count < size - 1)
{
//获取此时最小的边
Edge minEdge = minHeap.top();
minHeap.pop();
int srcI = minEdge._sourceIndex, dstI = minEdge._destionIndex;
W weight = minEdge._weight;
if (!ufs.IsInSet(srcI, dstI)) //边的源顶点和目标顶点不在同一个集合, 即无环
{
minTree._AddEdge(srcI, dstI, weight);
ufs.Union(srcI, dstI);
++count;
totalWeight += weight;
cout << "选边: " << _vertexs[srcI] << "->" << _vertexs[dstI] << ":" << weight << endl;
}
}
if (count == size - 1) {
cout << "构建最小生成树成功" << endl;
return totalWeight;
}
else {
cout << "无法构成最小生成树" << endl;
return W();
}
}
基本思想
具体实现
W Prim(Graph& minTree, const V& start)
{
int size = _vertexs.size();
//最小生成树初始化
minTree._vertexs = _vertexs;
minTree._indexMap = _indexMap;
minTree._matrix.resize(size, vector(size, MAX_W));
int startIndex = GetVertexIndex(start);
vector forest(size, false);
forest[startIndex] = true;
priority_queue, greater> minHeap;
//将初始顶点连接的边加入堆中
for (int j = 0; j < size; ++j)
if (_matrix[startIndex][j] != MAX_W)
minHeap.push(Edge(startIndex, j, _matrix[startIndex][j]));
int count = 0;
W totalWeight = W();
while (!minHeap.empty() && count < size - 1)
{
Edge minEdge = minHeap.top();
minHeap.pop();
int srcIndex = minEdge._sourceIndex, dstIndex = minEdge._destionIndex;
W weight = minEdge._weight;
if (forest[dstIndex] == false) // 边的目标顶点还没有被加入到forest集合中
{
//将目标顶点连接出去的边加入到优先级队列中
for (int j = 0; j < size; ++j)
if (_matrix[dstIndex][j] != MAX_W)
minHeap.push(Edge(dstIndex, j, _matrix[dstIndex][j]));
minTree._AddEdge(srcIndex, dstIndex, weight);
forest[dstIndex] = true;
++count;
totalWeight += weight;
cout << "选边: " << _vertexs[srcIndex] << "->" << _vertexs[dstIndex] << ":" << weight << endl;
}
}
if (count == size - 1) {
cout << "构建最小生成树成功" << endl;
return totalWeight;
}
else {
cout << "无法构成最小生成树" << endl;
return W();
}
}
注意:使用前提,图中所有边的权值非负
迪杰斯特拉算法基本思想
具体实现
void Dijkstra(const V& source, vector& dist, vector& parentPath)
{
int size = _vertexs.size();
int srcIndex = GetVertexIndex(source);
dist.resize(size, MAX_W);
dist[srcIndex] = W();
parentPath.resize(size, -1);
vector S(size, false);//已确定最短路径的顶点的集合
for (int i = 0; i < size; ++i)//目标是将Q集合中的所有顶点都加入S集合
{
//从集合Q中选出一个估计值最小的顶点
W minW = MAX_W; //记录最小估计值
int u = -1; //记录拥有最小估计值的顶点
for(int j = 0; j < size; ++j)
if (S[j] == false && dist[j] < minW) {
minW = dist[j];
u = j;
}
//将选出的顶点加入S集合
S[u] = true;
//对u连接出去的顶点进行松弛更新
for (int v = 0; v < size; ++v)
{
if (S[v] == false && _matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v])
{
dist[v] = dist[u] + _matrix[u][v];
parentPath[v] = u;
}
}
}
}
//打印最短路径及路径权值
void PrintShortPath(const V& src, const vector& dist, const vector& parentPath)
{
int n = _vertexs.size();
int srci = GetVertexIndex(src); //获取源顶点的下标
for (int i = 0; i < n; i++)
{
vector path;
int cur = i;
while (cur != -1) { //源顶点的前驱顶点为-1
path.push_back(cur);
cur = parentPath[cur];
}
reverse(path.begin(), path.end()); //逆置
for (int j = 0; j < path.size(); j++) {
cout << _vertexs[path[j]] << "->";
}
cout << "路径权值: " << dist[i] << "" << endl;
}
}
算法原理
贝尔曼福特算法基本思想
具体实现
bool BellmanFord(const V& source, vector& dist, vector& parentPath)
{
int size = _vertexs.size();
int srcIndex = GetVertexIndex(source);
dist.resize(size, MAX_W);
dist[srcIndex] = W();
parentPath.resize(size, -1);
for (int k = 0; k < size - 1; ++k) // 最多更新size - 1轮
{
bool update = false;//记录本轮是否更新过
for (int i = 0; i < size; ++i)
{
for (int j = 0; j < size; ++j)
{
if (_matrix[i][j] != MAX_W && dist[i] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
{
dist[j] = dist[i] + _matrix[i][j];
parentPath[j] = i;
update = true;
}
}
}
if (update == false) break;
}
for (int i = 0; i < size; ++i)
for (int j = 0; j < size; ++j)
if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
return false; //带有负权回路的图无法求出最短路径
return true;
}
为什么最多进行 n-1 轮松弛更新?
从一个顶点到另一个顶点的最短路径中不能包含回路:
在每一轮松弛过程中,后面路径的更新可能会影响到前面已经更新过的路径,比如使得前面已经更新过的路径的长度可以变得更短,或者使得某些源顶点之前不可达的顶点变得可达,但每一轮松弛至少能确定最短路径中的一条边,若图中有 个顶点,那么两个顶点之间的最短路径最多有 条边,因此最多需要进行 次松弛更新。
下图中,顶点 、、、、 的下标分别是0、1、2、3、4,现在要计算以顶点 为源顶点的单源最短路径
对于上述图来说,Bellman-Ford算法在第一轮松弛的时候只能更新出这条边,在第二轮的时候只能更新出 ,以此类推,最终就会进行4轮松弛更新
弗洛伊德算法基本思想
具体实现
void FloydWarshall(vector>& vvDist, vector>& vvParentPath)
{
int size = _vertexs.size();
vvDist.resize(size, vector(size, MAX_W));
vvParentPath.resize(size, vector(size, -1));
//根据邻接矩阵初始化直接相连的顶点
for (int i = 0; i < size; ++i)
{
for (int j = 0; j < size; ++j)
{
if (_matrix[i][j] != MAX_W)//i -> j 有边
{
vvDist[i][j] = _matrix[i][j];
vvParentPath[i][j] = i;
}
if (i == j) vvDist[i][j] = W();
}
}
for (int k = 0; k < size; ++k)//依次选取各个顶点作为i->j路径的中间顶点
for (int i = 0; i < size; ++i)
for (int j = 0; j < size; ++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];
vvParentPath[i][j] = vvParentPath[k][j];
}
}