0
点赞
收藏
分享

微信扫一扫

图论学习-最短路模型

艾米吖 2022-02-07 阅读 55
图论算法

这里只是我对于最短路模型学习的一个记录,不正确的地方希望大家指出。

文章目录

最短路模型

一、 什么是最短路?

1.1 概念

1.2 基本术语

正权边: 从点A到点B有一条边,边的权值为正数。
负权边: 从点A到点B有一条边,边的权值为负数。
环: 从点A出发,经过若干条边又能回到A,这样的路径称为环。
负环: 环的所有边的权值之和为负数,那么这个环就是负环。
有向边:有方向的边,a->b即从a到b
无向边:没有方向的边,a和b两点可以相互到达,a->b且b->a,显然,无向边可以看成是两条有向边。
邻接矩阵: 存储图的矩阵。
邻接表,链式前向星: 除了邻接矩阵之外的两种存图方式。一般比邻接矩阵存图更高效。
dist数组:表示从起点到节点i的最短路径的长度,起点不一定是1。
三角形不等式:对于图中的某一条边(x,y,z) 有dist[y]<=dist[x]+z

二、主要模型

2.1 dijkstra

dijkstra只适用于不含负权边的图,即这个图中所有的边权值都是正的,为什么不适用于负权图稍后会说明。

2.1.1 步骤

  1. 初始化dist[1] = 0,其余节点的dist为正无穷大
  2. 找到一个未被标记的,dist[x]最小的点,然后标记它。
  3. 扫描这个标记点x的所有边,用它来更新它能到达所有点的dist
  4. 重复上述步骤,直到所有点都被访问完。

2.1.2 画图理解

  1. 例如给出下面这样一个图,求从1号节点出发,到4号节点的最短路。
    在这里插入图片描述
    首 先 d i s t [ 1 ] = 0 , 而 d i s t [ 2 ] = d i s t [ 3 ] = d i s t [ 4 ] = ∞ 首先dist[1] = 0, 而dist[2] = dist[3] =dist[4] = \infin dist[1]=0,dist[2]=dist[3]=dist[4]=
const int inf = 0x3f3f3f3f;
memset(dist,inf,sizeof dist);
dist[1] = 0;
  1. 找一个没有被标记的且dist[i]最小的点,然后标记它。
    在这里插入图片描述
    在这里插入图片描述
