目录
一.图的基本概念
二.图的存储结构
1.邻接矩阵
(1)无向图、有向图矩阵存储
(2)实现:
2.邻接表
(1)无向图邻接表存储
(2)有向图邻接表存储
(3)实现
三.图的遍历
1.广度优先遍历
2.深度优先遍历
四.最小生成树
1.Kruskal(克鲁斯卡尔算法)
2.Prim(普里姆算法)
五.最短路径
1.单源最短路径
(1)Dijkstra(迪杰斯特拉算法)
(2)Bellman-Ford(贝尔曼-福特算法)
2.多源最短路径
(1)Floyd-Warshall(弗洛伊德算法)
六.总代码
前言:图是一个非常抽象的数据结构,之前学过的二叉树也属于图。想要理解图,就要一步一步的去分析理解。
一.图的基本概念
(1)图:图是由顶点集合及顶点间的关系组成的一种数据结构:G = (V, E),其中V是顶点集合,V = {x | x属于某个数据对象集} 是有穷非空集合;E是边的集合,E = {(x, y) | x, y属于V} 或者 E = {<x, y> | x, y属于V && Path(x, y)}是顶点间关系的有穷集合。(x, y)表示x到y的一条双向通路,即(x, y)是无方向的;Path(x, y)表示从x到y的一条单向通路,即Path(x, y)是有方向的。
(2)顶点和边:图的结点称为顶点,第i个顶点记作vi。两个顶点vi和vj相关联称作顶点vi和顶点vj之间有一条边,图中的第k条边记作ek,ek = (vi, vj) 或 <vi, vj>
(3)有向图和无向图:在有向图中,顶点对<x, y>是有序的,顶点对<x, y>称为顶点x到顶点y的一条边(弧),<x, y>和<y, x>是两条不同的边。在无向图中,顶点对(x, y)是无序的,顶点对(x, y)称为顶点x和顶点y相关联的一条边,这条边没有特定方向,(x, y)和(y, x)是同一条边。【无向边(x, y)等于有向边<x, y>和<x, y>】。
(4)完全图:在有n个顶点的无向图中,若有n * (n-1) / 2条边,即任意两个顶点之间有且仅有一条边,则称此图为无向完全图;在有n个顶点的有向图中,若有n * (n-1)条边,即任意两个顶点之间有且仅有方向相反的边,则称此图为有向完全图。
(5)邻接顶点:在无向图中G中,若(u, v)是E(G)中的一条边,则称u和v互为邻接顶点,并称边(u, v)依附于顶点u和v;在有向图G中,若<u, v>是E(G)中的一条边,则称顶点u邻接到v,顶点v邻接自顶点u,并称边<u, v>与顶点u和顶点v相关联。
(6)顶点的度:顶点v的度是指与它相关联的边的条数,记作deg(v)。在有向图中,顶点的度等于该顶点的入度与出度之和,其中顶点v的入度是以v为终点的有向边的条数,记作indev(v);顶点v的出度是以v为起始点的有向边的条数,记作outdev(v)。因此:dev(v) = indev(v) + outdev(v)。注意:对于无向图,顶点的度等于该顶点的入度和出度,即dev(v) = indev(v) = outdev(v)。
(7)路径:在图G = (V, E)中,若从顶点vi出发有一组边使其可到达顶点vj,则称顶点vi到顶点vj的顶点序列为从顶点vi到顶点vj的路径。
(8)路径长度:对于不带权的图,一条路径的路径长度是指该路径上的边的条数;对于带权的图,一条路径的路径长度是指该路径上各个边权值的总和。
(9)简单路径:若路径上各顶点v1,v2,v3,…,vm均不重复,则称这样的路径为简单路径
(10)回路:若路径上第一个顶点v1和最后一个顶点vm重合,则称这样的路径为回路或环。
(11)子图:设图G = {V, E}和图G1 = {V1,E1},若V1属于V且E1属于E,则称G1是G的子图。
(12)连通图:在无向图中,若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图。
(13)强连通图:在有向图中,若在每一对顶点vi和vj之间都存在一条从vi到vj的路径,也存在一条从vj到vi的路径,则称此图是强连通图。
(14)生成树:一个连通图的最小连通子图称作该图的生成树。有n个顶点的连通图的生成树有n个顶点和n-1条边。
二.图的存储结构
图既有结点又有边,因此在图的存储中,需要保存:结点和边的关系。
1.邻接矩阵
邻接矩阵(二维数组):先用一个数组将定点保存,然后采用矩阵来表示结点与结点之间的关系。
(1)无向图、有向图矩阵存储
① 无向图的邻接矩阵是对称的,第i行(列)元素之和,就是顶点i的度,有向图的邻接矩阵则不一定是对称的,第i行(列)元素之和就是顶点i的出(入)度。
② 如果边带有权值,并且两个结点之间是连通的,那么边的关系就用权值代替,如果两个顶点不连通,则使用无穷大代替。
③ 用邻接矩阵存储图的优点是能够快速知道两个顶点是否连通,缺点是如果顶点比较多,边比
较少时,矩阵中存储了大量的0成为系数矩阵,比较浪费空间,并且要求两个节点之间的路
径不是很好求。
(2)实现:
namespace matrix
{
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
typedef Graph<V, W, MAX_W, Direction> Self;
public:
// 图的创建
// 1.IO输入 -- 不方便测试
// 2.图结构关系写到文件,读取文件
// 3.手动添加边
Graph() = default;
Graph(const V* a, size_t n)
{
_vertexs.reserve(n);
for (size_t i = 0; i < n; ++i)
{
_vertexs.push_back(a[i]);
_indexMap[a[i]] = i;
}
_matrix.resize(n);
for (size_t i = 0; i < _matrix.size(); ++i)
{
_matrix[i].resize(n, MAX_W);
}
}
// 得到元素位置
size_t GetVertexIndex(const V& v)
{
auto it = _indexMap.find(v);
if (it != _indexMap.end())
{
return it->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 << "[" << i << "]" << "->" << _vertexs[i] << endl;
}
cout << endl;
// 矩阵
// 横下标
cout << " ";
for (size_t i = 0; i < _vertexs.size(); ++i)
{
//cout << i << " ";
printf("%4d", i);
}
cout << endl;
for (size_t i = 0; i < _matrix.size(); ++i)
{
cout << i << " "; // 竖下标
for (size_t j = 0; j < _matrix[i].size(); ++j)
{
//cout << _matrix[i][j] << " ";
if (_matrix[i][j] == MAX_W)
{
//cout << "* ";
printf("%4c", '*');
}
else
{
//cout << _matrix[i][j] << " ";
printf("%4d", _matrix[i][j]);
}
}
cout << endl;
}
cout << endl;
}
private:
vector<V> _vertexs; // 顶点集合
map<V, int> _indexMap; // 顶点映射下标
vector<vector<W>> _matrix; // 邻接矩阵
};
}
首先,定义变量时,要能存下顶点,以及矩阵,同时要能通过对应的下标找到顶点。因此私有成员为vector的顶点集合,map的顶点映射下标,以及vector<vector<W>>的邻接矩阵。
上面的模板中,V是存入的数据的类型,W是权值的类型,MAX_W是类型最大值(默认是int最大值),Direction来区分有向图和无向图(false为无向图,true为有向图)。
然后需要实现构造函数,为顶点和矩阵开辟足够的空间,然后将顶点值存入顶点集合中,同时用map映射对应的下标,最后再将矩阵全部初始化为类型最大值。又因为需要用到默认构造函数,所以就Graph() = default;强制编译器生成默认构造函数。
要得到元素位置,就要利用之前的map,通过值,利用map的find找到对应的迭代器,然后it->second就可以得到该值对应的下标,如果没找到(即it == _indexMap.end()),就抛异常,同时返回-1。
我们要在矩阵中保存结点和边的关系,因此实现函数AddEdge,这里我们通过子函数的方法实现,因为后面需要用到子函数。首先根据传入的两个数据,得到这两个数据对应的位置,再将该位置的值变为其传来的权值。如果为无向图,因为无向图是对称的,所以在矩阵中将之前得到的两个数据的位置互换,再更改为权值。
最后,为了看到矩阵,我们实现打印函数。
2.邻接表
邻接表:使用数组表示顶点的集合,使用链表表示边的关系。
如果上一个邻接矩阵懂了,那么这个邻接表在逻辑上也差不多,也很容易弄懂。
(1)无向图邻接表存储
无向图中同一条边在邻接表中出现了两次。如果想知道顶点vi的度,只需要知道顶点vi边链表集合中结点的数目。
(2)有向图邻接表存储
有向图中每条边在邻接表中只出现一次,与顶点vi对应的邻接表所含结点的个数,就是该顶点的出度,也称出度表,要得到vi顶点的入度,必须检测其它所有顶电对应的边链表,看有多少边顶点的dst取值是i。
(3)实现
namespace link_table
{
template <class W>
struct Edge
{
// int _srci;
int _dsti; // 目标点的下标
W _w; // 权值
Edge<W>* _next;
Edge(int dsti, const W& w)
: _dsti(dsti)
, _w(w)
, _next(nullptr)
{}
};
template <class V, class W, bool Direction = false>
class Graph
{
typedef Edge<W> Edge;
public:
Graph(const V* a, size_t n)
{
_vertexs.reserve(n);
for (size_t i = 0; i < n; ++i)
{
_vertexs.push_back(a[i]);
_indexMap[a[i]] = i;
}
_tables.resize(n, nullptr);
}
// 得到元素位置
size_t 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& src, const V& dst, const W& w)
{
size_t srci = GetVertexIndex(src);
size_t dsti = GetVertexIndex(dst);
// 1->2
Edge* eg = new Edge(dsti, w);
eg->_next = _tables[srci];
_tables[srci] = eg;
// 2->1
// 无向图
if (Direction == false)
{
Edge* eg = new Edge(srci, w);
eg->_next = _tables[dsti];
_tables[dsti] = eg;
}
}
void Print()
{
// 顶点
for (size_t i = 0; i < _vertexs.size(); ++i)
{
cout << "[" << i << "]" << "->" << _vertexs[i] << endl;
}
cout << endl;
for (size_t i = 0; i < _tables.size(); ++i)
{
cout << _vertexs[i] << "[" << i << "]->";
Edge* cur = _tables[i];
while (cur)
{
cout << "[" << _vertexs[cur->_dsti] << ":" << cur->_dsti << ":" << cur->_w << "]->";
cur = cur->_next;
}
cout << "nullptr" << endl;
}
}
private:
vector<V> _vertexs; // 顶点集合
map<V, int> _indexMap; // 顶点映射下标
vector<Edge*> _tables; // 邻接表
};
}
这里我们只实现一个出度表。
首先,私有成员为vector的顶点集合,map的顶点映射下标,以及vector<Edge*>的邻接表。注意这里邻接表存的是Edge*,因为这个是链表,所以不仅要有权值,还要有next指针等,因此要额外实现一个类Edge,这里存Edge*,就可以通过该指针找到对应的目标点下标,权值和next指针。
上面的模板中,V是存入的数据的类型,W是权值的类型,Direction来区分有向图和无向图(false为无向图,true为有向图)。
然后需要实现构造函数,为顶点开辟足够的空间,然后将顶点值存入顶点集合中,同时用map映射对应的下标,最后再将链表表全部初始化为nullptr。
要得到元素位置,就要利用之前的map,通过值,利用map的find找到对应的迭代器,然后it->second就可以得到该值对应的下标,如果没找到(即it == _indexMap.end()),就抛异常,同时返回-1。(这个与邻接矩阵完全相同)
邻接表要想添加边,首先得到数据对应的位置,然后采用头插的方法,先创建新结点,然后头插链接。如果是无向图,就再次将对称的位置头插链接。
最后,为了看到邻接表,我们实现打印函数。
三.图的遍历
注意:后面的操作都是在邻接矩阵中完成的。
1.广度优先遍历
广度优先遍历类似于层序遍历。
是一层一层遍历的,我们通过队列和一个用来标记是否遍历过的数组来实现。
实现:
void BFS(const V& src)
{
size_t srci = GetVertexIndex(src);
// 队列和标记数组
queue<int> q;
vector<bool> visited(_vertexs.size(), false);
q.push(srci);
visited[srci] = true;
int levelSize = 1;
size_t n = _vertexs.size();
while (!q.empty())
{
// 一层一层出
for (int i = 0; i < levelSize; ++i)
{
int front = q.front();
q.pop();
cout << front << ":" << _vertexs[front] << " ";
// 把front顶点的邻接顶点入队列
for (size_t i = 0; i < n; ++i)
{
if (_matrix[front][i] != MAX_W)
{
if (visited[i] == false)
{
q.push(i);
visited[i] = true;
}
}
}
}
cout << endl;
levelSize = q.size();
}
cout << endl;
}
与层序遍历相同都是通过队列来完成。
首先根据传来的数据找到该数据对应的位置,然后分别创建一个队列和一个用来标记是否遍历过的数组(初始化为false)。
先将该数据对应的位置入队列,同时将标记数组中该位置变为true,代表该位置已经被遍历。同时levelSize来记录上一层的顶点数量,定义变量n得到当前顶点的数量。
然后一个while循环条件是队列是否为空,一个for循环使之一层一层的遍历(根据levelSize来进行)。先保存队列头,然后删掉该头,并把该头的邻接顶点入队列(通过for循环实现)。
入队列的方法就是通过利用矩阵的特点(与front同一行的矩阵中不是int的最大值的就是邻接的顶点),同时如果该位置标记数组为false,就可以将其入队列,并将该位置标记数组变为true。最后把levelSize变为当前队列中的数量(即是下一层的遍历次数)。
2.深度优先遍历
深度优先遍历类似于二叉树的前中后序遍历。
通过递归的方法来实现。
实现:
void _DFS(size_t srci, vector<bool>& visited)
{
cout << srci << ":" << _vertexs[srci] << endl;
visited[srci] = true;
// 找一个srci相邻的没有访问过多点,去往深度遍历
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);
}
首先得到数据的对应位置,并将标记数组全部初始化为false,进入_DFS子函数中。
将当前数据位置的标记数组变为true,接着找该位置相邻的(与当前数据位置同一行的,且不为int的最大值的)没有访问过的点(标记数组为false的),去进行深度遍历(递归)。
四.最小生成树
求最小生成树的都是无向图。
最小生成树特点:从其中删去任何一条边,生成树就不再连通;反之,在其中引入任何一条新边,都会形成一条回路。
若连通图由n个顶点组成,则其生成树必含n个顶点和n-1条边。
构成最小生成树的三个准则:
① 只能使用图中的边来构造最小生成树。
② 只能使用恰好n-1条边来连接图中的n个顶点。
③ 选用的n-1条边不能构成回路。
构成最小生成树的方法:Kruskal算法和Prim算法,这两个算法都采用了逐步求解的贪心策略。
贪心算法:在问题求解时,总是做出当前看起来最好的选择。
也就是说贪心算法做出是不是整体最优的选择,而是某种意义上的局部最优解,贪心算法不是对所有问题都能得到整体最优解的。
1.Kruskal(克鲁斯卡尔算法)
任给一个由n个顶点组成的图。
求最小生成树方法:
① 首先构造一个由这n个顶点组成,不含任何边的图G = {V, nullptr},其中每个顶点自成一个连通分量。
② 然后不断从E中取出权值最小的一条边(若有多条任取其一),若该边的两个顶点来自不同的连通分量,则将此边加入到G中。如此重复,直到所有顶点在同一个连通分量上为止。
算法核心:每次迭代时,选出一条具有最小权值,且两端点不在同一连通分量上的边,加入生成树。
Kruskal算法需要用到上一篇的并查集:
实现:
并查集:
#include <vector>
class UnionFindSet
{
public:
UnionFindSet(size_t n)
: _ufs(n, -1)
{}
size_t 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;
}
void Union(int x1, int x2)
{
int root1 = FindRoot(x1);
int root2 = FindRoot(x2);
// 如果本身就在一个集合就没必要合并了
if (root1 == root2)
{
return;
}
// 控制数据量小的往大的集合合并
if (abs(_ufs[root1]) < abs(_ufs[root2]))
{
swap(root1, root2);
}
_ufs[root1] += _ufs[root2];
_ufs[root2] = root1;
}
bool InSet(int x1, int x2)
{
return FindRoot(x1) == FindRoot(x2);
}
size_t SetSize()
{
size_t size = 0;
for (size_t i = 0; i < _ufs.size(); ++i)
{
if (_ufs[i] < 0)
{
++size;
}
}
return size;
}
private:
std::vector<int> _ufs;
};
_AddEdge函数:
void _AddEdge(size_t srci, size_t dsti, const W& w)
{
_matrix[srci][dsti] = w;
// 无向图
if (Direction == false)
{
_matrix[dsti][srci] = w;
}
}
Kruskal算法:
typedef Graph<V, W, MAX_W, Direction> Self;
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;
}
};
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]));
}
}
}
// 选出n-1条边
int size = 0;
// W一般是整数、浮点数,W()是0
W totalW = W();
UnionFindSet ufs(n);
while (!minque.empty())
{
Edge min = minque.top();
minque.pop();
if (!ufs.InSet(min._srci, min._dsti))
{
cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
minTree._AddEdge(min._srci, min._dsti, min._w);
ufs.Union(min._srci, min._dsti);
++size;
totalW += min._w;
}
else
{
cout << "构成环:";
cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
}
}
if (size == n - 1)
{
return totalW;
}
else
{
return W();
}
}
先创建一个Edge的类,用来存放该边的权值、以及该边的两个顶点。
首先将minTree初始化,让minTree的顶点集合和顶点映射下标map与要求最小生成树的图相同,同时为minTree的邻接矩阵开辟好空间,并将该矩阵全部初始化为类型最大值。
创建一个优先级队列(传greater,使其从小到大排好序),然后循环遍历矩阵,将i < j(矩阵的一半【无向图是对称矩阵】),存放Edge类。
接着定义一个size,用来判断该最小生成树是否为n-1条边,totalW记录最小生成树权值,并创建一个并查集类的ufs。
利用优先级队列是否为空来实现一个while循环,得到优先级队列顶部元素(最小的元素),并在优先级队列中删除掉该元素,然后根据并查集的InSet函数(判断是否在一个集合),如果不在一个集合,就调用_AddEdge函数,将矩阵的该位置的大小变为该边的权值大小,然后通过并查集的Union函数(合并),将边的两点合并到一个集合。同时++边数,总权值+=该边权值;如果在一个集合,就不需要进行操作,进行下一轮循环(这里我们打印构成环,方便观察)。
最后如果边数是n-1,就符合最小生成树的条件,返回总权值,如果不是,那么就没有最小生成树,就返回W(),这个一般是0。
2.Prim(普里姆算法)
Prim算法与Kruskal算法类似,只不过这个是从任意一个顶点开始,一直覆盖V中所有顶点时为止。
实现:
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;
// 从X->Y集合中连接的边里面选出最小的边
priority_queue<Edge, vector<Edge>, greater<Edge>> minq;
// 先把srci连接的边添加到队列中
for (size_t i = 0; i < n; ++i)
{
if (_matrix[srci][i] != MAX_W)
{
minq.push(Edge(srci, i, _matrix[srci][i]));
}
}
cout << "Prim开始选边" << endl;
size_t size = 0;
W totalW = W();
while (!minq.empty())
{
Edge min = minq.top();
minq.pop();
// 最小边的目标点也在X集合,则构成环
if (X[min._dsti])
{
//cout << "构成环:";
//cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
}
else
{
minTree._AddEdge(min._srci, min._dsti, min._w);
//cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
X[min._dsti] = true;
Y[min._dsti] = false;
++size;
totalW += min._w;
if (size == n - 1)
{
break;
}
for (size_t i = 0; i < n; ++i)
{
if (_matrix[min._dsti][i] != MAX_W && Y[i])
{
minq.push(Edge(min._dsti, i, _matrix[min._dsti][i]));
}
}
}
}
if (size == n - 1)
{
return totalW;
}
else
{
return W();
}
}
先得到数据对应的位置,然后对该图minTree进行初始化,让minTree的顶点集合和顶点映射下标map与要求最小生成树的图相同,同时为minTree的邻接矩阵开辟好空间,并将该矩阵全部初始化为类型最大值。
接下来定义两个标记数组vector,X中把已经覆盖的顶点设为true,Y中把未被覆盖的顶点设为true,因此在初始化时将X全部初始化为false,将Y全部初始化为true,然后再将当前数据的位置的X变为true,Y变为false。
再定义一个优先级队列,按照从小到大排序,然后把srci连接的边Edge类添加到队列中(与srci同行的数据中不为类型最大值的)。
接着定义一个size,用来判断该最小生成树是否为n-1条边,totalW记录最小生成树权值。
利用优先级队列是否为空来实现一个while循环,得到优先级队列顶部元素(最小的元素),并在优先级队列中删除掉该元素。如果最小边的目标点在X集合中,就说明构成了环;如果不在,就添加边,然后将dsti位置的X变成true,Y变成false(这里不用变srci,因为之前已经变了),同时++边数,总权值+=该边权值。如果size为n-1就满足最小生成树条件了,直接break,如果没有满足,就把dsti边的邻接顶点放入优先级队列中(与dsti同一行且不为类型最大值,同时在Y集合中【不在X集合中】)。
最后如果边数是n-1,就符合最小生成树的条件,返回总权值,如果不是,那么就没有最小生成树,就返回W(),这个一般是0。
五.最短路径
从带权的有向图G中的某一顶点出发,找出一条通往另一顶点的最短路径,最短就是沿路径各边的权值总和达到最小。
1.单源最短路径
单源最短路径问题:给定一个图,求源结点到图中每个结点的最短路径。
(1)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中,所以该算法使用的是贪心策略。
实现:
定义一个u变量,每次选s->u,去选最短路径边那个顶点 ,去更新其连接的路径,u是不是S中的点。
两个数组:dist[],临时存放s->{syztx}的最短路径;pPath[],存放路径前一个顶点的坐标。
void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath)
{
size_t srci = GetVertexIndex(src);
size_t n = _vertexs.size();
dist.resize(n, MAX_W);
pPath.resize(n, -1);
dist[srci] = 0;
pPath[srci] = srci;
// 已经确定最短路径的顶点集合
vector<bool> S(n, false);
for (size_t j = 0; j < n; ++j)
{
// 选最短路径顶点且不在S的去更新其它路径
int u = 0;
W min = MAX_W;
for (size_t i = 0; i < n; ++i)
{
if (S[i] == false && dist[i] < min)
{
u = i;
min = dist[i];
}
}
S[u] = true;
// 松弛更新u连接顶点v,srci->u + u->v < srci->v 更新
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;
}
}
}
}
首先得到数据所在位置,并将dist初始化为类型最大值,将pPath初始化为-1,再将dist[srci] = 0,因为自己到自己的最短路径是0,然后再将pPath[srci] = srci,这个是因为自己到自己的前一个顶点坐标就是自己。
接着定义一个vector为已经确定最短路径的顶点集合,然后定义变量u(去最短路径的那个顶点),以及min(最短路径的大小)。再去选最短路径顶点并且不在S中的,得到去最短路径顶点和最短路径大小min,然后再将u对应的位置S[u]置为true。(因为已经确定了该最短路径)
然后再进行松弛更新,首先要保证该顶点位置的S为false,并且该位置有边(不为类型最大值),如果 到u顶点的最短路径dist的值+这个两顶点边之间的值 比 前最短路径的值还要小,就说明有了新的最短路径,就对dist[v]进行更新,同时更新上一个顶点路径pPath[v] = u。
(2)Bellman-Ford(贝尔曼-福特算法)
Dijkstra算法只能用来解决正权图的单源最短路径问题,但有些题目会出现负权图。这时这个算法就不能帮助我们解决问题了,而Bellman-Ford算法可以解决负权图的单源最短路径问题。
它的优点是可以解决有负权边的单源最短路径问题,而且可以用来判断是否有负权回路。它也有明显的缺点,它的时间复杂度 O(N*E) (N是点数,E是边数)普遍是要高于Dijkstra算法O(N²)的。像这里如果我们使用邻接矩阵实现,那么遍历所有边的数量的时间复杂度就是O(N^3),这里也可以看出来Bellman-Ford就是一种暴力求解更新。
实现:
两个数组:dist[],存放s->{syztx}的最短路径;pPath[],存放路径前一个顶点的坐标。
bool BellmanFord(const V& src, vector<W>& dist, vector<int>& pPath)
{
size_t n = _vertexs.size();
size_t srci = GetVertexIndex(src);
// vector<int> dist 记录srci-其它顶点最短路径权值数组
dist.resize(n, MAX_W);
// vector<int> pPath 记录srci-其它顶点最短路径父顶点数组
pPath.resize(n, -1);
// 先更新srci->srci为缺省值
dist[srci] = W();
//cout << "更新边:i->j" << 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)
{
// srci->i + i ->j
if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
{
update = true;
cout << _vertexs[i] << "->" << _vertexs[j] << ":" << _matrix[i][j] << endl;
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;
}
}
}
}
首先得到数据所在位置,并将dist初始化为类型最大值,将pPath初始化为-1,再更新dist[srci]为缺省值(0)。
然后进行暴力更新,定义一个变量update(用作优化),如果这个轮次没有更新出更短路径,那么后续轮次就不需要继续了。一个双循环,如果该矩阵有边(不为类型最大值),并且到这个路径dist[i] + _matrix[i][j]这个边要小于dist[j]路径,就先将update改为true,再将dist[j]的最短路径改为新的最短路径大小,同时改变pPath为i。
如果已经更新够了轮次还能继续更新,就说明出现了带负权回来,返回false。
2.多源最短路径
多源最短路径问题:给定一个图,解决任意两点间的最短路径 。
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}取得的一条最短路径。
(1)Floyd-Warshall(弗洛伊德算法)
Floyd-Warshall算法本质是三维动态规划,D[i][j][k]表示从点i到点j只经过0到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];
}
}
}
}
}
首先初始化权值和路径矩阵,将dist初始化为类型最大值,将pPath初始化为-1。
然后对直接相连的边进行更新,矩阵不为类型最大值即可进行更新,让vvDist[i][j] = _matrix[i][j],同时更新路径vvpPath[i][j] = i。如果i和j相等说明是同一顶点,路径为0(缺省值)。
再对其它路径进行更新,3个循环,让k作为中间点尝试去更新i->j的路径,满足vvDist[i][k] 和vvDist[k][j]都不为类型最大值,同时vvDist[i][k] + vvDist[k][j] < vvDist[i][j]时,就将原本的路径vvDist[i][j] = vvDist[i][k] + vvDist[k][j],最后再对路径进行更新,让vvpPath[i][j] = vvpPath[k][j]。
六.总代码
#pragma once
#include <vector>
#include <map>
#include <string>
#include <queue>
#include <functional>
namespace matrix
{
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
typedef Graph<V, W, MAX_W, Direction> Self;
public:
// 图的创建
// 1.IO输入 -- 不方便测试
// 2.图结构关系写到文件,读取文件
// 3.手动添加边
Graph() = default;
Graph(const V* a, size_t n)
{
_vertexs.reserve(n);
for (size_t i = 0; i < n; ++i)
{
_vertexs.push_back(a[i]);
_indexMap[a[i]] = i;
}
_matrix.resize(n);
for (size_t i = 0; i < _matrix.size(); ++i)
{
_matrix[i].resize(n, MAX_W);
}
}
// 得到元素位置
size_t GetVertexIndex(const V& v)
{
auto it = _indexMap.find(v);
if (it != _indexMap.end())
{
return it->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 << "[" << i << "]" << "->" << _vertexs[i] << endl;
}
cout << endl;
// 矩阵
// 横下标
cout << " ";
for (size_t i = 0; i < _vertexs.size(); ++i)
{
//cout << i << " ";
printf("%4d", i);
}
cout << endl;
for (size_t i = 0; i < _matrix.size(); ++i)
{
cout << i << " "; // 竖下标
for (size_t j = 0; j < _matrix[i].size(); ++j)
{
//cout << _matrix[i][j] << " ";
if (_matrix[i][j] == MAX_W)
{
//cout << "* ";
printf("%4c", '*');
}
else
{
//cout << _matrix[i][j] << " ";
printf("%4d", _matrix[i][j]);
}
}
cout << endl;
}
cout << endl;
}
// 广度优先遍历
void BFS(const V& src)
{
size_t srci = GetVertexIndex(src);
// 队列和标记数组
queue<int> q;
vector<bool> visited(_vertexs.size(), false);
q.push(srci);
visited[srci] = true;
int levelSize = 1;
size_t n = _vertexs.size();
while (!q.empty())
{
// 一层一层出
for (int i = 0; i < levelSize; ++i)
{
int front = q.front();
q.pop();
cout << front << ":" << _vertexs[front] << " ";
// 把front顶点的邻接顶点入队列
for (size_t i = 0; i < n; ++i)
{
if (_matrix[front][i] != MAX_W)
{
if (visited[i] == false)
{
q.push(i);
visited[i] = true;
}
}
}
}
cout << endl;
levelSize = q.size();
}
cout << endl;
}
// 深度优先遍历
void _DFS(size_t srci, vector<bool>& visited)
{
cout << srci << ":" << _vertexs[srci] << endl;
visited[srci] = true;
// 找一个srci相邻的没有访问过的点,去往深度遍历
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);
}
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;
}
};
// 克鲁斯卡尔算法求最小生成树
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]));
}
}
}
// 选出n-1条边
int size = 0;
// W一般是整数、浮点数,W()是0
W totalW = W();
UnionFindSet ufs(n);
while (!minque.empty())
{
Edge min = minque.top();
minque.pop();
if (!ufs.InSet(min._srci, min._dsti))
{
cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
minTree._AddEdge(min._srci, min._dsti, min._w);
ufs.Union(min._srci, min._dsti);
++size;
totalW += min._w;
}
else
{
cout << "构成环:";
cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
}
}
if (size == n - 1)
{
return totalW;
}
else
{
return W();
}
}
// 普里姆算法求最小生成树
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;
// 从X->Y集合中连接的边里面选出最小的边
priority_queue<Edge, vector<Edge>, greater<Edge>> minq;
// 先把srci连接的边添加到队列中
for (size_t i = 0; i < n; ++i)
{
if (_matrix[srci][i] != MAX_W)
{
minq.push(Edge(srci, i, _matrix[srci][i]));
}
}
cout << "Prim开始选边" << endl;
size_t size = 0;
W totalW = W();
while (!minq.empty())
{
Edge min = minq.top();
minq.pop();
// 最小边的目标点也在X集合,则构成环
if (X[min._dsti])
{
//cout << "构成环:";
//cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
}
else
{
minTree._AddEdge(min._srci, min._dsti, min._w);
//cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
X[min._dsti] = true;
Y[min._dsti] = false;
++size;
totalW += min._w;
if (size == n - 1)
{
break;
}
for (size_t i = 0; i < n; ++i)
{
if (_matrix[min._dsti][i] != MAX_W && Y[i])
{
minq.push(Edge(min._dsti, i, _matrix[min._dsti][i]));
}
}
}
}
if (size == n - 1)
{
return totalW;
}
else
{
return W();
}
}
void PrintShortPath(const V& src, const vector<W>& dist, const vector<int>& pPath)
{
size_t srci = GetVertexIndex(src);
size_t n = _vertexs.size();
for (size_t i = 0; i < n; ++i)
{
if (i != srci)
{
// 找出i顶点的路径
vector<int> path;
size_t parenti = i;
while (parenti != srci)
{
path.push_back(parenti);
parenti = pPath[parenti];
}
path.push_back(srci);
reverse(path.begin(), path.end());
for (auto index : path)
{
cout << _vertexs[index] << "->";
}
cout << dist[i] << endl;
}
}
}
// 迪杰斯特拉算法求单源最短路径
void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath)
{
size_t srci = GetVertexIndex(src);
size_t n = _vertexs.size();
dist.resize(n, MAX_W);
pPath.resize(n, -1);
dist[srci] = 0;
pPath[srci] = srci;
// 已经确定最短路径的顶点集合
vector<bool> S(n, false);
for (size_t j = 0; j < n; ++j)
{
// 选最短路径顶点且不在S的去更新其它路径
int u = 0;
W min = MAX_W;
for (size_t i = 0; i < n; ++i)
{
if (S[i] == false && dist[i] < min)
{
u = i;
min = dist[i];
}
}
S[u] = true;
// 松弛更新u连接顶点v,srci->u + u->v < srci->v 更新
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;
}
}
}
}
// 贝尔曼-福特算法求单源最短路径
bool BellmanFord(const V& src, vector<W>& dist, vector<int>& pPath)
{
size_t n = _vertexs.size();
size_t srci = GetVertexIndex(src);
// vector<int> dist 记录srci-其它顶点最短路径权值数组
dist.resize(n, MAX_W);
// vector<int> pPath 记录srci-其它顶点最短路径父顶点数组
pPath.resize(n, -1);
// 先更新srci->srci为缺省值
dist[srci] = W();
//cout << "更新边:i->j" << 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)
{
// srci->i + i ->j
if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
{
update = true;
cout << _vertexs[i] << "->" << _vertexs[j] << ":" << _matrix[i][j] << endl;
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;
}
}
}
}
// 弗洛伊德算法求多源最短路径
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];
}
}
}
}
}
private:
vector<V> _vertexs; // 顶点集合
map<V, int> _indexMap; // 顶点映射下标
vector<vector<W>> _matrix; // 邻接矩阵
};
}
namespace link_table
{
template <class W>
struct Edge
{
// int _srci;
int _dsti; // 目标点的下标
W _w; // 权值
Edge<W>* _next;
Edge(int dsti, const W& w)
: _dsti(dsti)
, _w(w)
, _next(nullptr)
{}
};
template <class V, class W, bool Direction = false>
class Graph
{
typedef Edge<W> Edge;
public:
Graph(const V* a, size_t n)
{
_vertexs.reserve(n);
for (size_t i = 0; i < n; ++i)
{
_vertexs.push_back(a[i]);
_indexMap[a[i]] = i;
}
_tables.resize(n, nullptr);
}
// 得到元素位置
size_t 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& src, const V& dst, const W& w)
{
size_t srci = GetVertexIndex(src);
size_t dsti = GetVertexIndex(dst);
// 1->2
Edge* eg = new Edge(dsti, w);
eg->_next = _tables[srci];
_tables[srci] = eg;
// 2->1
// 无向图
if (Direction == false)
{
Edge* eg = new Edge(srci, w);
eg->_next = _tables[dsti];
_tables[dsti] = eg;
}
}
void Print()
{
// 顶点
for (size_t i = 0; i < _vertexs.size(); ++i)
{
cout << "[" << i << "]" << "->" << _vertexs[i] << endl;
}
cout << endl;
for (size_t i = 0; i < _tables.size(); ++i)
{
cout << _vertexs[i] << "[" << i << "]->";
Edge* cur = _tables[i];
while (cur)
{
cout << "[" << _vertexs[cur->_dsti] << ":" << cur->_dsti << ":" << cur->_w << "]->";
cur = cur->_next;
}
cout << "nullptr" << endl;
}
}
private:
vector<V> _vertexs; // 顶点集合
map<V, int> _indexMap; // 顶点映射下标
vector<Edge*> _tables; // 邻接表
};
}