当前位置: 首页 > news >正文

数据结构 - 图

文章目录

    • 一、图的基本概念
    • 二、图的储存结构
      • 1、邻接矩阵
      • 2、邻接表
    • 三、图的遍历
      • 1、广度优先遍历
      • 2、深度优先遍历
    • 四、最小生成树
      • 1、概念
      • 2、Kruskal算法
      • 3、Prim算法
    • 五、最短路径问题
      • 1、单源最短路径--Dijkstra算法
      • 2、单源最短路径--Bellman-Ford算法
      • 3、多源最短路径--Floyd-Warshall算法
    • 六、拓扑排序
      • 1、概念
      • 2、如何排序
      • 3、实现
      • 4、应用

一、图的基本概念

  1. 图是由顶点集合及顶点间的关系组成的一种数据结构: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)是有方向的。
  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>和<y, x>。
  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均不重复,则称这样的路径为简单路径。若路径上第一个顶点v1和最后一个顶点vm重合,则称这样的路径为回路或环。
  10. 子图:设图G = {V, E}和图G1 = {V1,E1},若V1属于V且E1属于E,则称G1是G的子图。
  11. 连通图:在无向图中,若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图。
  12. 强连通图:在有向图中,若在每一对顶点vi和vj之间都存在一条从vi到vj的路径,也存在一条从vj到vi的路径,则称此图是强连通图。
  13. 生成树:一个连通图的最小连通子图称作该图的生成树。有n个顶点的连通图的生成树有n个顶点和n-1条边

在这里插入图片描述

在这里插入图片描述

二、图的储存结构

因为图中既有节点,又有边(节点与节点之间的关系),因此,在图的存储中,只需要保存:节点和边关系即可。

1、邻接矩阵

因为节点与节点之间的关系就是连通与否,即为0或者1,因此邻接矩阵(二维数组)即是:先用一个数组将定点保存,然后采用矩阵来表示节点与节点之间的关系。
在这里插入图片描述
注意:
(1)如果有权值的话,相连的边数组保存边的权值,不相连的边一般用无穷大表示。
(2)无向图的邻接矩阵是对称的,有向图则不是。
(3)邻接矩阵的优点是可以快速查找两个顶点是否相连,缺点是如果顶点多而边少,就会造成空间浪费。
代码实现:

// 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);//存在返回索引否则返回-1if (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。

实现:
只实现出边表(一般只会用到)。

//顶点集合//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);//存在返回索引否则返回-1if (ret != _vIndexMap.end())return ret->second;elsecout << "不存在该顶点" << 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)通过队列,每次将队列的顶点拿出,并将其指向的顶点入队,循环上述操作。

//广度优先遍历
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、深度优先遍历

在这里插入图片描述
怎么实现?
从一个顶点出发,找到下一个顶点,再次以这个顶点出发,重复上述操作,直到找不到下一个顶点就进行回溯。

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条边。因此构造最小生成树的准则有三条:

  1. 只能使用图中的边来构造最小生成树
  2. 只能使用恰好n-1条边来连接图中的n个顶点
  3. 选用的n-1条边不能构成回路 构造最小生成树的方法:Kruskal算法和Prim算法。这两个算法都采用了逐步求解的贪心策略。

2、Kruskal算法

任给一个有n个顶点的连通网络N={V,E},首先构造一个由这n个顶点组成、不含任何边的图G={V,NULL},其中每个顶点自成一个连通分量, 其次不断从E中取出权值最小的一条边(若有多条任取其一),若该边的两个顶点来自不同的连通分量,则将此边加入到G中。如此重复,直到所有顶点在同一个连通分量上为止。
核心:每次迭代时,选出一条具有最小权值,且两端点不在同一连通分量上的边,加入生成树。

在这里插入图片描述
上图来自算法导论。

怎么实现?
(1)使用一个结构体保存边的信息。
(2)使用优先队列(小的在队头)将所有边进队。
(3)开始挑边,在挑的过程中使用一个并查集(将选顶点和不选的顶点分成两个集合)判断是否成环,直到挑到n-1(n:顶点的个数)条边结束。

//并查集
#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:顶点数)条边。

