学习图论(四)——最短路问题(留念)

该博客已更新到:单源最短路问题

学习时参考的博客:https://blog.csdn.net/qibofang/article/details/51594673

一、DFS或BFS搜索(单源最短路径)
思想:遍历所有从起点到终点的路径,选取一条权值最短的路径。
下面代码是参考博客中的代码,加上本人一些注释

void DFS(int u,int dist) //u 为当前节点; dist 为当前点到起点的距离 
{
	//min 表示目前起点到终点的最短距离,初始化为 无穷(用一个很大的数去表示) 
	if(dist>min) //当前距离已经大过目前起点终点的最短距离,没必要往下 
		return ;
	if(u==dest)  //到达目的地 
		if(min>dist)
		{
			min=dist;
			return ;
		} 
	for(int i=1;i<=n;++i)  //遍历每一条边,标记点 
	{
		//对于同一个点,edge[i][i] = 0,下面条件中让 edge[u][i]大于0,是为了防止陷入无限循环
		//即老是搜索同一个点。 
		if(edge[u][i]!=INF&&edge[u][i]&&!vis[i])
		{
			vis[i]=1;
			DFS(i,dist+edge[u][i]); //更新当前距离 
			vis[i]=0;
		}
	}
	return ;
}

二、弗洛伊德算法(多源最短路径)
思想:通过比较得出图中的任意两个点,依次通过0个,1个,2个…中介点所形成的路径中最短的那一条。
一开始如果两点(U和V)之间的路径(路径①)上没有其他点,那么这条路径必定是这两点过上的最短路径。现在如果两点之间有另外一条路径(路径②),该路径上存在第三个点(T),若这条路径比原来的路径①短,则将该路径②更新为U和V之前的最短路径,而这条路径②,是由U-T,T-U两条直接相连的路径构成。依次往下,当存在一条路径连接U、T、V且上面有第四个点时,在这条路径上U到V的距离是否更小?…

