文章目录
一、图的基本概念
- 图是由顶点集合及顶点间的关系组成的一种数据结构: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)是同一条边。注意:无向边(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条边
二、图的储存结构
因为图中既有节点,又有边(节点与节点之间的关系),因此,在图的存储中,只需要保存:节点和边关系即可。
1、邻接矩阵
因为节点与节点之间的关系就是连通与否,即为0或者1,因此邻接矩阵(二维数组)即是:先用一个数组将定点保存,然后采用矩阵来表示节点与节点之间的关系。
注意:
(1)如果有权值的话,相连的边数组保存边的权值,不相连的边一般用无穷大表示。
(2)无向图的邻接矩阵是对称的,有向图则不是。
(3)邻接矩阵的优点是可以快速查找两个顶点是否相连,缺点是如果顶点多而边少,就会造成空间浪费。
代码实现:
bash
// V - 顶点类型,W - 权值类型, MAX_W - 权值最大值,
// Direction - 是否有向 true - 无向 false - 无向
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
typedef Graph<V, W, MAX_W, Direction> Self;
public:
Graph() = default;
Graph(const V* vertexs, size_t n)
{
//初始化
_vertexs.resize(n);
_matrix.resize(n);
for (int i = 0; i < n; i++)
{
//初始化矩阵
_matrix[i].resize(n, MAX_W);
//初始化映射关系
_vertexs[i] = vertexs[i];
_vIndexMap[vertexs[i]] = i;
}
}
//获取映射下标
size_t GetVertexIndex(const V& v)
{
//查找是否存在
auto ret = _vIndexMap.find(v);
//存在返回索引否则返回-1
if (ret != _vIndexMap.end())
return ret->second;
else
cout<<"不存在顶点"<<endl;
return -1;
}
void _AddEdge(size_t srci, size_t dsti, const W& w)
{
//默认有向
_matrix[srci][dsti] = w;
//无向
if (Direction)
{
_matrix[dsti][srci] = w;
}
}
//添加边
void AddEdge(const V& src, const V& dst, const W& w)
{
//获取下标
int srci = GetVertexIndex(src);
int dsti = GetVertexIndex(dst);
//判断是否存在
if (srci >= 0 && dsti >= 0)
_AddEdge(srci, dsti, w);
}
private:
map<V, size_t> _vIndexMap;//元素与下标的映射
vector<V> _vertexs; // 顶点集合
vector<vector<W>> _matrix; //矩阵
}
下面其他算法均使用邻接矩阵实现。
2、邻接表
邻接表:使用数组表示顶点的集合,使用链表表示边的关系。
无向图邻接表存储:
注意:无向图中同一条边在邻接表中出现了两次。如果想知道顶点vi的度,只需要知道顶点vi边链表集合中结点的数目即可。
有向图邻接表存储:
注意:有向图中每条边在邻接表中只出现一次,与顶点vi对应的邻接表所含结点的个数,就是该顶点的出度,也称出度表,要得到vi顶点的入度,必须检测其他所有顶点对应的边链表,看有多少边顶点的dst取值是i。
实现:
只实现出边表(一般只会用到)。
bash
//顶点集合
//W - 权值类型
template<class W>
struct LinkEdge
{
int _srcIndex;
int _dstIndex;
W _w;
LinkEdge<W>* _next;
LinkEdge(int srcIndex , int dstIndex , const W& w)
: _srcIndex(srcIndex)
, _dstIndex(dstIndex)
, _w(w)
, _next(nullptr)
{}
};
//V - 顶点类型,W - 权值类型
// Direction - 是否有向 true - 无向 false - 无向
template<class V, class W, bool Direction = false>
class Graph
{
typedef LinkEdge<W> Edge;
public:
Graph() = default;
//初始化
Graph(const V* vertexs, size_t n)
{
//初始化
_linkTable.resize(n,nullptr);
_vertexs.resize(n);
for (int i = 0; i < n; i++)
{
//初始化映射关系
_vertexs[i] = vertexs[i];
_vIndexMap[vertexs[i]] = i;
}
}
//获取下表
size_t GetVertexIndex(const V& v)
{
//查找是否存在
auto ret = _vIndexMap.find(v);
//存在返回索引否则返回-1
if (ret != _vIndexMap.end())
return ret->second;
else
cout << "不存在该顶点" << endl;
return -1;
}
//添加边
void AddEdge(const V& src, const V& dst, const W& w)
{
//获取映射下标
int srci = GetVertexIndex(src);
int dsti = GetVertexIndex(dst);
if (srci == -1 || dsti == -1)
return;
//使用头插法
//有向图
Edge* srceg = new Edge(srci, dsti, w);
srceg->_next = _linkTable[srci];
_linkTable[srci] = srceg;
//无向图
if (Direction)
{
Edge* dsteg = new Edge(dsti, srci, w);
dsteg->_next = _linkTable[dsti];
_linkTable[dsti] = dsteg;
}
}
private:
map<string, int> _vIndexMap; //顶点与下标的映射关系
vector<V> _vertexs; // 顶点集合
vector<Edge*> _linkTable; //邻接表
};
三、图的遍历
1、广度优先遍历
通过一个顶点一层层不断往外扩。
怎么实现?
(1)通过一个数组记录顶点是否被遍历到。
(2)通过队列,每次将队列的顶点拿出,并将其指向的顶点入队,循环上述操作。
bash
//广度优先遍历
void BFS(const V& src)
{
//获取索引
int srci = GetVertexIndex(src);
//节点数
int n = _vertexs.size();
//标记数组
vector<bool> vis(n);
//通过队列遍历访问
queue<int> q;
q.push(srci);
vis[srci] = true;
while (!q.empty())
{
//一层一层遍历
int sz = q.size();
for (int i = 0; i < sz; i++)
{
//取队头元素
int index = q.front();
q.pop();
cout << index << ": " << _vertexs[index]<<" | ";
//将与队头元素连接且灭访问过的点进队并做标记
for (int j = 0; j < n; j++)
{
if (vis[j] == false && _matrix[index][j] != MAX_W)
{
vis[j] = true;
q.push(j);
}
}
}
cout << endl;
}
}
2、深度优先遍历
怎么实现?
从一个顶点出发,找到下一个顶点,再次以这个顶点出发,重复上述操作,直到找不到下一个顶点就进行回溯。
bash
void _DFS(int index, vector<bool>& vis)
{
cout << index << ": " << _vertexs[index] << endl;
//将index指向且没有访问过的节点进行标配并递归搜索
for (int i = 0; i < _vertexs.size(); i++)
{
if (vis[i] == false && _matrix[index][i] != MAX_W)
{
vis[i] = true;
_DFS(i, vis);
}
}
}
//深度优先搜索
void DFS(const V& v)
{
//标记数组
vector<bool> vis(_vertexs.size());
//获取v的索引
int srci = GetVertexIndex(v);
//进行遍历
_DFS(srci, vis);
//将剩下没有遍历到的节点遍历
for (int i = 0; i < _vertexs.size(); i++)
{
if (vis[i] == false)
_DFS(i, vis);
}
}
四、最小生成树
1、概念
连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从其中删去任何一条边,生成树就不在连通;反之,在其中引入任何一条新边,都会形成一条回路。
若连通图由n个顶点组成,则其生成树必含n个顶点和n-1条边。因此构造最小生成树的准则有三条:
- 只能使用图中的边来构造最小生成树
- 只能使用恰好n-1条边来连接图中的n个顶点
- 选用的n-1条边不能构成回路 构造最小生成树的方法:Kruskal算法和Prim算法。这两个算法都采用了逐步求解的贪心策略。
2、Kruskal算法
任给一个有n个顶点的连通网络N={V,E},首先构造一个由这n个顶点组成、不含任何边的图G={V,NULL},其中每个顶点自成一个连通分量, 其次不断从E中取出权值最小的一条边(若有多条任取其一),若该边的两个顶点来自不同的连通分量,则将此边加入到G中。如此重复,直到所有顶点在同一个连通分量上为止。
核心:每次迭代时,选出一条具有最小权值,且两端点不在同一连通分量上的边,加入生成树。
上图来自算法导论。
怎么实现?
(1)使用一个结构体保存边的信息。
(2)使用优先队列(小的在队头)将所有边进队。
(3)开始挑边,在挑的过程中使用一个并查集(将选顶点和不选的顶点分成两个集合)判断是否成环,直到挑到n-1(n:顶点的个数)条边结束。
c
//并查集
#include<iostream>
#include<vector>
using namespace std;
class UnionFindSet
{
public:
UnionFindSet(size_t size)
:_ufs(size, -1)
{}
// 给一个元素的编号,找到该元素所在集合的名称
int FindRoot(int index)
{
int root = index;
while (_ufs[root] >= 0)
{
root = _ufs[root];
}
while(_ufs[index] >= 0)
{
int p = _ufs[index];
_ufs[index] = root;
index = p;
}
return root;
}
//将两个元素合拼到同一个集合里
bool Union(int x1, int x2)
{
int root1 = FindRoot(x1);
int root2 = FindRoot(x2);
if (root1 == root2)
return false;
//小的并到大的里面
if(abs(_ufs[root1]) < abs(_ufs[root2]))
swap(root1,root2);
_ufs[root1] += _ufs[root2];
_ufs[root2] = root1;
return true;
}
// 数组中负数的个数,即为集合的个数
size_t Count()const
{
size_t ret = 0;
for (int i = 0; i < _ufs.size(); i++)
{
if (_ufs[i] < 0)
ret++;
}
return ret;
}
//判断两个元素是否在同一个集合里
bool IsGather(int x1, int x2)
{
return FindRoot(x1) == FindRoot(x2);
}
private:
std::vector<int> _ufs;
};
//W - 权值类型
template<class W>
struct Edge
{
int _srci;
int _dsti;
W _w;
Edge(int srci, int dsti, const W& w)
: _srci(srci)
, _dsti(dsti)
, _w(w)
{}
//重载大于
bool operator>( const Edge<W>& e) const
{
return _w > e._w;
}
};
//最小生成树 -- Kruskal算法
W Kruskal(Self& minTree)
{
//通过现有的图对minTree进行初始化
int n = _vertexs.size();
minTree._vertexs = _vertexs;
minTree._vIndexMap = _vIndexMap;
minTree._matrix.resize(n);
for (int i = 0; i < n; i++)
{
minTree._matrix[i].resize(n, MAX_W);
}
//使用优先队列保存所有边
priority_queue<Edge, vector<Edge>, greater<Edge>> pq;
for (int i = 0; i < n; i++)
{
for (int j = i; j < n; j++)
{
if (_matrix[i][j] != MAX_W)
{
pq.push(Edge(i, j, _matrix[i][j]));
}
}
}
//通过并查集来判环
UnionFindSet ufs(n);
//开始选边 -- 选n-1条
int count = 0;//记录边的数
W ret = W();//记录权值
while (!pq.empty())
{
//取队头元素
Edge e = pq.top();
pq.pop();
//只要两个点不同时在ufs中说明这条边可选
if (!ufs.IsGather(e._srci, e._dsti))
{
//加入并查集
ufs.Union(e._srci,e._dsti);
//添加边
minTree._AddEdge(e._srci,e._dsti,e._w);
count++;
ret += e._w;
}
//选完边结束
if(count == n-1)
break;
}
return ret;
}
3、Prim算法
上图来自算法导论。
怎么实现
(1)从一个顶点开始,将与其相连的边加入优先队列(小的在队头)。
(2)使用一个vector容器标记顶点是否被访问
(3)出队,通过被指向的边是否被标记来判环(该边的起点肯定已经被标记了,因为该边进队时就是选择被标记的点作为起点的),没被标记,选择,直到选择n-1(n:顶点数)条边。
c
//W - 权值类型
template<class W>
struct Edge
{
int _srci;
int _dsti;
W _w;
Edge(int srci, int dsti, const W& w)
: _srci(srci)
, _dsti(dsti)
, _w(w)
{}
//重载大于
bool operator>( const Edge<W>& e) const
{
return _w > e._w;
}
};
//最小生成树 -- Prim算法
W Prim(Self& minTree, const V& src)
{
//通过现有的图对minTree进行初始化
int n = _vertexs.size();
minTree._vertexs = _vertexs;
minTree._vIndexMap = _vIndexMap;
minTree._matrix.resize(n);
for (int i = 0; i < n; i++)
{
minTree._matrix[i].resize(n, MAX_W);
}
//标记点是否被选
vector<int> vis(n);
int srci = GetVertexIndex(src);
vis[srci] = true;
//使用优先队列保存当前点出去的边
priority_queue<Edge, vector<Edge>, greater<Edge>> pq;
for (int i = 0; i < n; i++)
{
if (_matrix[srci][i] != MAX_W)
{
pq.push(Edge(srci, i, _matrix[srci][i]));
}
}
W ret = W();//记录权值
int count = 0;//记录当前边数
while (!pq.empty())
{
//取出队头元素
Edge e = pq.top();
pq.pop();
//判环 --只要当前边的e._dsti没被标记就行,因为该边是由e._srci点发出的,所以
//e._srci肯定被标志了,不用判断
if (vis[e._dsti] == false)
{
//修改标志
vis[e._dsti] = true;
//添加边
minTree._AddEdge(e._srci, e._dsti, e._w);
//将新加入的点出去的边添加到优先队列中
for (int i = 0; i < n; i++)
{
if (_matrix[e._dsti][i] != MAX_W)
{
pq.push(Edge(e._dsti, i, _matrix[e._dsti][i]));
}
}
ret += e._w;
count++;
}
//选完了结束
if (count == n - 1)
break;
}
return ret;
}
五、最短路径问题
最短路径问题:从在带权有向图G中的某一顶点出发,找出一条通往另一顶点的最短路径,最短也就是沿路径各边的权值总和达到最小。
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算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路 径的最短路径。
上图来自算法导论。
c
//src -- 开始的点 dist -- src到每个点的最短距离
// parentPath -- 记录src到其他点的过程上的最近顶点 如:src -> k -> j j点记录的是k的下标
void Dijkstra(const V& src, vector<W>& dist, vector<int>& parentPath)
{
//初始化
int n = _vertexs.size();
dist.resize(n, MAX_W);
parentPath.resize(n, -1);
//标记数组
vector<bool> vis(n);
//给dist数组srci位置赋最小值,方便第一次找到
int srci = GetVertexIndex(src);
dist[srci] = W();
//n个顶点更新n次
for (int i = 0; i < n; i++)
{
int u = 0;
int min = MAX_W;
//到srci最小的点
for(int j = 0; j < n; j++)
{
if (vis[j] == false && min > dist[j])
{
u = j;
min = dist[j];
}
}
//标志
vis[u] = true;
//松弛操作
// 更新u->其他点(srci->u->其他点)的dist
for (int k = 0; k < n; k++)
{
if (vis[k] == false && _matrix[u][k] != MAX_W &&
dist[k] > dist[u] + _matrix[u][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就是一种暴力求解更新
上图来自算法导论。
c
//src -- 开始的点 dist -- src到每个点的最短距离
// parentPath -- 记录src到其他点的过程上的最近顶点 如:src -> k -> j j点记录的是k的下标
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();
//最多更新n-1次(最坏的情况就是到另一个点需要更新n-1次)
for (int k = 0; k < n-1; k++)
{
//标记,如果没有修改则完成查找最短路径
bool flag = false;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
//找到更小的了,进行修改
if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
{
cout << _vertexs[i] << "->" << _vertexs[j] << ":" << _matrix[i][j] << " | ";
dist[j] = dist[i] + _matrix[i][j];
parentPath[j] = i;
flag = true;
}
}
}
//全部都是最短路径了 - 结束
if (flag == false)
break;
}
//检查有没有负权回路
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
{
return false;
}
}
}
return true;
}
3、多源最短路径--Floyd-Warshall算法
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}取得的一条最短路径。
上图来自算法导论。
即Floyd算法本质是三维动态规划,D[i][j][k]表示从点i到点j只经过0到k个点最短路径,然后建立起转移方程,然后通过空间优化,优化掉最后一维度,变成一个最短路径的迭代算法,最后即得到所以点的最短路。
c
//vvDist -- 记录全部的点到其他点的小权值
//vvParentPath -- 记录全部点到其他点的过程上的最近顶点 如:src -> k -> j j点记录的是k的下标
void FloydWarShall(vector<vector<W>>& vvDist, vector<vector<int>>&
vvParentPath)
{
// 初始化
size_t n = _vertexs.size();
vvDist.resize(n);
vvParentPath.resize(n);
//初始化权值和路径矩阵
for (size_t i = 0; i < n; ++i)
{
vvDist[i].resize(n, MAX_W);
vvParentPath[i].resize(n, -1);
}
//将相连的边连在一起
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
if (_matrix[i][j] != MAX_W)
{
vvDist[i][j] = _matrix[i][j];
vvParentPath[i][j] = i;
}
else if (i == j)
{
vvDist[i][j] = W();
vvParentPath[i][j] = -1;
}
}
}
//将k作为中转点依次遍历
for (int k = 0; k < n; k++)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
//i ->( k )-> 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];
}
}
}
}
}
六、拓扑排序
1、概念
拓扑排序是对有向无环图(DAG)顶点的一种排序。 在一个DAG中,如果存在一条有向边(u, v),那么在拓扑排序的结果中,顶点u会排在顶点v的前面。它主要用于解决任务调度、课程学习顺序等依赖关系问题(就是找做事情的先后顺序),通常可以用深度优先搜索(DFS)或 Kahn算法来实现拓扑排序。
注意:拓扑排序的顺序不唯一。
2、如何排序
以下是使用 Kahn 算法实现拓扑排序的步骤:
- 。
遍历有向图中的所有边,对于每条边(u, v),将顶点 v 的入度加一。- 将入度为 0 的顶点加入队列。
初始化一个队列,用于存储入度为 0 的顶点。遍历所有顶点,将入度为 0 的顶点加入队列。- 进行拓扑排序。
创建一个空列表,用于存储排序后的顶点。当队列不为空时,从队列中取出一个顶点 u。将顶点 u 加入排序后的列表中。遍历顶点 u 的所有邻接顶点 v,将顶点 v 的入度减一。如果顶点 v 的入度变为 0,则将其加入队列。- 检查是否存在有向环。
如果排序后的列表中顶点的数量与图中的顶点数量相同,则说明图中不存在有向环,拓扑排序成功。否则,说明图中存在有向环,无法进行拓扑排序。
3、实现
c
//拓扑排序
//graph数组 -- 下标指向一个vector<int>,vector<int> - 该下标顶点指向的顶点
//如0 -> {1,2} ,在图中有两条边(0,1),(0,2)
vector<int> topologicalSort(vector<vector<int>>& graph)
{
//1、统计入度
int n = graph.size();
vector<int> in(n, 0);
for (int i = 0; i < n; i++)
{
for (auto e : graph[i])
in[e]++;
}
//2、将入度为0的进队列
queue<int> q;
for (int i = 0; i < n; i++)
{
if (in[i] == 0)
q.push(i);
}
//3、拓扑排序
vector<int> ret;
while (!q.empty())
{
//出队
int front = q.front();
q.pop();
//加入
ret.push_back(front);
//将front指向的入度减1
for (auto e : graph[front])
{
in[e]--;
//再次统计入度为0的顶点
if (in[e] == 0)
q.push(e);
}
}
//4、判断是否存在有向环
if (ret.size() < n)
return {};
return ret;
}
4、应用
题目:课程表
使用算法:拓扑排序
这题多了一步就是需要我们自己建图,遍历prerequisites使用vector<vector> 或者map<int,vector>,将图建(如顶点a指向其他顶点的集合)出来,其他步骤和上述代码差不多了。
代码实现:
c
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites)
{
//1、建图
vector<vector<int>> graph(numCourses);
int n = prerequisites.size();
for(int i = 0; i < n; i++)
{
int a = prerequisites[i][0];
int b = prerequisites[i][1];
graph[b].push_back(a);
}
//统计入度
vector<int> in(numCourses, 0);
for (int i = 0; i < numCourses; i++)
{
for (auto e : graph[i])
{
in[e]++;
}
}
//3、将入度为0的进队列
queue<int> q;
for (int i = 0; i < numCourses; i++)
{
if (in[i] == 0)
q.push(i);
}
//4、拓扑排序
while (!q.empty())
{
//出队
int front = q.front();
q.pop();
//将front指向的入度减1
for (auto e : graph[front])
{
in[e]--;
//再次统计入度为0的顶点
if (in[e] == 0)
q.push(e);
}
}
//4、判断是否存在有向环
for(int i = 0; i < numCourses; i++)
{
if(in[i]) return false;
}
return true;
}
};