//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算法

  1. 单源最短路径问题:给定一个图G = ( V , E ) G=(V,E)G=(V,E),求源结点s ∈ V s∈Vs∈V到图 中每个结点v ∈ V v∈Vv∈V的最短路径。Dijkstra算法就适用于解决带权重的有向图上的单源最短 路径问题,同时算法要求图中所有边的权重非负。一般在求解最短路径的时候都是已知一个起点和一个终点,所以使用Dijkstra算法求解过后也就得到了所需起点到终点的最短路径。
  2. 针对一个带权有向图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中,所 以该算法使用的是贪心策略。
  3. Dijkstra算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路 径的最短路径。

在这里插入图片描述
上图来自算法导论。

//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->其他点)的distfor (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就是一种暴力求解更新
在这里插入图片描述

上图来自算法导论。

//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个点最短路径,然后建立起转移方程,然后通过空间优化,优化掉最后一维度,变成一个最短路径的迭代算法,最后即得到所以点的最短路。

//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 )-> jif (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 算法实现拓扑排序的步骤:


  1. 遍历有向图中的所有边,对于每条边(u, v),将顶点 v 的入度加一。
  2. 将入度为 0 的顶点加入队列。
    初始化一个队列,用于存储入度为 0 的顶点。遍历所有顶点,将入度为 0 的顶点加入队列。
  3. 进行拓扑排序。
    创建一个空列表,用于存储排序后的顶点。当队列不为空时,从队列中取出一个顶点 u。将顶点 u 加入排序后的列表中。遍历顶点 u 的所有邻接顶点 v,将顶点 v 的入度减一。如果顶点 v 的入度变为 0,则将其加入队列。
  4. 检查是否存在有向环。
    如果排序后的列表中顶点的数量与图中的顶点数量相同,则说明图中不存在有向环,拓扑排序成功。否则,说明图中存在有向环,无法进行拓扑排序。

3、实现

//拓扑排序
//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指向的入度减1for (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指向其他顶点的集合)出来,其他步骤和上述代码差不多了。

代码实现:

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指向的入度减1for (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;}
};

相关文章:

数据结构 - 图

文章目录 一、图的基本概念二、图的储存结构1、邻接矩阵2、邻接表 三、图的遍历1、广度优先遍历2、深度优先遍历 四、最小生成树1、概念2、Kruskal算法3、Prim算法 五、最短路径问题1、单源最短路径--Dijkstra算法2、单源最短路径--Bellman-Ford算法3、多源最短路径--Floyd-War…...

如何在Linux系统中管理和优化Swap空间

如何在Linux系统中管理和优化Swap空间 Swap空间简介 检查Swap空间 创建Swap空间 创建Swap文件 创建Swap分区 配置Swap空间 编辑fstab文件 设置vm.swappiness Swap使用策略 调整vm.vfs_cache_pressure 设置vm.min_free_kbytes Swap空间的监控 使用top命令 使用free命令 Swap…...

瑞格智慧心理服务平台 NPreenSMSList.asmx sql注入漏洞复现

0x01 产品描述&#xff1a; ‌ 瑞格智慧心理服务平台‌是一个集心理测评、心理咨询、心理危机干预、心理放松训练等功能于一体的综合性心理健康服务平台。该平台由北京瑞格心灵科技有限公司开发&#xff0c;旨在为用户提供全方位的心理健康服务。0x02 漏洞描述&#xff1a;…...

大模型是否具备推理能力?解读苹果新论文:GSM-Symbolic和GSM8K

在人工智能领域&#xff0c;大模型的推理能力一直备受关注。OpenAI的GPT-4和其他大模型的表现令人惊叹&#xff0c;但究竟是否具备真正的数学推理和抽象逻辑能力&#xff1f;最近&#xff0c;苹果的研究人员发表了一篇题为“GSM-Symbolic&#xff1a;理解大语言模型中数学推理的…...

自动化部署-02-jenkins部署微服务

文章目录 前言一、配置SSH-KEY1.1 操作jenkins所在服务器1.2 操作github1.3 验证 二、服务器安装git三、jenkins页面安装maven四、页面配置自动化任务4.1 新建任务4.2 选择4.3 配置参数4.4 配置脚本 五、执行任务5.1 点击执行按钮5.2 填写参数5.3 查看日志 六、查看服务器文件七…...

