图的基本概念
图是由顶点集合及顶点间的关系组成的一种数据结构:G=(V,E)
其中:顶点集合V,边集合E
V={x|x属于某个数据对象集}
E={(x,y)|x,y属于V}
(x,y)表示点x到点y的一条双向通路,是无方向的
顶点:图中的节点,第几个顶点记作vi
两个顶点vi和vj相关联称为顶点vi到顶点vj之间的一条边
图分为有向图和无向图
在有向图中,顶点对
<x,y>
是有序的,顶点对<x,y>
称为顶点x到y的一条边<x,y>
和<y,x>
是两条不同的边。在无向图中,顶点对
(x,y)
称为顶点x和顶点y相关联的一条边,边是没有方向的,(x,y)
与(y,x)
是同一条边。完全图:在无向图的基础上,任意两个顶点之间有且仅有一条边——有n*(n-1)/2条边,则称此图为无向图。 在有向图中,任意两个顶点之间有且仅有方向相反的边——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的度是指与它相关联的边的条数,记作
dev(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都存在一条路径,同时vj到vi也存储一条路径,则称此图是强连通图。
生成树:一个连通图的最小连通子图称为该图的生成树。有n个顶点的连通图的生成树有n个顶点和n-1条边。
图的存储结构
图要怎么进行存储呢?
对于节点只需要连续的空间即可,对于边的存储,有下面几种方法。
- 用矩阵存储——邻接矩阵
- 用链表存储——邻接表
邻接矩阵
邻接矩阵就是用一个二维数组存储节点和节点之间的关系在无向图中,邻接矩阵是对称的 如果边带有权值,并且两个节点之间是连通的,边的关系就用权值代替,如果两个顶点不通,则使用无穷大代替 用邻接矩阵存储可以快速只读两个顶点之间的关系,但是当顶点比较多的时候、边比较少的时候,不仅浪费空间,而且无法快速知道一个顶点和多少个边相连
实现:
用数组存储点
用二维数组存储边
用map进行转换——把其他类型的数据与下标进行对应
成员函数及其构造函数
//V点,W边的权值
template<class V, class W,W MAX_W=INT_MAX,bool Direction=false>
class Graph
{
vector<V> _vertex;
map<V, int> _vertextosub;
vector<vector<W>> _edge;
public:
Graph(const V* str,int size)
{
//把顶点和坐标进行对应
_vertex.resize(size);
for (int i = 0; i < size; i++)
{
_vertex[i] = str[i];
_vertextosub[str[i]] = i;
}
//边都初始为最大,表示不相连
_edge.resize(size);
for (auto& x : _edge)
x.resize(size, MAX_W);
}
int Getvertexsub(const V& x)
{
//找到返回,找不到返回-1
auto iterator = _vertextosub.find(x);
if (iterator == _vertextosub.end())
return -1;
return iterator->second;
}
void AddEdge(const V& src,const V& dst,const W& w)
{
//找到顶点的下标
int srcsub = Getvertexsub(src);
int detsub = Getvertexsub(dst);
if (srcsub == -1 && detsub == -1)
{
perror("顶点获取下标失败");
exit(-1);
}
//成功进行添加权值
_edge[srcsub][detsub] = w;
if (Direction == false)
_edge[detsub][srcsub] = w;
}
};
邻接表
邻接表使用数组表示顶点的集合,使用链表表示边的关系
实现
template<class W>
struct Edge//边的结构
{
int _scr;//起点
int _dst;//终点
W _w;//权值
Edge* _next;
Edge(int scr, int dst, const W& w)
:_scr(scr),
_dst(dst),
_w(w),
_next(nullptr)
{}
};
template<class V, class W, bool Direction =false >
class Graph
{
typedef Edge<W> Edge;
vector<V> _vertex;
map<V, int> _vertextosub;
vector<Edge*> _edge;//边
public:
Graph(const V* str,int size )
{
_vertex.resize(size);
for (int i = 0; i < size; i++)
{
_vertex[i] = str[i];
_vertextosub[str[i]] = i;
}
_edge.resize(size, nullptr);
}
int Getvertexsub(const V& x)
{
//找到返回,找不到返回-1
auto iterator = _vertextosub.find(x);
if (iterator == _vertextosub.end())
return -1;
return iterator->second;
}
void AddEdge(const V& src, const V& dst, const W& w)
{
//找到顶点的下标
int srcsub = Getvertexsub(src);
int detsub = Getvertexsub(dst);
if (srcsub == -1 && detsub == -1)
{
perror("顶点获取下标失败");
exit(-1);
}
//成功进行添加权值
Edge* e = new Edge(srcsub, detsub, w);
//头插
e->_next = _edge[srcsub];
_edge[srcsub] = e;
if (Direction == false)
{
e = new Edge(detsub, srcsub, w);
e->_next = _edge[detsub];
_edge[detsub] = e;
}
}
}
两者的优缺点
邻接矩阵:
邻接矩阵的方式非常适合稠密图邻接矩阵可以在O(1)的基础上判断两个顶点的连接关系,并取得权值 相对而言不适合查找一个顶点连接的所有边——O(N)
邻接表:
适合存储稀疏图适合查找一个顶点连接出去的所有边 不适合确定两个顶点是否相连及权值
图的遍历
这里我们用邻接矩阵来进行深度和广度搜索
BFS(广度)
广度遍历相当于一层一层的进行遍历(根据节点遍历它所在该行的二维数组),那么我们就要用到队列,但是我们在遍历的时候要注意不要形成环,所以遍历的时候要注意标记,标记已经遍历的节点
void BFS(const V& src)
{
int srcsub = Getvertexsub(src);
if (srcsub < 0)
{
perror("不存在该节点");
exit(-1);
}
queue<int> q;//存每层的节点下标
vector<bool> v(_vertex.size(),false);//存节点是不是被访问过了
//遍历二维数组
q.push(srcsub);
v[srcsub] = true;
int level = 1;
while (!q.empty())
{
for (int x = 1; x <= level; x++)
{
int i = q.front();
cout << _vertex[i] << "->" << i << ';';
q.pop();
for (int j = 0; j < _edge.size(); j++)
{
if (_edge[i][j] != MAX_W && v[j] != true)
{
q.push(j);
v[j] = true;
}
}
}
level = q.size();
cout << endl;
}
cout << endl;
for (int i = 0; i < v.size(); i++)
{
if (v[i] == false)
cout << _vertex[i] << "->" << i << endl;
}
}
DFS(深度)
深度遍历也是有肯存在环路的问题,所以也要进行节点的标记
void _DFS(const V& src,vector<bool>& v)
{
int srcsub = Getvertexsub(src);
if (srcsub < 0)
{
perror("不存在该节点");
exit(-1);
}
cout << _vertex[srcsub] << "->" << srcsub << endl;
v[srcsub] = true;
for (int i = 0; i < _edge.size(); i++)
{
if (_edge[srcsub][i] != MAX_W && v[i] != true)
_DFS(_vertex[i],v);
}
}
void DFS(const V& src)
{
vector<bool> v(_vertex.size(), false);
_DFS(src, v);
}
最小生成树
连通图中的每一棵生成树,都是原图的一个极大无环子图 。即:从其中删除任何一条边,生成树就不在连通;反之,在其中引入任何一条新边,都会形成一条回路。
若连通图由n个顶点组成,则其生成树必含有n个顶点和n-1条边。
构成最小生成树的准则有三条:
- 只能使用图中的边来构造最小生成树
- 只能使用恰好n-1条边来连接图中的n个顶点
- 选用的n-1条边不能构成回路
Kruskal算法
从所有边中,优先选取权值最小的那个边,注意选取的时候要注意不要形成环
W Kruskal(Self& mintree)
{
//首先完成最小生成树的构造
int n = _vertex.size();
mintree._vertex = _vertex;
mintree._vertextosub = _vertextosub;
mintree._edge.resize(n);
for (int i = 0; i < n; i++)
mintree._edge[i].resize(n, MAX_W);
//用优先级队列获得权值的大小排列——小到大
priority_queue<edge,vector<edge>,greater<edge>> pq;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
//i<j对于无向图来说,不需要保存重复的数据
//对于有向图来说,不需要遍历下面一半数据
//因为i表示的是节点的起点,j为节点的终点
if (i < j && _edge[i][j] != MAX_W)
{
pq.push(edge(i, j, _edge[i][j]));
}
}
}
//构造最小生成树
W minw = W();
int Size = 0;
//并查集
UnionSet Uniset(n);
Edge<W> t;
while (!pq.empty())
{
t = pq.top();
int scr = t._scr;
int dst = t._dst;
W w = t._w;
if (Uniset.Find(scr)!=Uniset.Find(dst))
{
cout << _vertex[scr] << "->" << _vertex[dst] << ":" << w << endl;
Uniset.Union(scr, dst);
mintree._edge[scr][dst] = w;
Size++;
minw += w;
}
pq.pop();
}
//最小生成树的边数一定是n-1条
if (Size != n - 1)
return W();
return minw;
}
Prim算法
该算法的实现是:
把这些节点分成两部分,
一部分是生成树的节点——记作X集合
另一部分是出去除去生成树的节点的部分——记作Y集合
从X中找与Y连接中最短的边,然后把Y中的该点加入到X集合中,从Y集合中去除该节点
在下面我们实现的时候,没有用Y集合,因为完全没有必要
W Prim(Self& mintree,const V& src)
{
int n = _vertex.size();//有多少节点
mintree._vertex = _vertex;
mintree._vertextosub = _vertextosub;
mintree._edge.clear();
mintree._edge.resize(n);
for (int i = 0; i < n; i++)
mintree._edge[i].resize(n, MAX_W);
vector<bool> X(n, false);
int srcsub = Getvertexsub(src);//起点下标
int dstsub;//终点下标
W w = W();//权值
W minw = W();
X[srcsub] = true;
int size = 0;//边的数
priority_queue<edge, vector<edge>, greater<edge>> pq;
for (int i = 0; i < n; i++)
{
if (_edge[srcsub][i] != MAX_W)
pq.push(edge(srcsub, i, _edge[srcsub][i]));
}
while (!pq.empty())
{
edge e = pq.top();
srcsub = e._scr;
dstsub = e._dst;
w = e._w;
pq.pop();
//这个最小的边是终点是没有被选取的
if (X[dstsub] == true)
continue;
//连接该边
mintree._edge[srcsub][dstsub] = w;
X[dstsub] = true;
minw += w;
size++;
for (int i = 0; i < n; i++)
{
if (_edge[dstsub][i] != MAX_W&&X[i]==false)
pq.push(edge(dstsub, i, _edge[dstsub][i]));
}
}
if (size == n - 1)
return minw;
else
return W();
}
最短路径问题
最短路径问题:从在带权有向图G中的某一顶点出发,找出一条通往另一个顶点的最短路径,最短也就是沿路径各边的权值总和达到最小
单源最短路径算法
确定起点的最短路径问题——单源最短路径
Dijkstra算法
该算法只能解决非负权值的问题——权值不是负数
刚开始的时候,到所有点的路径和都是无穷大,表示无法到达。对于起始点,到该点的最短路径为0。然后去更新与之相连的其他节点的路径。 下一次选取到其他节点最短的路径为下一个路过的节点,同时更新它所连接的节点的路径,如果比之前的小,就进行更新,否则不进行更新。反复此过程,直到所有的节点的访问完,最短路径也就出来了。
void Dijkstra(const V& src,vector<int>& dist,vector<int>& path)
{
int n = _vertex.size();
//初始化
dist.resize(n, MAX_W);
path.resize(n, -1);
//节点是否访问过
vector<bool> v(n, false);
//起点初始化
int srcsub = Getvertexsub(src);
dist[srcsub] = 0;//最小路径的权值
path[srcsub] = srcsub;
for (int j = 0; j < n; j++)
{
//对起点的相连的边进行松弛操作
for (int i = 0; i < n; i++)
{
if (_edge[srcsub][i] != MAX_W && v[i] == false
&& _edge[srcsub][i] + dist[srcsub] < dist[i])
{
dist[i] = _edge[srcsub][i] + dist[srcsub];
path[i] = srcsub;
}
}
//获取下一个最小的路径起点
W minsub = MAX_W;
for (int i = 0; i < n; i++)
{
if (v[i] == false && dist[i] < minsub)
{
srcsub = i;
minsub = dist[i];
}
}
v[srcsub] = true;
}
}
Bellman-Ford算法
该算法就是解决Dijkstra不能解决负权值的问题——暴力求解的方法
该算法是怎么工作的呢?
s->i是我们求的是s经过多个点到i的路径权值i->j表示的所有路径 当我们更新出s->i的路径的时候,同时更新用i->j对所有路径进行更新操作(松弛操作) 这样就能得出s->j的路径权值——这种求出的方法是s->i->j的最短,但是如果出现k(k) s->k的路径权值是由s->i求出的。那么我们还需要再次从新开始遍历——更新路径,每次更新会更新一条,最坏的情况是更新n条,就是n次。 要注意负权回路的问题,该算法也能检查负权回路
bool BellmanFord(const V& src,vector<int>& dist,vector<int>& path)
{
int n = _vertex.size();
//初始化
dist.resize(n, MAX_W);
path.resize(n, -1);
//起点初始化
int srcsub = Getvertexsub(src);
dist[srcsub] = 0;//最小路径的权值
path[srcsub] = srcsub;
bool flag = true;
for (int k = 0; k < n; k++)
{
flag = true;//判断是否全部更新完成
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
//i->j的路径
//dist[i]表示s->i的路径权值,松弛操作进行更新其他边
if (_edge[i][j] != MAX_W&&dist[i]+ _edge[i][j]<dist[j])
{
dist[j] = dist[i] + _edge[i][j];
path[j] = i;
flag = false;
}
}
}
if (flag)
break;
}
//判断是否有负权回路
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
if (_edge[i][j] != MAX_W && dist[i] + _edge[i][j] < dist[j])
{
return false;
}
}
}
return true;
}
多元最短路径算法——Floyd-Warshall算法
全局最短路径问题——多源最短路径,求图中所有的最短路径
该算法用的是动态规划的思想,求的是任意两点之间的权值
具体求法:
假设我们求的是i->j那么可能存在节点k,使得i->k->j 所有i->j之间的路径可以用i->k加上k->j来求 Dij=min(Dik+Dkj,Dij) 那么路径和权值都应该用二维进行存储(压缩空间之后)
void FloydWarshall(vector<vector<int>>& dist,vector<vector<int>>& path)
{
//初始化
int n = _vertex.size();
dist.resize(n);
path.resize(n);
for (int i = 0; i < n; i++)
{
dist[i].resize(n, MAX_W);
path[i].resize(n, -1);
for (int j = 0; j < n; j++)
{
if (_edge[i][j] != MAX_W)
{
dist[i][j] = _edge[i][j];
path[i][j] = i;
}
if (i == j)
{
dist[i][j] = 0;
}
}
}
//依次用k作为中间转换
for (int k = 0; k < n; k++)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
if (dist[i][k] != MAX_W && dist[k][j] != MAX_W
&& dist[i][k] + dist[k][j] < dist[i][j])
{
dist[i][j] = dist[i][k] + dist[k][j];
//path[i][j]存的是j对应的父节点,而此时父节点应该存储的“k”
//该“k”存储在path中
path[i][j] = path[k][j];
}
}
}
}
}
标签:dist,int,路径,edge,顶点,srcsub,数据结构
From: https://blog.51cto.com/u_15869810/7922510