数据结构 - 图
文章目录
- 一、图的基本概念
- 二、图的储存结构
- 1、邻接矩阵
- 2、邻接表
- 三、图的遍历
- 1、广度优先遍历
- 2、深度优先遍历
- 四、最小生成树
- 1、概念
- 2、Kruskal算法
- 3、Prim算法
- 五、最短路径问题
- 1、单源最短路径--Dijkstra算法
- 2、单源最短路径--Bellman-Ford算法
- 3、多源最短路径--Floyd-Warshall算法
- 六、拓扑排序
- 1、概念
- 2、如何排序
- 3、实现
- 4、应用
一、图的基本概念
- 图是由顶点集合及顶点间的关系组成的一种数据结构: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)邻接矩阵的优点是可以快速查找两个顶点是否相连,缺点是如果顶点多而边少,就会造成空间浪费。
代码实现:
// 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条边。因此构造最小生成树的准则有三条:
- 只能使用图中的边来构造最小生成树
- 只能使用恰好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:顶点的个数)条边结束。
//并查集
#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算法
- 单源最短路径问题:给定一个图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算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路 径的最短路径。
上图来自算法导论。
//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 算法实现拓扑排序的步骤:
- 。
遍历有向图中的所有边,对于每条边(u, v),将顶点 v 的入度加一。- 将入度为 0 的顶点加入队列。
初始化一个队列,用于存储入度为 0 的顶点。遍历所有顶点,将入度为 0 的顶点加入队列。- 进行拓扑排序。
创建一个空列表,用于存储排序后的顶点。当队列不为空时,从队列中取出一个顶点 u。将顶点 u 加入排序后的列表中。遍历顶点 u 的所有邻接顶点 v,将顶点 v 的入度减一。如果顶点 v 的入度变为 0,则将其加入队列。- 检查是否存在有向环。
如果排序后的列表中顶点的数量与图中的顶点数量相同,则说明图中不存在有向环,拓扑排序成功。否则,说明图中存在有向环,无法进行拓扑排序。
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 产品描述: 瑞格智慧心理服务平台是一个集心理测评、心理咨询、心理危机干预、心理放松训练等功能于一体的综合性心理健康服务平台。该平台由北京瑞格心灵科技有限公司开发,旨在为用户提供全方位的心理健康服务。0x02 漏洞描述:…...

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

自动化部署-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",则称 x 是一个 有效 字符串。 返回所有长度为 n 的 有效 字符串,可以以任意顺序排列。 示例 1: 输入: 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是一个用于解析和验证参数的工具。它可以帮助开发者从请求中提取参数,并确保这些参数符合预期的格式和类型。参数解析的意思就是规范化传入的参数并获取到这些参数。 一、什么是 reqparse.RequestParser? reqpa…...

gcc与mingw64版本介绍
三类编译器 GCC,全称为GNU Compiler Collection,是一个强大的编译器集合,它不仅支持C和C语言,还支持Fortran、Ada、Java等多种编程语言的编译。在GCC工具链中,gcc和g是两个核心的编译器工具。gcc是专门用于编译C语言程…...
CSS3新增长度单位
CSS3新增长度单位 rem:根元素字体的倍数,只与根元素字体大小有关;vw:占视口宽度的百分比;vh:占视口高度的百分比;vmax:占视口中宽和高最大的百分比;vmin:占视…...

【Spring】创建Spring项目前的配置工作
🥊作者:一只爱打拳的程序猿,Java领域新星创作者,CSDN、阿里云社区优质创作者。 🤼文章收录于: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…...

黑马数据库学习笔记
课程地址 (基础篇)MySQL的启动 mysql 默认使用 3306 端口 在 centos下,启动 mysql 数据库:service mysqld start; 查看状态/启动/停止/重启:systemctl status/start/stop/restart mysqld; 登录到mysql数据库&…...

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

【数据结构和算法】三、动态规划原理讲解与实战演练
目录 1、什么是动态规划? 2、动态规划实战演练 2.1 力扣题之爬楼梯问题 (1)解题思路1: (2)解题思路2: (3)动态规划(DP):解题思路 (4&#x…...
交叉编译 perl-5.40.0(riscv64)
交叉编译 perl-5.40.0(riscv64) 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解法。以下是对该算法思想的中文解释: 算法思想 二分查找的基本思路: 由于数组是部分有序的(被旋转过),我们可以利用二分查找的思想,逐步缩小…...

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

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

springboot 百货中心供应链管理系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,百货中心供应链管理系统被用户普遍使用,为方…...

黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门 
以光量子为例,详解量子获取方式
光量子技术获取量子比特可在室温下进行。该方式有望通过与名为硅光子学(silicon photonics)的光波导(optical waveguide)芯片制造技术和光纤等光通信技术相结合来实现量子计算机。量子力学中,光既是波又是粒子。光子本…...

LLMs 系列实操科普(1)
写在前面: 本期内容我们继续 Andrej Karpathy 的《How I use LLMs》讲座内容,原视频时长 ~130 分钟,以实操演示主流的一些 LLMs 的使用,由于涉及到实操,实际上并不适合以文字整理,但还是决定尽量整理一份笔…...
GitHub 趋势日报 (2025年06月06日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 590 cognee 551 onlook 399 project-based-learning 348 build-your-own-x 320 ne…...

[大语言模型]在个人电脑上部署ollama 并进行管理,最后配置AI程序开发助手.
ollama官网: 下载 https://ollama.com/ 安装 查看可以使用的模型 https://ollama.com/search 例如 https://ollama.com/library/deepseek-r1/tags # deepseek-r1:7bollama pull deepseek-r1:7b改token数量为409622 16384 ollama命令说明 ollama serve #:…...
NPOI操作EXCEL文件 ——CAD C# 二次开发
缺点:dll.版本容易加载错误。CAD加载插件时,没有加载所有类库。插件运行过程中用到某个类库,会从CAD的安装目录找,找不到就报错了。 【方案2】让CAD在加载过程中把类库加载到内存 【方案3】是发现缺少了哪个库,就用插件程序加载进…...
为什么要创建 Vue 实例
核心原因:Vue 需要一个「控制中心」来驱动整个应用 你可以把 Vue 实例想象成你应用的**「大脑」或「引擎」。它负责协调模板、数据、逻辑和行为,将它们变成一个活的、可交互的应用**。没有这个实例,你的代码只是一堆静态的 HTML、JavaScript 变量和函数,无法「活」起来。 …...

HubSpot推出与ChatGPT的深度集成引发兴奋与担忧
上周三,HubSpot宣布已构建与ChatGPT的深度集成,这一消息在HubSpot用户和营销技术观察者中引发了极大的兴奋,但同时也存在一些关于数据安全的担忧。 许多网络声音声称,这对SaaS应用程序和人工智能而言是一场范式转变。 但向任何技…...