核心代码:
用了三层循环,时间复杂度较高
(这篇博文对弗洛伊德算法解释很详细:https://blog.csdn.net/qq_34374664/article/details/52261672)

void Floyd(int all) //all 共有多少节点 
{
	//三层循环表示的是:从 j 到 k 经过点 i 后 j和k之间的距离有没有缩小 
	for(int i=1;i<=all;++i)
		for(int j=1;j<=all;++j)
			for(int k=1;k<=all;++k)
				//最好加入 egde[j][i] 和 egde[i][k] 都小于inf的条件,防止数据爆掉 
				if(egde[j][k]>egde[j][i]+egde[i][k]) //判断借助中转点后,路程是否缩小 
					egde[j][k]=egde[j][i]+egde[i][k];  //每个 egde[i][j]都表示当前时刻下 i 到 j 的最短距离 
} 

使用Floyd算法后,二维数组edge表示的边都是两点之间的最短路,所以可以输出任意两点之间最短的路径。

三、迪杰斯特拉算法(单源最短路径)
思想: 通过 n-1 层循环,每一次,寻找距离起点最近的点,做标记(不再访问),然后用该点作为其他点到起点路径上的中介,更新其他点到起点之间的距离。即,假设起点为U,中介点为T,图上任意没有被标记过的点为V,判断U-V的距离是否大于U-T-V距离,是的话更新U-V的距离为U-T-V的距离。(用一个一维数组存放在图上其他点到起点的距离),这种对两点直接距离的更新称为松弛操作(记住一些专业名词对以后的阅读有好处)。然后再次从未标记的点中寻找和起点最近的点,做同样的操作,知道遍历所有的点。最终得到的 dist数组中存放的是图上任意点到起点的最短距离

为什么可以实现找到最短路?
因为这个算法先是找到和起点直接相连的距离最短的点,在更新其他点到起点的距离的时候,都是利用已计算过和起点的最短距离的点作为中介点去比较,所以每次得到的必然都是最短距离。

void Dijkstra(int u)
{
	int min;//表示某一点到起点的最短距离
	int k;// k 记录为访问的点中距离起点最近的点
	for(int i=1; i<=n; ++i)
	{
		flag[i]=0;// flag 用于标记节点是否已经访问过;
		//dist 数组,存储图上各点到起点的距离
		dist[i]= edge[u][i]; // edge 存放的是两个节点之间的距离,若两个点之间没有边,则赋值为 INF(极大的数)
		pre[i] = 0; //pre 数组用来存储 最短路径中节点 i 的上一个节点是哪一个
	}
	flag[u]=1;//标记起点,没必要访问
	dist[u]=0;
	//开始Dijkstra算法,遍历 n-1次,即遍历除起点外其他所有的点
	for(int i=1; i<n; ++i)
	{
		//寻找离起点最近的点
		min = INF;
		for(int j=1; j<=n; ++j)
			if(!flag[j]&&min>dist[j])
			{
				min=dist[j];
				k = j;
			}
		//相应的点后,更新图上其他点到起点的路径距离
		flag[k]=1;
		for(int j=1; j<=n; ++j)
			if(edge[k][j]!=INF)// k 和 j 之间存在边时
			{
				int temp = edge[k][j]+dis[k];  //i 到 j 的新路径
				if(!flag[j]&&temp<dis[j])  //如果该路径距离小于原本 j到i 的距离,刷新替换
				{
					dis[j]=temp;
					pre[j]=k;
				}
			}
	}
}

此处补充四个小概念:(N表示节点数,M表示边数)
①稀疏图:M远比N²小的图称为稀疏图
②稠密图:M相对较大的图称为稠密图
③大顶堆:堆顶元素最大的堆
④小顶堆:堆顶元素最小的堆

紫书上对该算法进行了优化并且封装,其中涉及优先队列的知识,基本思想一致,用优先队列只是进行了优化,使得即使是稠密图,使用该优化算法的速度也比用邻接矩阵的算法快。因为执行 push存在前提。
(用优先队列,是因为算法中d[i]越小,越应该先出队(每次先取和起点距离最近的点))
下面给出紫书把此算法封装成结构体的代码
链接所指大佬提供了迪杰斯特拉算法堆优化的另一种思路:不需要定义结构体重载 <,而是直接对距离取相反数


//把整个算法以及所用到的数据结构封装到一个结构体中
struct Dijkstra
{
	struct Edge
	{
		int from,to,dist;
		Edge(int u,int v,int d):from(u),to(v),dist(d) {}
	}; //存放边的结构体
	int n,m;
	vector<Edge> edges;
	vector<int> G[MAXN];
	bool done[MAXN];// 标记是否访问过
	int d[MAXN];//起点到各个点的距离
	int p[MAXN];// 记录最短路径

	void inti(int n)
	{
		this->n=n; //赋值节点数
		//清空操作,是为了可以重复使用此结构体
		for(int i=1; i<=n; ++i)
			G[i].clear();
		edges.clear();
	}

	void AddEdge(int f,int t,int d)
	{
		edges.push_back(Edge(f,t,d));
		m = edges.size(); //每次添加边后都把边数赋值给 m 最后得到总边数
		G[f].push_back(m-1);  //m-1是保存了 边在edges中的编号(下标),可以通过此编号访问边
	}
	//定义一个结构体作为优先队列中的元素类型 

	struct HeapNode{
		int d,u; //d 是距离, u 是节点
		//表示优先级为从小到大 
		bool operator<(const HeapNode& a)const
		{
			return d>a.d; //小堆顶 
			//个人理解,队列的每次push操作都要根据优先级判断再放置队顶元素,
			//即是否 队顶<新入队元素的操作(STL默认操作是大根堆)
			//重载<运算符后使得比较结果颠倒,优先级变成从小到大
		}
	};
	//主算法 
	void dijkstra(int u)
	{
		priority_queue<HeapNode> q; //创建一个优先队列 
		for(int i=1;i<=n;++i)
			d[i] = INF;
		d[u] = 0;//起点到起点的距离当然为0 
		memset(done,0,sizeof(done));
		HeapNode v(0,u); //起点 
		q.push(v);
		while(!q.empty())
		{
			v=q.top();
			q.pop();
			if(done[v.u]) //已经访问过的点不再访问 
				continue;
			done[v.u] = true;
			
			//可以省去done数组,改为 if(v.d!=d[v.u]) 防止节点重复扩展 
			
			//思路同,只是实现方式改变 
			for(int i=0;i<G[v.u].size();++i)  //这种情况下每条边只被访问了一次 
			{
				Edge& e = edges[G[v.u][i]];
				if(d[e.to]>d[v.u]+e.dist)
				{
					d[e.to]=d[v.u]+e.dist;
					p[e.to]=v.u;
					q.push((HeapNode){d[e.to],e.to});
				}
			}
		}
	}
};

注意:迪杰斯特拉算法不可以求含有负权边的最短路!
具体原因等我搞明白了再写一篇博客。

四、Bellman-Ford(贝尔曼 - 福特)算法(单源最短路径,可以求解含负权边的图)
思想:(节点数N,边数M)进行至多N-1次循环,每一次都更新图上任意点到起点的距离(松弛操作)。其实个人觉得类似Dijkstra算法,对任意点到起点的距离进行更新,只是每一次都遍历所有边去更新。至多N-1次,是考虑了最坏的情况,即每一次遍历所有边,只有和起点相连的边得到更新 。

核心代码:

// u[i]和v[i] 分别存放第 i 条边的前后顶点,w[i]存放权值  
bool BellmanFord()
{
	for(int i=1;i<=n;++i)
		d[i]=(i==1?0:INF);
	//至多遍历  n-1 次
	//考虑最坏的情况,即每一次遍历所有边,只有和起点相连的边得到更新 
	for(int i=1;i<n;++i)
		for(int j=1;j<=m;++j)
		{
			int x=u[i],y=v[i];
			if(d[x]<INF)
				d[y]=min(d[y],d[x]+w[i]);
		}
	//判断是否存在 含有负权的环 
	for(int i=1;i<=m;++i)
		if(d[v[i]]>d[u[i]]+w[i])
			return false;
	return true;
}

参考此博客(https://www.cnblogs.com/xiu68/p/7993514.html)

此算法的缺点是效率不高。因为要进行N-1次循环,但可能在第一次就已经得到答案了,亦即是做了很多无用的工作。
下面是对该算法的改进,一般称为SPFA算法。
思想:在Bellman-Ford算法的基础上,使用队列保存待松弛的点,对每个出队的点,遍历跟它相邻的点,如果这两点间的边能够松弛且相邻点不在队列中,则把相邻点存入队列。(因为松弛操作可能对该相邻点的相邻点产生影响)

核心代码:

void SPFA()
{

	vector<int> d(n+1,INF); //距离
	vector<int> cnt(n+1,0); //记录每个点遍历的次数
	vector<int> inq(n+1,0); //记录节点是否在队列内
	d[s]=0;
	cnt[s]=inq[s]=1;
	deque<int> q(1,s);
	while(!q.empty())
	{
		int u=q.front(),i=0,to,dist;
		inq[u] = 0; //出队
		q.pop_front();
		for(; i<g[u].size(); ++i)
		{
			to=g[u][i].to;//g[u][i].to表示u-i边箭头端(末端)的点
			dist=g[u][i].dist;
			if(d[to]>d[u]+dist)
			{
				d[to]=d[u]+dist; 
				if(!inq[to])  //如果点不在队列内再入队 
				{
					//若有负权环,则会无限循环下去,所以肯定有一个点遍历的次数大于 n 
					if(n==++cnt[to])return 0;  //判断是否为负权环
					inq[to]=1;
					//SLF优化:减少重复扩展的次数。
					if(!q.empty()&&d[to]<q.front()) //确保队列非空才能访问q.front() 
						q.push_front(to);
					else
						q.push_back(to);
				}
			}
		}
	}

其中运用了SLF优化(关于此优化,好像还没有确切的证明,但实际运用上确实优化了不少,如果有读者知晓麻烦评论告知一下,多谢)
SLF优化:判断将要入队的节点到源点的距离是否比队首元素小,是的话插入队首,否则插入队尾。所以上面代码用了双向队列。这样做可能会减少原队首的某些邻接点或者一些本来没有SLF优化时需要入队的点的入队次数,即减少了扩展次数。但是存在反例使得使用了SLF优化反而使程序变慢。所以一般可以用,但不是绝对都能用。(我对此的理解还很浅薄)

学习、学好图论的路,还很长…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值