2月3日:今天从“最短路径”说起

说在前面

最短路径问题是数据结构图中的一个经典问题,分为单源最短路和多源最短路。
单源最短路是指从一个起点出发,到其他各个顶点的最短路径;多源最短路是指图中任意两点之间的最短路径。
一般采用弗洛伊德(Floyd)算法解决多源最短路问题,采用迪杰斯特拉(Dijkstra)算法解决单源最短路问题。

说弗洛伊德(多源最短路,无负环,动态规划)

如果需要计算图中任意两点之间的最短距离,在数据规模不太大的情况下,可以采用弗洛伊德算法(时间复杂度O(n^3))。
弗洛伊德算法的基本思想是,如果要更新任意两点之间的最短距离,必须找出第三个点,使得这两个点分别到第三个点的距离之和小于原来这两点之间的距离,这样就能更新这两点之间的最短距离。
为达到这个效果,弗洛伊德算法需要尝试将所有的点依次作为中转点,再去验证所有的两个顶点经过该中转点后距离是否缩短。
采用邻接矩阵存图为三重循环:

for(k=1;k<=n;k++)//所有顶点从1~n进行编号
    for(i=1;i<=n;i++)
    	for(j=1;j<=n;j++)
    		if(e[i][j]>e[i][k]+e[k][j])//e[i][j]表示从i到j的最短距离
            	e[i][j]=e[i][k]+e[k][j];

说迪杰斯特拉(单源最短路,无负权,贪心)

如果起点已经固定,需要求它到其他各点的最短距离,可以采用迪杰斯特拉算法。
迪杰斯特拉的基本思想是,固定一个起点后,记录起点到其他各点的距离,并从中选取一条最短距离,这个距离就是起点到该点的最短距离;之后在经过该点的基础上计算起点到其他剩下各点的距离,如果小于原先起点直接到剩下各点的距离就更新起点到该点的距离,并从中再选取一条最短的距离作为一条最短距离,然后再依据上述的方法更新起点到剩下各点的距离……循环往复,直至求完起点到其他各点的最短距离。
定义:vis[i]→值为0表示起点到i点的最短距离还未求出,值为1代表已求出;d[i]→存储起点到i点的最短距离;vs→起点编号;e[i][j]→表示i点到j点的距离(所有顶点从1~n进行编号)。
采用邻接矩阵存图则关键代码如下:

void dijkstra()
{
    int k;//记录当前已求出最短距离的点
    //初始化vis[i]数组和d[i]数组
    for (int i = 1; i <= n; i++){vis[i] = 0;d[i] = e[vs][i];}
    vis[vs] = 1; d[vs] = 0;
    for (int i = 2; i <= n; i++)
    {
        int min = inf;
        for (int j = 1; j <= n; j++)
            if (vis[j]==0 && d[j]<min){min = d[j];k = j;}
        vis[k] = 1;
        for (int j = 1; j <= n; j++)
        {
            int tmp = (e[k][j]==inf ? inf : (min + e[k][j])); // 防止溢出
            if (vis[j] == 0 && (tmp  < d[j]) )d[j] = tmp;
        }
    }
}

采用链式前向星存图则关键代码如下:


void dijkstra()
{
    memset(d,0x3f,sizeof(d));//初始化为0x3f可以防止松弛操作中溢出
    int k; d[vs] = 0;
    for(int i = 2; i <= n; i++)
    {
        int min = inf;
        for(int j = 1; j <= n; j++)
            if(vis[j] == 0 && dis[j] < min){min = dis[j]; k = j;}
        vis[k] = 1;
        for(int i = head[k]; i != 0; i = e[i].next) //遍历以k为起点的边
        {
            int v = e[i].to; 
            if(d[v] > d[k] + e[i].w)d[v] = d[k] + e[i].w; //松弛操作
        }
    }
}

迪杰斯特拉算法可以利用优先队列进行优化。
先说优先队列。优先队列其实是一个堆。普通队列入队出队遵循先进先出的原则;而优先队列中元素入队出队按照优先级别执行,优先级别高的元素先出队。在优先队列中,我们可以自行规定优先队列中元素的优先级,来满足我们的实际需求。
利用优先队列优化迪杰斯特拉算法的基本思想是,借助优先队列来代替最短距离的查找,因为优先队列每次弹出的元素一定是整个队列中的最大元素,从而降低原来O(n^2)的复杂度。

void dijkstra(){
    priority_queue<pair<int,int> > q;//STL中的优先队列
    memset(d,0x3f,sizeof(d));
    d[vs]=0;
    q.push(make_pair(0,1));//代表距离为0,终点为1
    while(!q.empty())
    {
        pair<int,int> t;
        t=q.top();q.pop();
        int u=t.second;vis[u]=1;
        for(int j=head[u];j!=0;j=e[j].next)
        {
            int v=e[j].to;
            if(vis[v]==0&&d[v]>d[u]+e[j].w)
            {
                d[v]=d[u]+e[j].w;
                q.push(make_pair(d[v]*(-1),v));//默认优先级比较从大到小,所以乘-1
            }
        }
    }
}

经过优先队列优化过的迪杰斯特拉算法时间复杂度为O(m*log(n))(m为边数,n为顶点数)。

说SPFA算法(含负权单源最短路,可判负环,动态逼近)

前面说的迪杰斯特拉算法在含有负边权的图中便无法求出准确的最短路径了,这是因为迪杰斯特拉是以贪心的策略一步步往下走的,即使遇到更优解也无法回头修改。这时SPFA算法便派上用场了。
SPFA算法是经过队列优化的Bellman-Ford算法。Bellman-Ford算法复杂度较高,经队列优化后效率有明显的提升。
同时SPFA算法还可以判断图中是否含有负环。SPFA算法中判断负环的基本思想是,在算法执行过程中记录每个点的出队次数,若次数大于n(图中顶点个数)则可判定存在负环(这是因为在SPFA算法中每个点最多被其他n-1个点优化)。
SPFA算法的基本思想是,利用队列存储可以用来更新最短距离的点,每次从队头取出点进行优化,检测起点经过这个点到达目标点的距离是否小于原来起点直接到达目标点的距离,小于则更新;而且之后这个点可能还会重新入队。经过反复的进队出队优化后,如果图中不含负环,则最后一定可以结束松弛操作;如果某个点进队或出队的次数超过图中所有点的个数,则判定存在负环,不存在最短路。
采用链式前向星存图则关键代码如下:

int spfa()
{
	memset(d,0x3f,sizeof(d));
	queue<int> q;
	vis[vs]=1; d[vs]=0;
	q.push(vs);
	while(!q.empty())
	{
		int top=q.front();q.pop();
		vis[top]=0;
		outqueue[top]++;
		if(outqueue[top]>n)	return 0;//表示存在负环
		for(int i=head[top];i!=0;i=e[i].next)
			if(d[e[i].to]>d[top]+e[i].w)
			{
				d[e[i].to]=d[top]+e[i].w;
				if(!vis[e[i].to]){vis[e[i].to]=1;q.push(e[i].to);}
			}
	}
	return 1;
}

SPFA时间复杂度平均为O(km),在最坏情况下时间复杂度为O(nm)(m为边数,n为顶点数)。

说在后面

最短路径问题是学习图时容易遇到的经典问题,掌握解决最短路径问题的方法在算法竞赛中是不可或缺的。由图的链式前向星存储结构,我们还将说到”多叉树转二叉树“问题以及树形DP。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值