HTB:Analytics[WriteUP]

目录 连接至HTB服务器并启动靶机 1.How many open TCP ports are listening on Analytics? 2.What subdomain is configured to provide a different application on the target web server? 3.What application is running on data.analytical.htb? 4.What version of…...

【每日题解】3211. 生成不含相邻零的二进制字符串

给你一个正整数 n。 如果一个二进制字符串 x 的所有长度为 2 的 子字符串 中包含 至少 一个 "1"&#xff0c;则称 x 是一个 有效 字符串。 返回所有长度为 n 的 有效 字符串&#xff0c;可以以任意顺序排列。 示例 1&#xff1a; 输入&#xff1a; n 3 输出&a…...

Nginx、Tomcat等项目部署问题及解决方案详解

目录 前言1. Nginx部署后未按预期显示结果1.1 查看Nginx的启动情况1.2 解决启动失败的常见原因 2. 端口开启问题2.1 Windows环境下的端口开放2.2 Linux环境下的端口开放 3. 重视日志分析3.1 Nginx日志分析3.2 Tomcat日志分析 4. 开发环境与部署后运行结果不同4.1 开发环境与生产…...

【PythonWeb开发】Flask-RESTful参数解析

flask-restful中的reqparse.RequestParser是一个用于解析和验证参数的工具。它可以帮助开发者从请求中提取参数&#xff0c;并确保这些参数符合预期的格式和类型。参数解析的意思就是规范化传入的参数并获取到这些参数。 一、什么是 reqparse.RequestParser&#xff1f; reqpa…...

gcc与mingw64版本介绍

三类编译器 GCC&#xff0c;全称为GNU Compiler Collection&#xff0c;是一个强大的编译器集合&#xff0c;它不仅支持C和C语言&#xff0c;还支持Fortran、Ada、Java等多种编程语言的编译。在GCC工具链中&#xff0c;gcc和g是两个核心的编译器工具。gcc是专门用于编译C语言程…...

CSS3新增长度单位

CSS3新增长度单位 rem&#xff1a;根元素字体的倍数&#xff0c;只与根元素字体大小有关&#xff1b;vw&#xff1a;占视口宽度的百分比&#xff1b;vh&#xff1a;占视口高度的百分比&#xff1b;vmax&#xff1a;占视口中宽和高最大的百分比&#xff1b;vmin&#xff1a;占视…...

【Spring】创建Spring项目前的配置工作

&#x1f94a;作者&#xff1a;一只爱打拳的程序猿&#xff0c;Java领域新星创作者&#xff0c;CSDN、阿里云社区优质创作者。 &#x1f93c;文章收录于&#xff1a;Spring 目录 1.下载Spring Initializr 2.配置Spring国内源 3.添加Spring框架的支持(pom.xml) 4.刷新Maven仓…...

docker 安装部署 nginx

命令 docker run \ -p 15008:80 \ --name nginx1.21.6 \ -v /iepms/nginx/conf/nginx.conf:/etc/nginx/nginx.conf \ -v /iepms/nginx/conf/conf.d:/etc/nginx/conf.d \ -v /iepms/nginx/log:/var/log/nginx \ -v /iepms/nginx/html:/usr/share/nginx/html \ -d 192.168.1.103…...

黑马数据库学习笔记

课程地址 &#xff08;基础篇&#xff09;MySQL的启动 mysql 默认使用 3306 端口 在 centos下&#xff0c;启动 mysql 数据库&#xff1a;service mysqld start; 查看状态/启动/停止/重启&#xff1a;systemctl status/start/stop/restart mysqld; 登录到mysql数据库&…...

MYSQL-SQL-03-DQL(Data Query Language,数据查询语言)(单表查询)

DQL&#xff08;数据查询语言&#xff09; DQL英文全称是Data Query Language(数据查询语言)&#xff0c;数据查询语言&#xff0c;用来查询数据库中表的记录。 查询关键字: SELECT 在一个正常的业务系统中&#xff0c;查询操作的频次是要远高于增删改的&#xff0c;当我们去访…...

【数据结构和算法】三、动态规划原理讲解与实战演练

