拓扑排序
1.思路
当我们做拓扑排序的时候,应该优先找入度为 0 的节点,只有入度为 0,它才是出发节点。
拓扑排序的过程,其实就两步:
- 找到入度为 0 的节点,加入结果集
- 将该节点从图中移除
循环以上两步,直到 所有节点都在图中被移除了。
如果我们发现结果集元素个数 不等于 图中节点个数,我们就可以认定图中一定有有向环!
这也是拓扑排序判断有向环的方法。
cpp
#include <iostream>
#include <vector>
#include <unordered_map>
#include <queue>
using namespace std;
int main(){
int n,m;cin>>n>>m;
vector<int>indegree(n,0); // 记录每个文件的入度
unordered_map<int,vector<int>>ump; // 记录文件依赖关系
for(int i=0;i<m;i++){
int s,t;cin>>s>>t;
indegree[t]++; // t的入度加一
ump[s].push_back(t); // 记录s指向哪些文件
}
queue<int>que;
vector<int>res; // 记录结果
for(int i=0;i<n;i++){
// 入度为0的文件,可以作为开头,先加入队列
if(indegree[i]==0){
que.push(i);
}
}
while(!que.empty()){
int cur=que.front();que.pop(); // 当前选中的文件
res.push_back(cur);
vector<int> nodes=ump[cur]; //获取该文件指向的文件
for(int i:nodes){
indegree[i]--; // cur的指向的文件入度-1
if(indegree[i]==0){
que.push(i);
}
}
}
// 如果结果集元素个数不等于图中节点个数,图中一定有有向环
if(res.size()==n){
for(int i=0;i<n-1;i++){
cout<<res[i]<<" ";
}
cout<<res[n-1]<<endl;
}
else cout<<-1<<endl;
return 0;
}
2.思考
看到拓扑排序,第一想法可能以为是排序,不过它却是图论算法。
拓扑排序:把有向图转成线性的排序,也是图论中判断有向无环图的常用方法。
拓扑排序的过程也就两步:
- 找到入度为0 的节点,加入结果集
- 将该节点从图中移除
循环以上两步,直到 所有节点都在图中被移除了。
3.Reference:拓扑排序精讲 | 代码随想录
dijkstra(朴素版)
1.思路
dijkstra 算法和 prim算法思路非常接近。
- dijkstra 算法可以同时求起点到所有节点的最短路径
- 权值不能为负数
dijkstra 三部曲:
- 第一步,选源点到哪个节点近且该节点未被访问过
- 第二步,该最近节点被标记访问过
- 第三步,更新非访问节点到源点的距离(即更新minDist数组)
解题思路步骤:
-
初始化
-
距离数组:创建一个数组
minDist,用来记录从源点到每个节点的最短距离。初始时,将源点(节点1)的距离设为 0,其他所有节点的距离设为"无穷大"。 -
访问标记:创建一个
visited数组,用来标记该节点是否被访问。
-
-
找最近的未标记节点
- 因为所有边的权重都是非负的,所以当前找到的未标记节点中,距离最小的那个,其路径不可能再通过其他未标记的节点来缩短了。
-
标记节点并更新非访问节点到源点的距离
-
将上一步找到的节点 cur 标记。
-
用节点 cur 去更新它所有邻居 j 的距离。检查"从源点到
cur的距离" + "从cur到 j 的边权"是否小于"当前记录的从源点到 j 的距离"。 -
如果更小,就更新 j 的距离。
-
-
循环与结果
-
重复步骤 2 和 3,共
n次,直到所有节点的最短路径都被确定。 -
最后,查看
minDist数组中目标节点(节点n)的值。如果值仍是"无穷大",说明从源点无法到达该节点;否则,该值就是从源点到目标节点的最短路径长度。
-
cpp
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
int main(){
int n,m;cin>>n>>m;
vector<vector<int>>graph(n+1,vector<int>(n+1,INT_MAX));
vector<bool>visited(n+1,false); // 记录顶点是否被访问过
vector<int>minDist(n+1,INT_MAX); // 存储从源点到每个节点的最短距离
for(int i=0;i<m;i++){
int s,t,val;cin>>s>>t>>val;
graph[s][t]=val;
}
minDist[1]=0; // 起始点到自身的距离为0
for(int i=1;i<=n;i++){
int mind=INT_MAX;
int cur=1;
// 1、选距离源点最近且未访问过的节点
for(int j=1;j<=n;j++){
if(!visited[j] && minDist[j]<mind){
mind=minDist[j];
cur=j;
}
}
// 2、标记该节点已被访问
visited[cur]=true;
// 3、第三步,更新非访问节点到源点的距离(即更新minDist数组)
for(int j=1;j<=n;j++){
if(!visited[j] && graph[cur][j]!=INT_MAX && graph[cur][j]+minDist[cur]<minDist[j]){
minDist[j]=graph[cur][j]+minDist[cur];
}
}
}
// 到达终点最短路径
if(minDist[n]!=INT_MAX){
cout<<minDist[n]<<endl;
}
// 不能到达终点
else cout<<-1<<endl;
return 0;
}
2.拓展
在上述基础上把最短路的路径打印出来。
cpp
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
int main(){
int n,m;cin>>n>>m;
vector<vector<int>>graph(n+1,vector<int>(n+1,INT_MAX));
vector<bool>visited(n+1,false);
vector<int>minDist(n+1,INT_MAX);
// 初始化
vector<int>father(n+1,-1);
for(int i=0;i<m;i++){
int s,t,val;cin>>s>>t>>val;
graph[s][t]=val;
}
minDist[1]=0;
for(int i=1;i<=n;i++){
int mind=INT_MAX;
int cur=1;
for(int j=1;j<=n;j++){
if(!visited[j] && minDist[j]<mind){
mind=minDist[j];
cur=j;
}
}
visited[cur]=true;
for(int j=1;j<=n;j++){
if(!visited[j] && graph[cur][j]!=INT_MAX && graph[cur][j]+minDist[cur]<minDist[j]){
minDist[j]=graph[cur][j]+minDist[cur];
father[j]=cur; // 记录边
}
}
}
if(minDist[n]!=INT_MAX){
cout<<minDist[n]<<endl;
}
else cout<<-1<<endl;
// 输出最短情况
for(int i=1;i<=n;i++){
cout<<father[i]<<"->"<<i<<endl;
}
return 0;
}
3.思考
prim 是求非访问节点到最小生成树的最小距离,而 dijkstra 是求非访问节点到源点的最小距离。
| 特性 | 拓扑排序 (卡恩算法) | 最短路径(dijkstra) |
|---|---|---|
| 解决问题 | 对有向无环图的节点进行线性排序。 | 求解带权图中从一个源点到其他所有节点的最短路径。 |
| 核心原理 | 依赖关系。一个节点必须在其所有前驱节点之后。 | 距离。一个节点的最短路径必须通过比它更短的节点来更新。 |
| 处理顺序 | 每次选择入度为 0 的节点进行处理。 | 每次选择距离源点最近的未访问节点进行处理。 |
| 关键数据结构 | indegree, queue |
minDist (最短距离数组), visited (访问标记) |
| 前提 | 图必须是有向无环图。 | 图中所有边的权重必须非负。 |
| 结果输出 | 一个拓扑排序序列 ,或者 -1(表示有环)。 |
一个最短距离数组,从中可以查到任意目标点的最短距离。如果节点不可达,其距离仍为"无穷大"。 |