图
图的基本概念
图是由顶点集合及顶点间的关系组成的一种数据结构:G = (V, E),其中:顶点集合V = {x|x属于某个数据对象集},是有穷非空集合;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)是有方向的。
顶点和边:图中结点称为顶点,第i个顶点记作vi。两个顶点vi和vj相关联称作顶点vi和顶点vj之间有一条边,图中的第k条边记作ek,ek = (vi,vj)或<vi,vj>。
有向图和无向图:在有向图中,顶点对<x, y>是有序的,顶点对<x,y>称为顶点x到顶点y的一条边(弧),<x, y>和<y, x>是两条不同的边,比在无向图中,顶点对(x, y)是无序的,顶点对(x,y)称为顶点x和顶点y相关联的一条边,这条边没有特定方向,(x, y)和(y,x)是同一条边
完全图:在有n个顶点的无向图中,若有n * (n-1)/2条边,即任意两个顶点之间有且仅有一条边,则称此图为无向完全图,在n个顶点的有向图中,若有n * (n-1)条边,即任意两个顶点之间有且仅有方向相反的边,则称此图为有向完全图
邻接顶点 :在无向图中G中,若(u, v)是E(G)中的一条边,则称u和v互为邻接顶点,并称边(u,v)依附于顶点u和v;在有向图G中,若<u, v>是E(G)中的一条边,则称顶点u邻接到v,或顶点v邻接自顶
点u,并称边<u, v>与顶点u和顶点v相关联。
顶点的度:顶点v的度是指与它相关联的边的条数,记作deg(v)。在有向图中,顶点的度等于该顶点的入度与出度之和,其中顶点v的入度是以v为终点的有向边的条数,记作indev(v),顶点v的出度是以v为起始点的有向边的条数,记作outdev(v)。因此:dev(v) = indev(v) + outdev(v)。注意:对于无向图,顶点的度等于该顶点的入度和出度,即dev(v) = indev(v) = outdev(v)。
路径:在图G = (V, E)中,若从顶点vi出发有一组边使其可到达顶点vj,则称顶点vi到顶点vj的顶点序列为从顶点vi到顶点vj的路径。
路径长度:对于不带权的图,一条路径的路径长度是指该路径上的边的条数;对于带权的图,一条路径的路径长度是指该路径上各个边权值的总和。
简单路径与回路:若路径上各顶点v1,v2,v3,...,vm均不重复,则称这样的路径为简单路径。若路径上第一个顶点v1和最后一个顶点vm重合,则称这样的路径为回路或环。
子图:设图G = {V, E}和图G1 = {V1,E1},若V1属于V且E1属于E,则称G1是G的子图。
连通图:在无向图中,若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图。
强连通图:在有向图中,若在每一对顶点vi和vj之间都存在一条从vi到vj的路径,也存在一条从vj到vi的路径,则称此图是强连通图。
生成树:一个连通图的最小连通子图称作该图的生成树(意味着其是针对无向图而言的),有n个顶点的连通图的生成树有n个顶点和n-1条边。
图的存储结构
图中的每一个顶点我们可以用一个唯一非负整数表示,这是十分合理的,因为我们可以在上层将每一个顶点都保存在一个数组中,这样每一个顶点就具有了一个唯一下标,我们在图中只需要对下标进行操作即可。
邻接矩阵
假设现在有n个节点,我们需要将这n个节点放到一个长度为n的数组vex中以保存这些节点,同时还需要建立一个n*n的二维数组arc,arc[i,j]记录的是在vex的第i个节点到第j个节点的权值,如果vex的第i个节点到第j个节点没有边,将其权值设置成某个特殊值,在这里我们用整型表示权值,因此我们可以用整型的最大值表示该边不存在。
无向图的邻接矩阵是对称的,而有向图的邻接矩阵则不一定是对称的。
用邻接矩阵存储图的优点是能够快速知道两个顶点是否连通,缺陷是如果顶点比较多,边比较少时,矩阵中存储了大量无穷值,比较浪费空间,并且不适合通过结点找到与其相关联的边(需要遍历一列或一行数组),因此邻接矩阵适用于边比较密集的图。
cpp
include<iostream>
#include<vector>
using namespace std;
int maxWeight = INT_MAX;
class graphAdjancencyMatrix {
public:
graphAdjancencyMatrix(bool isDirect = false)
:_isDirect(isDirect)
{}
~graphAdjancencyMatrix()
{}
int getWeight(size_t src, size_t dest) {
if (src >= _graph.size() || dest >= _graph.size()) {
return -1;
}
return _graph[src][dest];
}
bool addVertex(size_t src) {
if (src != _graph.size()) {
return false;
}
if (0 == _graph.size()) {
_graph.push_back(vector<int>(1, 0));
return true;
}
int i = 0;
for (; i < _graph.size(); ++i) {
_graph[i].push_back(maxWeight);
}
_graph.push_back(vector<int>(_graph[0].size(), maxWeight));
_graph[_graph.size() - 1][_graph.size() - 1] = 0;
return true;
}
bool addEdge(size_t src, size_t dest, int weight) {
if (src==dest||src > _graph.size() || dest > _graph.size()) {
return false;
}
addVertex(src);
addVertex(dest);
_graph[src][dest] = weight;
if (!_isDirect) {
_graph[dest][src] = weight;
}
return true;
}
bool eraseVertex(size_t src) {
if (src >= _graph.size()) {
return false;
}
_graph.erase(_graph.begin() + src);
int i = 0;
for (; i < _graph.size(); ++i) {
_graph[i].erase(_graph[i].begin() + src);
}
return true;
}
bool eraseEdge(size_t src, size_t dest) {
if (src >= _graph.size() || dest >= _graph.size()) {
return false;
}
if (src == dest) {
_graph[src][dest] = 0;
}
else {
_graph[src][dest] = maxWeight;
_graph[dest][src] = maxWeight;
}
}
void print() {
int i = 0;
int j = 0;
printf(" ");
for (i = 0; i < _graph.size(); ++i) {
printf("%d ", i);
}
printf("\n");
for (i = 0; i < _graph.size(); ++i) {
printf("%d ", i);
for (j = 0; j < _graph[i].size(); ++j) {
if (_graph[i][j] == maxWeight) {
printf("* ");
}
else {
printf("%d ", _graph[i][j]);
}
}
printf("\n");
}
}
private:
bool _isDirect; //是否是有向图
vector<vector<int>> _graph; //邻接矩阵
};
int main() {
graphAdjancencyMatrix g;
g.addVertex(0);
g.addVertex(1);
g.addVertex(2);
g.addVertex(7);
g.print();
g.addEdge(0, 1, 3);
g.addEdge(2, 1, 3);
g.addEdge(2, 3, 9);
g.addEdge(6, 3, 9);
g.print();
g.eraseEdge(1, 1);
g.eraseEdge(8, 1);
g.eraseEdge(0, 1);
g.print();
g.eraseVertex(1);
g.eraseVertex(3);//前面删除成功,此时的结点3已经不是之前的结点3
g.eraseVertex(9);
g.print();
return 0;
}
邻接表
我们可以将每一个结点都创建一个链表,链表中存放的是与该结点相关联的边的终端结点,然后将所有链表都放到一个数组当中,这样数组下标就可以唯一标识一个结点。
无向图中同一条边在邻接表中出现了两次。如果想知道顶点vi的度,只需要知道顶点vi边链表集合中结点的数目即可。
在有向图中,我们需要一张出边表记录每个结点的出边和一张入边表记录每个结点的入边,要寻找某个结点的出边或者入边,只需要到出边表或入边表寻找对应结点的边即可。也可以只记录出边表,但要寻找某个结点的入边时需要遍历整个邻接表查询哪个结点的出边的终端是该节点;同理,也可以只记录入边表,要查询某个结点的出边时同样需要遍历整个邻接表。
用邻接表存储图的优点是能够快速查询某个结点的边,但不能快速确定两个结点是否联通,如果图的边比较密集,邻接表中就存放了大量的指针,因此邻接表适用于边比较稀疏的图。
cpp
#include<iostream>
#include<vector>
using namespace std;
//无向图
struct edge {
edge(size_t vertexStart, size_t vertexEnd,int weight)
:_vertexStart(vertexStart)
,_vertexEnd(vertexEnd)
,_weight(weight)
,_next(nullptr)
{}
~edge()
{}
size_t _vertexStart; //该边的起始结点
size_t _vertexEnd; //该边的终端结点
int _weight; //边的权重
edge* _next; //指向下一个结点
};
class graphAdjancencyList {
public:
graphAdjancencyList()
{}
~graphAdjancencyList()
{
int i = 0;
for (; i < _graph.size(); ++i) {
edge* cur = _graph[i];
while (cur) {
edge* next = cur->_next;
delete cur;
cur = next;
}
}
}
bool addVertex(size_t src) {
if (src != _graph.size()) {
return false;
}
edge* np = new edge(-1,-1, -1);
if (nullptr == np) {
return false;
}
_graph.push_back(np);
return true;
}
bool addEdge(size_t src, size_t dest, int weight) {
if (src == dest || src >= _graph.size() || dest >= _graph.size()) {
return false;
}
//保证两个结点之间只有唯一边
edge* cur = _graph[src]->_next;
while (cur) {
if (cur->_vertexEnd == dest) {
cur->_weight = weight;
cur = _graph[dest]->_next;
while (cur) {
if (cur->_vertexEnd == src) {
cur->_weight = weight;
return true;
}
cur = cur->_next;
}
return true;
}
cur = cur->_next;
}
edge* snp = new edge(dest,src, weight);
edge* dnp = new edge(src,dest, weight);
if (nullptr == snp||nullptr==dnp) {
if (snp) {
delete snp;
}
if (dnp) {
delete dnp;
}
return false;
}
snp->_next = _graph[dest]->_next;
_graph[dest]->_next = snp;
dnp->_next = _graph[src]->_next;
_graph[src]->_next = dnp;
return true;
}
bool eraseEdge(size_t src, size_t dest) {
if (src == dest || src >= _graph.size() || dest >= _graph.size()) {
return false;
}
edge* cur = _graph[src];
while (cur->_next) {
if (dest == cur->_next->_vertexEnd) {
edge* tmp = cur->_next;
cur->_next = cur->_next->_next;
delete tmp;
cur = _graph[dest];
while (cur->_next) {
if (src == cur->_next->_vertexEnd) {
tmp = cur->_next;
cur->_next = cur->_next->_next;
delete tmp;
return true;
}
cur = cur->_next;
}
}
cur = cur->_next;
}
return false;
}
void print() {
int i = 0;
for (; i < _graph.size(); ++i) {
edge* cur = _graph[i]->_next;
while (cur) {
printf("%d <--- %d ---> %d\n", i, cur->_weight, cur->_vertexEnd);
cur = cur->_next;
}
}
printf("\n");
}
private:
vector<edge*> _graph; //邻接表
};
int main() {
graphAdjancencyList g;
g.addVertex(0);
g.addVertex(1);
g.addVertex(2);
g.addVertex(7);
g.print();
g.addEdge(0, 1, 3);
g.addEdge(2, 1, 7);
g.addEdge(2, 3, 9);
g.addEdge(6, 3, 9);
g.print();
g.eraseEdge(1, 1);
g.eraseEdge(8, 1);
g.eraseEdge(0, 1);
g.print();
return 0;
}
十字链表
考虑到使用邻接表时寻找某个顶点的入度或者出度比较困难,否则就要既存入边表和出边表,于是提出了十字链表(Orthogonal List)。十字链表的基本结构与邻接表是一样的,所有链表也都放到一个数组当G中,只不过链表中的每一个结点需要存放两个指针:pin和pout,pin的作用与邻接表的入边表一样,指向该顶点的下一条入边,pout的作用也与与邻接表的出边表一样,指向该顶点的下一条出边,当顶点V1与V2之间要新增一条边V1->V2时,我们新建一个结点pNode,在G[2]的pin头插pNode,在G[1]的pout头插pNode,这样我们就只需要一条边只需要存储一个结点即可,删除边时只需要执行与插入相反的操作即可。
cpp
struct orthogonalEdge {
orthogonalEdge(size_t start,size_t end,int weight)
:_weight(weight)
,_start(start)
,_end(end)
,_inNext(nullptr)
,_outNext(nullptr)
{}
~orthogonalEdge()
{}
size_t _start; //起始顶点
size_t _end; //终端顶点
int _weight; //权值
orthogonalEdge* _inNext; //指向下一条入边
orthogonalEdge* _outNext; //指向下一条出边
};
class orthogonalList {
public:
orthogonalList()
{}
~orthogonalList()
{
int i = 0;
for (; i < _graph.size(); ++i) {
orthogonalEdge* cur = _graph[i];
while (cur) {
orthogonalEdge* next = cur->_outNext;
delete cur;
cur = next;
}
}
}
bool addVertex(size_t src) {
if (src != _graph.size()) {
return false;
}
orthogonalEdge* np = new orthogonalEdge(-1, -1, -1);
if (nullptr == np) {
return false;
}
_graph.push_back(np);
return true;
}
bool addEdge(size_t src, size_t dest,int weight) {
if (src >= _graph.size() || dest >= _graph.size()) {
return false;
}
orthogonalEdge* np = new orthogonalEdge(src,dest,weight);
if (nullptr == np) {
return false;
}
np->_inNext = _graph[dest]->_inNext;
_graph[dest]->_inNext = np;
np->_outNext = _graph[src]->_outNext;
_graph[src]->_outNext = np;
return true;
}
bool eraseEdge(size_t src, size_t dest) {
if (src >= _graph.size() || dest >= _graph.size()) {
return false;
}
orthogonalEdge* pre = nullptr;
orthogonalEdge* cur=_graph[src]->_outNext;
while (cur) {
if (cur->_end == dest) {
break;
}
pre = cur;
cur = cur->_outNext;
}
if (nullptr == cur) {
return false;
}
pre->_outNext = cur->_outNext;
pre = nullptr;
cur = _graph[dest]->_inNext;
while (cur) {
if (cur->_start == src) {
break;
}
pre = cur;
cur = cur->_inNext;
}
pre->_inNext = cur->_inNext;
delete cur;
return true;
}
private:
vector<orthogonalEdge*> _graph;
};
邻接多重表
考虑到在无向图中,如果使用邻接表,无论是入边表还是出边表,同一边都保存了两次,这对于一些操作如删除边或者标记边是否已经搜索过是很不方便的,邻接多重表与十字链表的结构十分相似,十字链表每个顶点维护两条链表:一条是该顶点的入边链表,一条是该顶点的出边链表。邻接多重表每个顶点只维护一条链表,即与该顶点相关的边的链表,但每个边结点与十字链表一样,需要维护两个指针:一个startNext,指向与该顶点start相关的下一条边,一个endNext指向下一条与顶点end相关的顶点,这样每一条边我们只要存一个边顶点即可。同时一般为了一些操作方便如标志边是否已经被搜索过,边中还会添加一个标志位。
cpp
struct multiEdge {
multiEdge(size_t endPoint1,size_t endPoint2,int weight )
:_endPoint1(endPoint1)
,_endPoint2(endPoint2)
,_weight(weight)
,_mark(0)
,_endPoint1Next(nullptr)
,_endPoint2Next(nullptr)
{}
multiEdge()
{}
size_t _endPoint1; //边的端点
size_t _endPoint2; //边的端点
int _weight; //权值
int _mark; //标志位
multiEdge* _endPoint1Next; //指向与顶点 _endPoint1相关的下一条边
multiEdge* _endPoint2Next; //指向与顶点 _endPoint2相关的下一条边
};
class adjacentMultiList {
public:
adjacentMultiList()
{}
~adjacentMultiList()
{
int i = 0;
vector<multiEdge*> edges;
for (; i < _graph.size(); ++i) {
edges.push_back(_graph[i]);
multiEdge* cur = _graph[i]->_endPoint1Next;
while (cur) {
multiEdge* back = cur->_endPoint1Next;
if (i == cur->_endPoint2) {
back = cur->_endPoint2Next;
}
if (0 == cur->_mark) {
edges.push_back(cur);
cur->_mark = 1;
}
cur = back;
}
}
for (auto e : edges) {
delete e;
}
}
bool addVertex(size_t src) {
if (src != _graph.size()) {
return false;
}
multiEdge* np = new multiEdge(-1, -1, -1);
if (nullptr == np) {
return false;
}
_graph.push_back(np);
return true;
}
bool addEdge(size_t src, size_t dest, int weight) {
if (src >= _graph.size() || dest >= _graph.size()) {
return false;
}
multiEdge* np = new multiEdge(src, dest, weight);
if (nullptr == np) {
return false;
}
np->_endPoint1Next = _graph[src]->_endPoint1Next;
_graph[src]->_endPoint1Next = np;
np->_endPoint2Next = _graph[dest]->_endPoint1Next;
_graph[dest]->_endPoint1Next = np;
return true;
}
bool eraseEdge(size_t src, size_t dest) {
if (src >= _graph.size() || dest >= _graph.size()) {
return false;
}
multiEdge* pre = _graph[src];
multiEdge* cur = _graph[src]->_endPoint1Next;
multiEdge* back = nullptr;
while (cur) {
if (cur->_endPoint1 == dest || cur->_endPoint2 == dest) {
if (cur->_endPoint1 == src) {
back = cur->_endPoint1Next;
}
else {
back = cur->_endPoint2Next;
}
break;
}
pre = cur;
if (cur->_endPoint1 == src) {
cur = cur->_endPoint1Next;
}
else {
cur = cur->_endPoint2Next;
}
}
if (nullptr == cur) {
return false;
}
if (pre == _graph[src]) {
pre->_endPoint1Next = back;
}
else if(src==pre->_endPoint1){
pre->_endPoint1Next = back;
}
else {
pre->_endPoint2Next = back;
}
pre = _graph[dest];
cur = _graph[dest]->_endPoint1Next;
back = nullptr;
while (cur) {
if (cur->_endPoint1 == src || cur->_endPoint2 == src) {
if (cur->_endPoint1 == dest) {
back = cur->_endPoint1Next;
}
else {
back = cur->_endPoint2Next;
}
break;
}
pre = cur;
if (cur->_endPoint1 == dest) {
cur = cur->_endPoint1Next;
}
else {
cur = cur->_endPoint2Next;
}
}
if (pre == _graph[dest]) {
pre->_endPoint1Next = back;
}
else if (dest == pre->_endPoint1) {
pre->_endPoint1Next = back;
}
else {
pre->_endPoint2Next = back;
}
delete cur;
return true;
}
private:
vector<multiEdge*> _graph;
};
图的遍历
广度优先遍历
广度优先遍历(Breadth First Search,BFS)的思想为从给定的结点开始,一层一层的往外遍历,类似于树的层序遍历一样,为了避免重复遍历某些结点,我们需要一个数组visit记录该结点是否已经被访问过。同时图中可能存在多个不连通的子图,因此需要我们遍历结束后检查数据visit确保是否所有结点都已经访问,存在未访问结点继续则选择该未结点继续遍历,直至遍历完所有结点。
cpp
void BFS(size_t src) {
if (src >= _graph.size()) {
return;
}
vector<bool> visit(_graph.size(), false);
int i = 0;
for (; i < visit.size(); ++i) {
if (visit[i]) {
src = i + 1;
continue;
}
queue<size_t> q;
q.push(src);
visit[src] = true;
int count = 1;
printf("%d \n", src);
while (!q.empty()) {
edge* cur = _graph[q.front()]->_next;
q.pop();
--count;
while (cur)
{
if (!visit[cur->_vertexEnd]) {
printf("%d ", cur->_vertexEnd);
q.push(cur->_vertexEnd);
visit[cur->_vertexEnd] = true;
}
cur = cur->_next;
}
if (0 == count) {
printf("\n");
count = q.size();
}
}
src = i+1;
}
}
深度优先遍历
深度优先遍历(Depth First Search,DFS)的思想为从给定根节点开始,不断的往深处遍历,直至当前结点相邻的结点全部已经遍历就向上回溯,上一层从其相邻结点时继续寻找一个没有遍历的结点遍历。我们也需要一个数组visit记录该结点是否已经被访问过。同时图中可能存在多个不连通的子图,因此也需要我们遍历结束后检查数据visit确保是否所有结点都已经访问,存在未访问结点继续选择该未访问的结点继续遍历,直至遍历完所有结点。
cpp
void _DFS(size_t src, vector<bool>& visit) {
edge* cur = _graph[src]->_next;
while (cur) {
if (!visit[cur->_vertexEnd]) {
printf("%d ", cur->_vertexEnd);
visit[cur->_vertexEnd] = true;
_DFS(cur->_vertexEnd, visit);
}
cur = cur->_next;
}
}
void DFS(size_t src) {
if (src >= _graph.size()) {
return;
}
vector<bool> visit(_graph.size(), false);
printf("%d ", src);
visit[src] = true;
_DFS(src, visit);
int i = 0;
for (; i < _graph.size(); ++i) {
if (!visit[i]) {
printf("\n");
_DFS(i,visit);
}
}
}
图的应用
最小生成树
连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从生成树中删去任何一条边,生成树就不在连通;反之,在其中引入任何一条新边,都会形成一条回路。若连通图由n个顶点组成,则其生成树必含n个顶点和n-1条边。因此构造最小生成树的准则有三条:
- 只能使用图中的边来构造最小生成树
- 只能使用恰好n-1条边来连接图中的n个顶点
- 选用的n-1条边不能构成回路
最小生成树就是边权值之和最小的生成树,构造最小生成树的算法有Kruskal算法和Prim算法,这两个算法都采用了逐步求解的贪心策略。
考虑到原图可能并不是连通的,需要我们记录已经加入最小生成树的结点,每一次生成一颗最小生成树后,如果还有没有加入最小生成树的结点,就继续以该结点相关的边继续生成最小生成树,直至所有连通图都生成最小生成树,
克鲁斯卡尔算法
克鲁斯卡尔(Kruskal)算法的基本思想是选边:即从图中选择权值最小的边,但要求所选择的边的两个顶点不能同时在已经选择的子图的同一连通分量中,如果最小边有多条,任意选择一条即可,直至所有顶点都在最小生成树里面。
实现思路为:采用一个并查集,让处于同一连通分量的顶点处于同一个集合中,通过并查集选取一条边后将边的两个顶点所在的集合合并,并记录该边,直至选取够n-1条边。为了方便取到最小边,可以考虑将所有的边放到一个优先级队列priority_queue中。
cpp
template<class T>
class unionFindSet {
public:
unionFindSet(vector<T>& datas)
:_elements(datas)
, _union(datas.size(), -1)
{
int i = 0;
for (; i < _elements.size(); ++i) {
_index.insert(make_pair(_elements[i], i));
}
}
~unionFindSet()
{}
//检查2个元素是否在同一个集合当中
bool IsInOneSet(T data1, T data2) {
int root1 = FindRoot(data1);
int root2 = FindRoot(data2);
if (-1 == root1 || -1 == root2) {
return false;
}
return root1 == root2;
}
//合并2个集合
bool Union(T data1, T data2) {
int root1 = FindRoot(data1);
int root2 = FindRoot(data2);
if (-1 == root1 || -1 == root2) {
return false;
}
_union[root1] = root2;
return true;
}
//查找集合的个数
int Count() {
int ans = 0;
for (auto e : _union) {
if (-1 == e) {
++ans;
}
}
return ans;
}
private:
//返回元素data的根节点
int FindRoot(T& data) {
if (_index.end() == _index.find(data)) {
return -1;
}
int pos = _index[data];
while (-1 != _union[pos]) {
pos = _union[pos];
}
if (pos != _index[data]) {
//将该元素连接到根节点下面
_union[_index[data]] = pos;
}
return pos;
}
private:
vector<int> _union; //集合
vector<T> _elements; //将原始数据和下标建立关系的数组
map<T, int> _index; //利用原始数据寻找它在_elemrnts中的下标的关联式容器
};
class edgeCompare {
public:
bool operator()(edge* e1,edge* e2) {
return e1->_weight > e2->_weight;
}
};
void kruskal() {
//为顶点建立一个并查集
vector<int> vertexSet;
int i = 0;
for (; i < _graph.size(); ++i) {
vertexSet.push_back(i);
}
unionFindSet<int> fs(vertexSet);
//对边按权重排序
priority_queue<edge*, vector<edge*>,edgeCompare> pq;
for (i = 0; i < _graph.size(); ++i) {
edge* cur = _graph[i]->_next;
while (cur) {
if (cur->_vertexStart < cur->_vertexEnd) {
pq.push(cur);
}
cur = cur->_next;
}
}
vector<edge*> es;//最小生成树边的集合
//选边
int count = _graph.size() - 1;
while (count--) {
edge* cur = pq.top();
pq.pop();
while (fs.IsInOneSet(cur->_vertexStart, cur->_vertexEnd)) {
cur = pq.top();
pq.pop();
}
fs.Union(cur->_vertexStart, cur->_vertexEnd);
es.push_back(cur);
}
//打印选取的边集合
for (i=0; i < es.size(); ++i) {
printf("%d<--- %d --->%d\n", es[i]->_vertexStart, es[i]->_weight, es[i]->_vertexEnd);
}
}
普里姆算法
普里姆(Prim)算法的基本思想是选顶点:它将顶点分为两类,一类是已经加入最小生成树的结点集合m,另一类则是未加入最小生成树的结点集合n,我们每次挑选这样的边:它的一个端点在m中,另一个端点在n中,然后从挑选出的边中选取权重最小的边作为最小生成树的一条边,直至所有的点加入到最小生成树中。
实现思路为:使用一个集合inSet记录已经加入生成树的顶点,不在inSet集合的顶点就是为加入最小生成树的顶点,使用一个优先级队列pq,每加入一个顶点到inSet中,就将该点相关联所有的边放到优先级队列中以方便取到最小边。开始时任意选取一个顶点放到inSet,按照Prim算法生成最小生成树,直至所有顶点加入最小生成树。
cpp
class edgeCompare {
public:
bool operator()(edge* e1,edge* e2) {
return e1->_weight > e2->_weight;
}
};
void prim() {
set<int> inSet;
inSet.insert(0);
vector<edge*> es;//最小生成树边的集合
priority_queue<edge*, vector<edge*>, edgeCompare> pq;
int i = 0;
edge* cur = _graph[0]->_next;
while (cur) {
if (cur->_vertexStart < cur->_vertexEnd) {
pq.push(cur);
}
cur = cur->_next;
}
int count = _graph.size() - 1;
while (count--) {
cur = pq.top();
pq.pop();
while (!((inSet.end() == inSet.find(cur->_vertexStart))^(inSet.end() == inSet.find(cur->_vertexEnd)))) {
cur = pq.top();
pq.pop();
}
edge* tmp = _graph[cur->_vertexEnd]->_next;
while (tmp) {
pq.push(tmp);
tmp = tmp->_next;
}
es.push_back(cur);
inSet.insert(cur->_vertexStart);
inSet.insert(cur->_vertexEnd);
}
//打印选取的边集合
for (i = 0; i < es.size(); ++i) {
printf("%d<--- %d --->%d\n", es[i]->_vertexStart, es[i]->_weight, es[i]->_vertexEnd);
}
}
最短路径
最短路径问题:从在带权有向图G中的某一顶点出发,找出一条通往另一顶点的最短路径,最短的含义为沿路径各边的权值总和达到最小。求解最短路径的算法有:迪杰斯特拉算法、贝尔曼-福特算法、弗洛伊德算法,迪杰斯特拉算法和贝尔曼-福特算法用于求解单源最短路径问题,所谓单源最短路径就是给定一个起始点,求解这个起始点到其他任意点之间的最短路径,但迪杰斯特拉算法不能求解带负数权值的最小路径,而贝尔曼-福特算法则可以求解带负数权值的最短路径,从效率上来说迪杰斯特拉算法是优于贝尔曼-福特算法的;弗洛伊德算法用于求解多源最短路径问题,所谓多源最短路径就是图中任意两点之间的最短路径。
迪杰斯特拉算法
迪杰斯特拉(Dijkstra)算法的基本思想为:将顶点分为两类,一类是已经确定最短路径的顶点集合m,另一类则是未确定最短路径的顶点集合n,用一个数组distance记录目前已经确定的从源点到各个顶点之间的距离,每次根据distance从n中选取一个路径和最小的顶点v,则当前的距离一定是源点到当前顶点v的最小距离,此时将该顶点v从n中移除加入m中,然后根据顶点v相关联的边更新distance,继续选择顶点,直至所有顶点确定最短路径。
实现思路为:使用一个数组visit标记顶点是否已经找到最短路径,使用数组dist记录目前寻找到的源点到各个顶点的较小距离,使用数组parentPath记录路径中该顶点的上一个顶点。
cpp
void pathPrint(size_t src,vector<size_t>& parentPath) {
if (-1 == src) {
return;
}
pathPrint(parentPath[src], parentPath);
printf("---> %d ",src);
}
void dijkstra(size_t src) {
if (src >= _graph.size()) {
return;
}
vector<bool> visit(_graph.size(), false);
vector<int> dist(_graph.size(), maxWeight);
vector<size_t> parentPath(_graph.size(), -1);
dist[src] = 0;
int count = _graph.size();
int i = 0;
while (count--) {
//在未确定最短路径的顶点中寻找距离最小的顶点
int minIndex = -1;
for (i=0; i < visit.size(); ++i) {
if ((!visit[i]) && (-1 == minIndex || dist[i] < dist[minIndex])) {
minIndex = i;
}
}
visit[minIndex] = true;
//更行距离
edge* cur = _graph[minIndex]->_next;
while (cur) {
if (dist[cur->_vertexStart] + cur->_weight < dist[cur->_vertexEnd]) {
dist[cur->_vertexEnd] = dist[cur->_vertexStart] + cur->_weight;
parentPath[cur->_vertexEnd] = cur->_vertexStart;
}
cur = cur->_next;
}
}
//打印路径
for (i = 0; i < _graph.size(); ++i) {
printf("%d--->%d : ",src,i);
pathPrint(i, parentPath);
printf(" : %d\n", dist[i]);
}
}
迪杰斯特拉算法的空间复杂度为O(n),时间复杂度为O(n^2)
贝尔曼-福特算法
贝尔曼-福特算法(Bellman-Ford)是一种暴力求解算法,不能解决带负数权值回路的图,它的思路为:用一个数组dist记录源点到各个顶点的距离,用Mi表示最短路径中某个目的顶点的前一个顶点,源点S到任意一个目的顶端D的路径为:
S->M1->M2->...->Mi->D
假设我们已经知道了S->Mi的距离,我们就可以更新出当前路径(未必是最短路径)S->D的距离,此时我们暴力的赋予Mi为所有顶点,最后就一定会有一些顶点的距离被更新出来(已经确定最短距离的顶点相邻的顶点的距离一定更新,例如已经确定最短距离的源点相邻的顶点的距离一定被更新出来了,所有更新出来的距离未必是最短距离,但一定有一个顶点的距离是最小距离,但我们无法确定,因为负数权值会让距离减小),如果此时我们在进行N轮(N为顶点个数)上面的操作,最后所有的顶点的最短距离一定更新出来。
它的过程类似于:源节点S相邻的顶点中至少有一个顶点X的最短路径为S->X(此时Mi为S),尽管我们不确定它是谁,但在第一轮更新中我们一定更新出了它的距离,下一轮我们又可以从S和X相邻的顶点中右更新出至少一个顶点的最小距离,尽管我们依旧不确定这个顶点是谁,这样最多N次就一定可以更新出所有顶点的最小距离。
实现思路为:使用一个数组dist记录距离,使用一个数组parentPath记录路径中该顶点的上一个顶点,最后采用2层循环即可,外循环记录轮次,内循环以每一个顶点为Mi,更新它相邻结点的距离,同时我们可以进行一定的优化:即内循环中没有距离更新时就直接跳出外循环。
cpp
void pathPrint(size_t src,vector<size_t>& parentPath) {
if (-1 == src) {
return;
}
pathPrint(parentPath[src], parentPath);
printf("---> %d ",src);
}
void bellman_Ford(size_t src) {
if (src >= _graph.size()) {
return;
}
vector<int> dist(_graph.size(), maxWeight);
vector<size_t> parentPath(_graph.size(), -1);
dist[src] = 0;
int i = 0;
//外循环:轮次
for (i = 0; i < _graph.size(); ++i) {
int j = 0;
int flag = 1;//用于优化,提前跳出循环
//内循环:更新距离
for (j = 0; j < _graph.size(); ++j) {
edge* cur = _graph[j]->_next;
//找某个顶点相邻的顶点
while (cur) {
if (maxWeight != dist[j] &&dist[j] + cur->_weight < dist[cur->_vertexEnd]) {
dist[cur->_vertexEnd] = dist[j] + cur->_weight;
parentPath[cur->_vertexEnd] = j;
flag = 0;
}
cur = cur->_next;
}
}
if (flag) {
break;
}
}
}
贝尔曼-福特算法的空间复杂度为O(n),时间复杂度为O(n^3)(因为如果使用邻接矩阵,找某个顶点的边需要一个循环遍历,如果使用邻接表也需要遍历链表找权值,也要一个循环),它无法解决带负数权值回路的图的最短路径,但这不是算法的问题,而是带负数权值回路的图本身就无法找到最短路径。我们在上面使用一个flag标记位进行优化以提前跳出外层循环,除了这种优化策略之外,还有其他的优化策略,比较著名有:SPFA
弗洛伊德算法
弗洛伊德算法(Floyd-Warshall)用于求图中任意两点之间的最短路径,也可以解决带负数权值的图的最短路径问题,它的思路为: 任意一个顶点S到目的D的路径为:S->M1->M2->...->Mk->D,如果S到D的最短路径经过顶点Mi,我们只需要这样更新距离即可:
Dist[S->D] = min( Dist[S->D] , Dist[S->Mi]+Dist[Mi->D] )
现在我们可以固定Mi为图中任意一个顶点,如果我们让S不断地尝试为图中的每一个顶点,在S每一次确定时,让目的顶点D不断的尝试为图中的每一个顶点,根据上面的更新公式,如果Mi有入边的话,我们至少会把一个直接到Mi的顶点Xin到Mi的最短路径更新出来,同理,如果Mi有出边的话,我们至少会把一个Mi到顶点Xout的最短路径更新出来,Xout为Mi出边中的一个顶点,原因是S一定有机会成为Xin且D也一定有机会成为Mi,同时S也一定有机会成为Mi且D一定有机会成为Xout,根据更新公式我们就一定能把这些最短距离更新出来,只不过我们不确定Xin和Xout是谁,而且任意Yin和Yout之间的距离(该距离不一定是最短距离)也一定可以被记录下来了,Yin为任何直接或前面已经记录的间接到Mi的顶点,Yout为任何Mi可以直接或前面已经记录的间接到达的顶点。
如果我们也让Mi断地尝试为图中的每一个顶点,重复上面的操作,最多N(N为顶点个数)次,就一定可以将任意两点之间的最短距离更新出来。原因是求解过程任何时候都可以归纳为以下情况:
图中a、b、c、d可以没有,也可以是子图,表示M1还没有作为过中间结点Mi,其他顶点都已经作为过中间结点Mi,此时除了M1->M2之间的最短距离被更新出来之外,任何左边绿色边框的顶点到右边红色边框的任意顶点的距离都没有更新,同理,任何右边红色边框的顶点到左边绿色边框的任意顶点的距离也没有更新,此时我们一旦让M1作为中间顶点Mi,顶点a中到顶点d中的距离(a->M2->M2->d)一定会被记录下来,因为距离(M2->M2->d)在M2作为中间结点时已经被记录下来了
cpp
void floyd_Warshall() {
vector<vector<size_t>> parent(_graph.size(), vector<size_t>(_graph.size(), -1));
vector<vector<int>> dist(_graph.size(), vector<int>(_graph.size(), maxWeight));
int m = 0;
//必须要初始化
for (; m < _graph.size(); ++m) {
edge* cur = _graph[m]->_next;
dist[m][m] = 0;
while (cur) {
dist[cur->_vertexStart][cur->_vertexEnd] = cur->_weight;
parent[cur->_vertexStart][cur->_vertexEnd] = cur->_vertexStart;
cur = cur->_next;
}
}
int k = 0;
for (; k < _graph.size(); ++k) {
int i = 0;
for (; i < _graph.size(); ++i) {
int j = 0;
for (; j < _graph.size(); ++j) {
if (maxWeight != dist[i][k] && maxWeight != dist[k][j] && (dist[i][k] + dist[k][j] < dist[i][j])) {
dist[i][j] = dist[i][k] + dist[k][j];
parent[i][j] = parent[k][j];
}
}
}
}
//打印路径
for (m = 0; m < _graph.size(); ++m) {
int n = 0;
for (; n < _graph.size(); ++n) {
printf("%d--->%d : ", m, n);
pathPrint(n, parent[m]);
printf(" : %d\n", dist[m][n]);
}
printf("\n");
}
}
弗洛伊德算法空间复杂度为O (n^2^),时间复杂度为O(n^3^),允许图中路径带有负数权值,求图中任意两点之间的最短路径也可以以图中的每个顶点为源点调用N次迪杰斯特拉算法,但这样做不允许图中路径带有负数权值。