【数据结构---图的原理与最小生成树算法,单源最短路径算法】
数据结构之 图
- 一.图的概念
- 二.图的存储结构
- 1.邻接矩阵
- 2. 邻接表
- 三. 图的遍历
- 1.深度优先遍历
- 2. 广度优先遍历
- 四.最小生成树
- 1.Kruskal算法
- 2.Prim算法
- 五.单源最短路径
- 1. Dijkstra算法
- 2.Bellman-Ford算法
一.图的概念
图是由顶点集合及顶点间的关系组成的一种数据结构:G = (V, E),其中: 顶点集合V = {x|x属于某个数据对象集}是有穷非空集合;E = {(x,y)|x,y属于V}或者E = {<x, y>|x,y属于V && Path(x, y)}是顶点间关系的有穷集合,也叫做边的集合。
图的相关术语:
-
顶点和边
:图中结点称为顶点,第i个顶点记作vi。两个顶点vi和vj相关联称作顶点vi和顶点vj之间有一条边,图中的第k条边记作ek,ek = (vi,vj)或<vi,vj> -
有向图和无向图
:在有向图中,顶点对<x, y>是有序的,顶点对<x,y>称为顶点x到顶点y的一条边(弧),<x, y>和<y, x>是两条不同的边,比如下图G3和G4为有向图。在无向图中,顶点对(x, y)是无序的,顶点对(x,y)称为顶点x和顶点y相关联的一条边,这条边没有特定方向,(x, y)和(y,x)是同一条边,比如下图G2为无向图。注意:无向边(x, y)等于有向边<x, y>和<y, x>。
-
完全图
:在有n个顶点的无向图中,若有n * (n-1)/2条边,即任意两个顶点之间有且仅有一条边,则称此图为无向完全图,比如上图G1;在n个顶点的有向图中,若有n * (n-1)条边,即任意两个顶点之间有且仅有方向相反的边,则称此图为有向完全图 -
顶点的度
:顶点v的度是指与它相关联的边的条数,记作deg(v)。在有向图中,顶点的度等于该顶点的入度与出度之和,其中顶点v的入度是以v为终点的有向边的条数,记作indev(v);顶点v的出度是以v为起始点的有向边的条数,记作outdev(v)。因此:dev(v) = indev(v) + outdev(v)。注意:对于无向图,顶点的度等于该顶点的入度和出度,即dev(v) = indev(v) = outdev(v)。 -
简单路径与回路
:若路径上各顶点v1,v2,v3,…,vm均不重复,则称这样的路径为简单路径。若路径上第一个顶点v1和最后一个顶点vm重合,则称这样的路径为回路或环。
-
子图
:设图G = {V, E}和图G1 = {V1,E1},若V1属于V且E1属于E,则称G1是G的子图。
-
连通图
:在无向图中,若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图 -
生成树
:一个连通图的最小连通子图称作该图的生成树。有n个顶点的连通图的生成树有n个顶点和n-1条边。
二.图的存储结构
因为图中既有节点,又有边(节点与节点之间的关系),因此,在图的存储中,只需要保存:节点和边关系即可。节点保存比较简单,只需要一段连续空间即可,那边关系该怎么保存呢?
1.邻接矩阵
因为节点与节点之间的关系就是连通与否,即为0或者1,因此邻接矩阵(二维数组)即是:先用一个数组将定点保存,然后采用矩阵来表示节点与节点之间的关系。
细节
:
- 无向图的邻接矩阵是对称的,第i行(列)元素之和,就是顶点i的度。有向图的邻接矩阵则不一定是对称的,第i行(列)元素之后就是顶点i 的出(入)度
- 如果边带有权值,并且两个节点之间是连通的,上图中的边的关系就用权值代替,如果两个顶点不通,则使用无穷大代替
代码实现:
#pragma once
#include <iostream>
#include <vector>
#include <map>
#include <queue>template <class V, class W, W MAX_W = INT32_MAX, bool Direction = false>
class Graph
{
public:typedef Graph<V, W, MAX_W, Direction> Self;Graph(const V *vetexs, size_t n){_vertexs.reserve(n);_matrix.reserve(n);for (int i = 0; i < n; i++){_vIndexMap.insert(std::make_pair(vetexs[i], i));_vertexs.push_back(vetexs[i]);_matrix[i].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);}private:std::map<V, size_t> _vIndexMap; // 映射std::vector<V> _vertexs; // 顶点的集合std::vector<std::vector<W>> _matrix; // 邻接矩阵
};
2. 邻接表
代码实现:
#pragma once
#include <iostream>
#include <vector>
#include <map>
#include <queue>
#include <string>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>
class Graph
{typedef LinkEdge<W> Edge;public:Graph(const V *vetexs, size_t n){_vertexs.reserve(n);_linkTable.resize(n, nullptr);for (int i = 0; i < n; i++){_vertexs.push_back(vertexs[i]);_vIndexMap[vertexs[i]] = i;}}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 1Edge *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:std::map<V, int> _vIndexMap;std::vector<V> _vertexs;std::vector<Edge> _linkTable;
};
三. 图的遍历
1.深度优先遍历
代码实现:
void _DFS(int index, vector<bool> &visited){if (!visited[index]){cout << _v[index] << " ";visited[index] = true;LinkEdge *pCur = _linkEdges[index];while (pCur){_DFS(pCur->_dst, visited);pCur = pCur->_pNext;}}}void DFS(const V &v){cout << "DFS:";vector<bool> visited(_v.size(), false);_DFS(GetIndexOfV(v), visited);for (size_t index = 0; index < _v.size(); ++index)_DFS(index, visited);cout << endl;}
2. 广度优先遍历
广度优先遍历就是一层一层向外扩展。
思路就是借助队列,一个节点出队列时将他的连接点都加入队列。
代码实现:
void BFS(const V &src){size_t srcindex = GetVertexIndex(src);vector<bool> visited;visited.resize(_vertexs.size(), false);queue<int> q;q.push(srcindex);visited[srcindex] = true;size_t d = 1;size_t dSize = 1;while (!q.empty()){printf("%s的%d度好友:", src.c_str(), d);while (dSize--){size_t front = q.front();q.pop();for (size_t i = 0; i < _vertexs.size(); ++i){if (visited[i] == false && _matrix[front][i] != MAX_W){printf("[%d:%s] ", i, _vertexs[i].c_str());visited[i] = true;q.push(i);}}}cout << endl;dSize = q.size();++d;}cout << endl;}
四.最小生成树
1.Kruskal算法
任给一个有n个顶点的连通网络N={V,E}, 首先构造一个由这n个顶点组成、不含任何边的图G={V,NULL},其中每个顶点自成一个连通分
量,其次不断从E中取出权值最小的一条边(若有多条任取其一),若该边的两个顶点来自不同的连通分量,则将此边加入到G中。如此重复,直到所有顶点在同一个连通分量上为止。
核心:每次迭代时,选出一条具有最小权值,且两端点不在同一连通分量上的边,加入生成树。
代码实现:
W Kruskal(Self &minTree){minTree._vertexs = _vertexs;minTree._vIndexMap = _vIndexMap;minTree._matrix.resize(_vertexs.size());for (auto &e : minTree._matrix){e.resize(_vertexs.size(), MAX_W);}std::priority_queue<Edge, std::vector<Edge>, std::greater<Edge>> pq;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){pq.push(Edge(i, j, _matrix[i][j]));}}}W total = W();// 贪心算法,从最小的边开始选size_t i = 1;UnionFindSet ufs(_vertexs.size());while (i < _vertexs.size() && !pq.empty()){Edge min = pq.top();pq.pop();// 边不在一个集合,说明不会构成环,则添加到最小生成树if (ufs.FindRoot(min._srci) != ufs.FindRoot(min._dsti)){// cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] <<":" << _matrix[min._srci][min._dsti] << endl;minTree._AddEdge(min._srci, min._dsti, min._w);total += min._w;ufs.Union(min._srci, min._dsti);++i;}}if (i == _vertexs.size()){return total;}else{return W();}}
2.Prim算法
代码实现:
W Prim(Self &minTree, const V &src){minTree._vertexs = _vertexs;minTree._vIndexMap = _vIndexMap;minTree._matrix.resize(_vertexs.size());for (auto &e : minTree._matrix){e.resize(_vertexs.size(), MAX_W);}size_t srci = GetVertexIndex(src);std::set<size_t> inSet;inSet.insert(srci);priority_queue<Edge, vector<Edge>, greater<Edge>> pq;for (size_t i = 0; i < _vertexs.size(); ++i){if (_matrix[srci][i] != MAX_W){pq.push(Edge(srci, i, _matrix[srci][i]));}}W total = W();while (inSet.size() < _vertexs.size() && !pq.empty()){Edge min = pq.top();pq.pop();// 防止环的问题if (inSet.find(min._srci) == inSet.end() ||inSet.find(min._dsti) == inSet.end()){// cout << _vertexs[min._srci] << "-" <<_vertexs[min._dsti] << ":" << _matrix[min._srci][min._dsti] << endl;minTree._AddEdge(min._srci, min._dsti, min._w);total += min._w;// 新入顶点的连接边进入队列for (size_t i = 0; i < _vertexs.size(); ++i){if (_matrix[min._dsti][i] != MAX_W && inSet.find(i) == inSet.end()){pq.push(Edge(min._dsti, i, _matrix[min._dsti][i]));}}inSet.insert(min._dsti);}}if (inSet.size() == _vertexs.size()){return total;}else{return W();}}
五.单源最短路径
1. Dijkstra算法
单源最短路径问题:给定一个图G = ( V , E ) G=(V,E)G=(V,E),求源结点s ∈ V s∈Vs∈V到图中每个结点v ∈
V
v∈Vv∈V的最短路径。Dijkstra算法就适用于解决带权重的有向图上的单源最短路径问题,同时算法要求图中所有边的权重非负。一般在求解最短路径的时候都是已知一个起点和一个终点,所以使用Dijkstra算法求解过后也就得到了所需起点到终点的最短路径。
针对一个带权有向图G,将所有结点分为两组S和Q,S是已经确定最短路径的结点集合,在初始时为空(初始时就可以将源节点s放入,毕竟源节点到自己的代价是0),Q 为其余未确定最短路径的结点集合,每次从Q 中找出一个起点到该结点代价最小的结点u ,将u 从Q 中移出,并放入S中,对u 的每一个相邻结点v 进行松弛操作。松弛即对每一个相邻结点v ,判断源节点s到结点u 的代价与u 到v 的代价之和是否比原来s 到v 的代价更小,若代价比原来小则将s 到v 的代价更新为s 到u 与u 到v 的代价之和,否则维持原样。如此一直循环直至集合Q 为空,即所有节点都已经查找过一遍并确定了最短路径,至于一些起点到达不了的结点在算法循环后其代价仍为初始设定的值,不发生变化。Dijkstra算法每次都是选择V-S中最小的路径节点来进行更新,并加入S中,所 以该算法使用的是贪心策略。
Dijkstra算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路径的最短路径。
代码实现:
void Dijkstra(const V &src, vector<W> &dist, vector<int> &parentPath){size_t N = _vertexs.size();size_t srci = GetVertexIndex(src);// vector<W> dist,记录srci-其他顶点最短路径权值数组dist.resize(N, MAX_W);// vector<int> parentPath 记录srci-其他顶点最短路径父顶点数组parentPath.resize(N, -1);// 标记是否找到最短路径的顶点集合Svector<bool> S;S.resize(N, false);// srci的权值给一个最小值,方便贪心第一次找到这个节点dist[srci] = W();// N个顶点更新N次for (size_t i = 0; i < N; ++i){// 贪心算法:srci到不在S中路径最短的那个顶点uW min = MAX_W;size_t u = srci;for (size_t j = 0; j < N; ++j){if (S[j] == false && dist[j] < min){min = dist[j];u = j;}}S[u] = true;// 松弛算法:更新一遍u连接的所有边,看是否能更新出更短连接路径for (size_t k = 0; k < N; ++k){// 如果srci->u + u->k 比 srci->k更短 则进行更新if (S[k] == false && _matrix[u][k] != MAX_W && dist[u] + _matrix[u][k] < dist[k]){dist[k] = dist[u] + _matrix[u][k];parentPath[k] = u;}}}}
2.Bellman-Ford算法
Dijkstra算法只能用来解决
正权图
的单源最短路径问题,但有些题目会出现负权图。这时这个算法就不能帮助我们解决问题了,而bellman—ford算法可以解决负权图的单源最短路径问题。它的优点是可以解决有负权边的单源最短路径问题,而且可以用来判断是否有负权回路。它也有明显的缺点,它的时间复杂度 O(N*E)(N是点数,E是边数)普遍是要高于Dijkstra算法O(N²)的。像这里如果我们使用邻接矩阵实现,那么遍历所有边的数量的时间复杂度就是O(N^3),这里也可以看出来Bellman-Ford就是一种暴力求解更新
代码实现:
bool BellmanFord(const V &src, vector<W> &dist, vector<int> &parentPath){size_t N = _vertexs.size();size_t srci = GetVertexIndex(src);// vector<W> dist,记录srci-其他顶点最短路径权值数组dist.resize(N, MAX_W);// vector<int> parentPath 记录srci-其他顶点最短路径父顶点数组parentPath.resize(N, -1);// 先更新srci->srci为最小值dist[srci] = W();for (size_t k = 0; k < N - 1; ++k){bool exchange = false;for (size_t i = 0; i < N; ++i){for (size_t j = 0; j < N; ++j){// srci->i + i->j < srci->j 则更新路径及权值if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]){dist[j] = dist[i] + _matrix[i][j];parentPath[j] = i;exchange = true;}}}if (exchange == false)break;}for (size_t i = 0; i < N; ++i){for (size_t j = 0; j < N; ++j){// 检查有没有负权回路if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]){return false;}}}return true;}