目录 1、什么是动态规划&#xff1f; 2、动态规划实战演练 2.1 力扣题之爬楼梯问题 &#xff08;1&#xff09;解题思路1: &#xff08;2&#xff09;解题思路2: &#xff08;3&#xff09;动态规划&#xff08;DP&#xff09;&#xff1a;解题思路 &#xff08;4&#x…...

交叉编译 perl-5.40.0(riscv64)

交叉编译 perl-5.40.0&#xff08;riscv64&#xff09; https://arsv.github.io/perl-cross/usage.html https://github.com/arsv/perl-cross 借助 perl-cross 进行交叉编译 https://www.perl.org/get.html#unix_like 这里获取 perl-5.40.0 的源码 https://github.com/arsv/pe…...

Leetcode 搜索旋转排序数组

这段代码是用于解决LeetCode第33题“搜索旋转排序数组”的Java解法。以下是对该算法思想的中文解释&#xff1a; 算法思想 二分查找的基本思路&#xff1a; 由于数组是部分有序的&#xff08;被旋转过&#xff09;&#xff0c;我们可以利用二分查找的思想&#xff0c;逐步缩小…...

Spring Task—定时任务

Spring Task 是 Spring 提供的一种轻量级定时任务调度功能&#xff0c;内置在 Spring 框架中。与 Quartz 等重量级调度框架相比&#xff0c;Spring Task 使用简便&#xff0c;无需额外依赖&#xff0c;适合在简单的调度任务场景中使用。通过注解配置方式&#xff0c;开发者可以…...

Spring Boot 应用开发概述

目录 Spring Boot 应用开发概述 Spring Boot 的核心特性 Spring Boot 的开发模式 Spring Boot 在企业应用开发中的优势 结论 Spring Boot 应用开发概述 Spring Boot 是由 Pivotal 团队开发的一个框架&#xff0c;基于 Spring 框架&#xff0c;旨在简化和加速基于 Spring …...

Chrome谷歌浏览器加载ActiveX控件之allWebDesktop控件介绍

背景 allWebDesktop控件是一款方便用户在线打开各类文档的OA办公控件。它设计比较轻巧&#xff0c;充分利用计算机程序资源打开文档&#xff0c;并将程序窗口嵌入到allWebDesktop控件区域内&#xff0c;从而实现浏览器内打开各类文档效果。 allWebPlugin中间件是一款为用户提供…...

GitHub Star 数量前 5 的开源应用程序生成器

欢迎来的 GitHub Star 数量排名系列文章的第 7 篇——最受欢迎的应用程序生成器。 之前我们已经详细探讨过&#xff1a;在 GitHub 上最受欢迎的——无代码工具、低代码项目、内部工具、CRUD项目、自部署项目和 Airtable 开源替代品。累计超过 50 个优质项目&#xff01;&#…...

DBC文件当中新建CANFD等类型的报文

同学最近有添加CANFD报文的需求&#xff0c;需要用到CANFD类型报文的DBC文件&#xff0c;这下就难住我了&#xff0c;我之前用的DBC文件只有“CAN Standard”“CAN Extended”两种类型&#xff0c;压根没见过FD的。 后来他找到了项目之前的DBC&#xff0c;打开来看&#xff0c…...

基于SpringBoot的房地产销售管理系统【附源码】

基于SpringBoot的房地产销售管理系统&#xff08;源码L文说明文档&#xff09; 目录 4 系统设计 4.1用户登录功能的详细实现 4.2管理员权限的功能实现 4.2.1客户信息管理功能的详细实现 4.2.2房产管理功能的详细实现 4.2.3预约看房功能的详细实现 4.2.4论…...

圆点虚线 Android

参考 https://blog.csdn.net/l_o_s/article/details/73550876 <com.xxx.wwww.weight.PointDividerViewandroid:layout_width"match_parent"android:layout_height"wrap_content"app:PDbackgroundColor"color/white"app:dotColor"color/…...

贵州鑫宏远农业-始终致力于推动现代农业的科技创新与发展

贵州鑫宏远农业科技有限公司&#xff0c;是一家在高科技农业领域深耕细作、锐意进取的企业。自成立以来&#xff0c;我们始终致力于推动现代农业的科技创新与发展&#xff0c;业务全面覆盖农业科学研发、组织培养生产、专业育苗培植、半成品及成品精细化养护、市场销售以及全方…...