int t = -1;//这里t初始化为任何值都可,主要是为了去存储dist最小的点
for(int j = 1;j<=n;j++)
{
	if(!st[j]&&(t==-1||dist[t]>dist[j])
	{
		//t==-1代表第一次访问
		//如果当前点没有被标记或者第一次访问,或者当前点i离起点的最短距离比t离起点的最短距离还更小,那么就更新
		t = j;
	}
}
st[t] = true;//标记它被访问

因为dist[1] = 0,所以循环结束后标记st[1] = true;此时1就是离起点最近的点。

  1. 因为1离起点最近,所以用1去更新图中其他所有点与起点的距离。
    在这里插入图片描述
    =>
    在这里插入图片描述
    上面这个更新过程的代码也很好实现:
for(int j = 1;j<=n;j++)
{
	dist[j] = min(dist[j],dist[t]+g[t][j]);
}

这样,一次更新就完成了。而要求图中所有点到起点的最短距离,还需要继续更新,一共需要更新n轮,即把每个点都作为出发点更新最短距离。代码如下:

void dijsktra()
{
	for(int i = 1;i<=n;i++)//迭代n轮,也可以写成i = 0;i<n;i++
	{
		int t = -1;
		for(int j = 1;j<=n;j++)//遍历图中的所有点
		{
			//找到dist最小的那个点
			if(!st[j]&&(t==-1)||dist[t]>dist[j])
				t = j;
		}
		st[t] = true;//标记它被访问过
		for(int j = 1;j<=n;j++)
		{
			//用t去更新图中的所有点
			dist[j] = min(dist[j],dist[t]+g[t][j]);
		}
	}
}

2.1.3 为什么dijkstra不能处理负权边?

考虑一个带有负权边的图:
在这里插入图片描述

显然按照dijkstra的算法流程:比如求节点1到节点5的最短路径:
1->2->5 ,dist[5] = 3+2 = 5;
而观察路径1->4->3->5 发现4+(-1)+(-6) = -3,
因此 在含负权边的图里,dijkstra算法失效。

2.2 堆优化的dijkstra

我们观察2.1的代码:发现寻找dist最小的点这一步可以不用for循环去找,而可以用堆(优先队列)去优化,这样可以把时间复杂度从O(n^2)降为O(mlogn),m代表边数。如算法竞赛进阶指南原话:
在这里插入图片描述

我们需要使用pair来存储节点的编号和当前点到起点的最短距离dist。
所以,每一次取出节点编号,判断它是否被访问过,如果没有访问,就直接去更新图中其他点。代码如下:

#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int>PII;
const int N = 2e5+10,M = N;
int h[N],e[M],ne[M],w[M],idx;
int dist[N];
bool st[N];
void add(int a,int b,int c)
{
    e[idx] = b,ne[idx] = h[a],w[idx] = c,h[a] = idx++;
}
void dijkstra()
{
    priority_queue<PII,vector<PII>,greater<PII>>heap;
    heap.push({0,1});//距离为0,节点编号为1,因为从1开始
    dist[1] = 0;
    while(heap.size())
    {
        PII t = heap.top();
        heap.pop();
        int id = t.second,distance = t.first;
        if(st[id]) continue;
        st[id] = true;
        for(int i = h[id];~i;i = ne[i])
        {
            int j = e[i];
            if(dist[j]>distance+w[i])
            {
                dist[j] = distance+w[i];
                heap.push({dist[j],j});
            }
        }
    }
}

2.3 bellman_ford

注:bellman_ford 可以求最多经过k条路径的最短路
注2:bellman_ford也可以用来判断负环是否存在

n个点最多只有n-1条边,所以对一个点最多只需要松弛n-1次,如果超过n-1次还能松弛说明显然有负环存在。

2.3.1 三角形不等式

显然对图求了最短路径以后,对于图中任意一条边(x,y,z) 都有dist[y]<=dist[x]+z.

2.3.2 步骤

1.扫描所有边(x,y,z) ,若dist[y]>dist[x]+z,说明当前边还没更新,那么就用dist[x]+z更新dist[y]
2.重复1,直到没有更新操作发生

容易发现,bellman_ford算法复杂度为O(nm) ,n为点数,m为边数
需要注意在实现时,要新开一个backup数组,确保每一轮dist数组是被上一轮的dist数组所更新。

2.3.3 代码实现

#include<bits/stdc++.h>
using namespace std;
const int N = 510,M = 10010;
struct Edge{
    int a,b,w;
}edges[M];
int n,m,k;
int backup[N],dist[N];
void bellman_ford()
{
    memset(dist,0x3f3f3f3f,sizeof dist);
    dist[1]  = 0;
    for(int i = 0;i<n;i++)
    {
        memcpy(backup,dist,sizeof dist);
        for(int j = 1;j<=m;j++)
        {
            int a = edges[j].a,b = edges[j].b,w = edges[j].w;
            dist[b] = min(dist[b],backup[a]+w);
        }
    }
}
int main()
{
    cin>>n>>m>>k;
    for(int i = 1;i<=m;i++)
    {
        int a,b,w;
        cin>>a>>b>>w;
        edges[i].a = a,edges[i].b = b,edges[i].w = w;
    }
    bellman_ford();
    if(dist[n]>0x3f3f3f3f/2) 
    {
        cout<<"impossible";return 0;
    }
    cout<<dist[n];
    return 0;
}
问题1:为什么要使用backup数组?
问题2:为什么不是像dijkstra那样判断dist[n]==0x3f3f3f3f?

2.4 spfa

spfa是队列优化的bellman_ford算法,但是注意spfa虽然最佳时间复杂度为O(km),k是常数,但是在特殊图一样会被卡成O(nm),比如棋盘图或者菊花图。
spfa可以判负环,而判负环在差分约束中也有应用。

开一个cnt数组,记录每个点出队次数,因为对于一个点最多只需要松弛n-1次
(因为n个点只有最多只有n-1条边)
由抽屉原理,如果松弛次数(即出队次数)超过n-1次,那么就有负环,所以只要有一个点cnt[i]>=n,说明这个点出队n次,说明说明有负环

2.4.1 步骤

  1. 建立一个队列,最初队列中只有起点u
  2. 取出队头节点x,遍历它的所有出边,若dist[y]>dist[x]+z,说明可以更新,则使用dist[x]+z更新dist[y]。同时,判断y是否在队列中,如果不在,则把y入队
  3. 重复1,2,直到队列为空

写成伪代码的形式:在这里插入图片描述
它的代码实现与堆优化的dijkstra很类似

2.4.2 代码实现

#include<bits/stdc++.h>
using namespace std;

const int N = 1e5+10,M = N;
int h[N],e[M],ne[M],w[M],idx;
bool st[N];
int n,m;
int dist[N];
int q[N];

void add(int a,int b,int c)
{
    e[idx] = b,w[idx] =c,ne[idx] = h[a],h[a] = idx++;
}
void spfa()
{
    int hh = 0,tt = -1;
    q[++tt] = 1;
    dist[1] = 0;
    while(hh<=tt)
    {
        int t = q[hh++];
        st[t] = false;
        /*因为spfa是枚举边,而且边的权值可能为负,所以一个点可能被多次更新,
        它可能会在之后被其他点遍历到,所以这里需要st[t] = false;
     	而dijkstra是基于贪心的,每个点只可能被当前距离起点最近的点
     	遍历到,所以不用写st[t] = false;*/
        for(int i =h[t];~i;i = ne[i])
        {
            int j = e[i];
            if(dist[j]>dist[t]+w[i])
            {
                dist[j] = dist[t]+w[i];
                if(!st[j])
                {
                    st[j] = true;
                    q[++tt] = j;
                }
            }
        }
    }
}
int main()
{
    memset(h,-1,sizeof h);
    memset(dist,0x3f3f3f3f,sizeof dist);
    cin>>n>>m;
    while(m--)
    {
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
    }
    spfa();
    if(dist[n]==0x3f3f3f3f)
    {
        cout<<"impossible";
        return 0;
    }
    cout<<dist[n];
}

2.4.3 主要问题

2.5 floyd

解决多源最短路问题:即图中任意两点的最短距离
下面摘自算法竞赛进阶指南:

时间复杂度O(n^3)
基于动态规划,令
D[k,i,j]表示“经过若干个编号不超过k的节点”从i到j的最短路的长度。
而显然可以划分成两个子问题,因为我们要求最短路径,所以是取min
D[k,i,j] = min(D[k-1,i,j],D[k-1,i,k]+D[k-1,k,j])
k是阶段,必须置于最外层循环中。
而类似于背包问题的优化方法,可以把k这一维优化掉,所以有:
D[i,j] = min(D[i,j],D[i,k]+D[k,j])
适用于稠密图,n<=280时比较保险。
注:记得把dist数组初始化为正无穷,另外dist[起点] = 0

实现代码:

#include<bits/stdc++.h>
using namespace std;

const int N = 210;
int d[N][N];
int n,m,t;
const int INF = 0x3f3f3f3f;

int main()
{
    cin>>n>>m>>t;
    for(int i = 1;i<=n;i++)
    {
        for(int j = 1;j<=n;j++)
        {
            if(i==j) d[i][j] = 0;
            else d[i][j] = INF;
        }
    }
    while(m--)
    {
        int a,b,c;
        cin>>a>>b>>c;
        d[a][b] = min(d[a][b],c);
    }
    for(int k = 1;k<=n;k++)
    {
        for(int i = 1;i<=n;i++)
        {
            for(int j = 1;j<=n;j++)
                d[i][j] = min(d[i][j],d[i][k]+d[k][j]);
        }
    }
    while(t--)
    {
        int x,y;
        cin>>x>>y;
        if(d[x][y]>0x3f3f3f3f/2)
        {
            cout<<"impossible\n";
        }
        else
        {
            cout<<d[x][y]<<endl;
        }
    }
    return 0;
}

一个小总结:对于最短路问题,一般判断无解可以直接判断

dist[n]==0x3f3f3f3f

但是对于有负权边的图,就算1和n不连通,dist[n]也可能被更新,所以最好写成dist[n]>0x3f3f3f3f/2

三、练习题目

详见图论刷题计划与题解1(最短路问题)

四、参考

acwing算法基础课
算法竞赛进阶指南
大佬的博客

举报

相关推荐

0 条评论