程序员做销售,从代码到客户的逆袭之路

大家好&#xff0c;我是小悟。 在这个互联网风起云涌、技术迭代日新月异的时代&#xff0c;“跨界”已然成为一种新潮流。就好似那从天而降的大侠&#xff0c;一不小心就可能横跨了数个充满奇遇与挑战的领域。 想象一下&#xff0c;一个平日里只跟代码打交道的程序员&#xf…...

Flink CDC系列之:理解学习Kubernetes模式

Flink CDC系列之&#xff1a;理解学习Kubernetes模式 准备会话模式启动会话集群设置 Flink CDC提交 Flink CDC Job Kubernetes 是一种流行的容器编排系统&#xff0c;用于自动化计算机应用程序的部署、扩展和管理。Flink 的原生 Kubernetes 集成允许您直接在正在运行的 Kuberne…...

git合并相关操作详解

在使用Git进行分支管理时,合并(merge)操作是非常常见的。下面是Git合并相关的详细步骤和一些常见的场景及注意事项。 一、 基本合并操作 假设我们有两个分支:main 和 feature,希望将 feature 合并到 main 上。 切换到目标分支 首先需要切换到你想合并到的分支。例如,切…...

前端经典【面试题】持续更新HTML、CSS、JS、VUE、FLUTTER、性能优化等

HTML/CSS 面试题 什么是语义化 HTML&#xff1f; 说明&#xff1a;语义化 HTML 使用 HTML 标签来描述内容的含义&#xff0c;而不仅仅是其外观。使用语义化标签可以提高可读性和可访问性&#xff0c;并对 SEO 友好。示例&#xff1a; <header><h1>网站标题</h1&…...

赚钱网站如何做/it教育培训机构

由于公司停了系统的密码认证&#xff0c;所有传输文件只能用其他认证方式。 rsync和scp都可以用自己的认证方式传输数据&#xff0c;rsync传输速度快&#xff0c;但是需要deamon模式运行&#xff0c;配置起来比较慢&#xff0c;而scp可以比较快速的部署两台机器之间的认证&…...

猪八戒上面还是淘宝上做网站技术好/三只松鼠营销案例分析

首页 所有文章 资讯 Web 架构 基础技术 书籍 教程 Java小组 工具资源 - 导航条 - 首页所有文章 资讯Web 架构基础技术 书籍教程 Java小组工具资源Integer.valueOf(String) 方法之惑 2014/02/11 | 分类&#xff1a; 基础技术 | 21 条评论 | 标签&#xff1a; 技术问答 分享到&am…...

沧州住房和城乡建设部网站/百度推广优化师

下列表格中除了个别函数外&#xff0c;其余函数都是图像处理工具箱提供的关于图像处理的函数&#xff0c;现摘录到此以备查找。 表1 图像显示函数名功能说明函数名功能说明colorbar颜色条显示montage按矩形剪辑方式显示多帧图像getimage从坐标系中获取图像数据immovie从多帧索引…...

洛阳做网站公司汉狮价格/创建软件平台该怎么做

最近做U800电话的二次开发&#xff0c;需要调用厂商的C函数库来打电话&#xff0c;后来想加入通话录音功能&#xff0c;但发现程序默认生产的WAV文件过大&#xff0c;又找了个WAV转MP3的C函数库程序&#xff0c;出了点问题。下面是转MP3的程序接口&#xff08;头文件&#xff0…...

社区网站做的比较好的有哪些/优化大师优化项目有哪些

江苏省计算机一级考试复习资料第一章 信息技术概述一、关键点1信息处理指为获取有效信息而施加于初始信息全部操作。包含&#xff1a;信息搜集、加工、存放、传输、施用。基础信息技术&#xff1a; 感测和识别技术——扩展感觉器官功效 处理信息获取和识别通信和存放技术——扩…...

禅城区网站建设/网站排名优化软件联系方式

贰、两条主线 &#xff1a;一切的开端 &#xff08;一&#xff09;、出发点 这里只从先天的原则出发来阐明纯粹的“先验知识”中仅仅用以维计先验感性、先验知性到先验理性的自身贯通而完全独立于任何经验且先在于任何经验的两条主线&#xff0c;&#xff08;它们给出的是对哲…...