最短路

话说至今也好久都没做到最短路的题了,大比赛被思维题卡得死死的,都没机会做到算法题,

甚至怀疑是不是图论现在不吃香了啊?

图论思维题倒是有遇到过,像是1e9数据的图里给定点找相邻的点,也算不上啥。

正文开始吧。

最短路呢,根据题目要求和数据范围有几种经典算法。

 

首先呢,讲讲存图

方法有四种,邻接表,邻接矩阵,前向星,链式前向星。

这里,就只讲链式前向星了。why?因为最好用,也泛用。

话我就写在代码注释里吧,分开写也不好看着理解。(草稿文件夹里的代码终于得见天日了

基本定义:

复制代码
#define N 2021
int head[N],num;
/* 
首先,num为边的编号,这个很简单理解 
head数组要跟mp结构体里的next一起理解,head[x]表示起点为x的路径(一串边的编号的最后一个),
每次更迭都会将head[x]的值存入mp[num].next
然后遍历访问的时候,通过访问next来找上一边 
*/ 
struct iii
{
    int to;//指向下一点 
    int next;//配合head数组理解 
    int dis;//没有边权的时候可以不写 
}mp[N];
基本定义
复制代码

边的增删操作,这是理解链式前向星的关键,没理解不用着急,

下面我准备了一组数据,大概容易到一看就懂的那种地步。

(结合基本定义来理解)

增:

复制代码
void add(int x,int y,int z)
{
    //增加x到y的边,权值为z 
    mp[++num].dis=z;
    mp[num].to=y;
    mp[num].next=head[x];
    head[x]=num;
}
add
复制代码

用一组数据来说明吧

5 5 1
1 2 10
2 3 1
4 5 10
3 4 10
1 3 20

 

5 5 1表示5个点,5条边,起点为1

 

 

 理解的关键看1 2 10和1 3 20的两行即可,

从遍历的角度讲一遍就可以理解了:for(int i=head[x];i!=0;i=mp[i].next)

比如现在遍历起点为1的边,

那么首先进入循环时,i=head[1],可以看出i=5

然后此时会去找mp[5],然后通过next去找这个起点的上一条边

看出next=1,自然,i=mp[5].next,也就是i=1

就去找到了mp[1]

这就是链式前向星!

很自然的可以知道,链式前向星的遍历是添加边时候的逆序遍历。

删:

复制代码
void del(int x,int y)
{
    //删除x到y的边 
    int last=0;
    for(int i=head[x];i;i=mp[i].next)
    {
        int z=mp[i].to;
        if(y==z)
        {
            if(i==head[x])head[x]=mp[i].next;
            else mp[last].next=mp[i].next;
            break;
        }
        last=i;
    }
}
del
复制代码

其实懂了添加以后,删除操作也就简单了,

对应修改一下next和head数组的值即可

 

最后,借以dijkstra算法来理解链式前向星吧。

复制代码
#include<cstdio>
#include<cstring>
#include<cmath>
#include<queue>
#include<algorithm>
using namespace std;
typedef long long ll;
#define N 1000009
#define INF 0x3f3f3f3f
/*

 
#define N 2021
int head[N],num;
/* 
首先,num为边的编号,这个很简单理解 
head数组要跟mp结构体里的next一起理解,head[x]表示起点为x的路径(一串边的编号的最后一个),
每次更迭都会将head[x]的值存入mp[num].next
然后遍历访问的时候,通过访问next来找上一边 
*/ 
struct iii
{
    int to;//指向下一点 
    int next;//配合head数组理解 
    int dis;//没有边权的时候可以不写 
}mp[N];
int n,m,s,dis[N],vis[N],head[N],num;
int cnt=0;
struct iii
{
    int to,next,dis;
}mp[N];
struct ii
{
    int dis,pos;
    bool operator <(const ii &x)const
    {
        return x.dis<dis;
    }
};
priority_queue<ii>q;
void add(int x,int y,int z)
{
    //增加x到y的边,权值为z 
    mp[++num].dis=z;
    mp[num].to=y;
    mp[num].next=head[x];
    head[x]=num;
    printf("mp[%d]:dis=%d, to=%d, next=%d, head[%d]=%d\n",num,z,y,mp[num].next,x,num);
}
void del(int x,int y)
{
    //删除x到y的边 
    int last=0;
    for(int i=head[x];i;i=mp[i].next)
    {
        int z=mp[i].to;
        if(y==z)
        {
            if(i==head[x])head[x]=mp[i].next;
            else mp[last].next=mp[i].next;
            break;
        }
        last=i;
    }
}
void Dijkstra()
{
    printf("第%d次:\n",cnt++);
    dis[s]=0;
    q.push((ii){0,s});
    while(q.size())
    {
        ii now=q.top();
        q.pop();
        int x=now.pos,y;
        if(vis[x])continue;
        vis[x]=1;
        for(int i=head[x];i;i=mp[i].next)
        {
            y=mp[i].to;
            printf("%d->mp[%d].to==%d\n",x,i,y);
            if(dis[y]>dis[x]+mp[i].dis)
            {
                dis[y]=dis[x]+mp[i].dis;
                if(!vis[y])
                {
                    q.push((ii){dis[y],y});
                }
            }
        }
    }
    while(q.size())q.pop();
}
int main()
{
    /*
示例数据:
    5 5 1
    1 2 10
    2 3 1
    4 5 10
    3 4 10     
    1 3 20 

    */ 
    num=0;
    scanf("%d %d %d",&n,&m,&s);
    for(int i=1;i<=n;++i)dis[i]=INF,vis[i]=0;  
    for(int i=1;i<=m;++i)
    {
        int x,y,z;
        scanf("%d %d %d",&x,&y,&z);
        add(x,y,z);
    }
    puts("\n原版:");
    Dijkstra();
    for(int i=1;i<=n;++i)
        printf("%d ",dis[i]);
    
    for(int i=1;i<=n;++i)dis[i]=INF,vis[i]=0; 
    puts("\n删2到3的边后:");
    del(2,3);
    Dijkstra();
    for(int i=1;i<=n;++i)
        printf("%d ",dis[i]);
    //del(2,3);
    
    for(int i=1;i<=n;++i)dis[i]=INF,vis[i]=0;  
    puts("\n删后又恢复性增加2到3的边后:");
    add(2,3,1); 
    Dijkstra();
    for(int i=1;i<=n;++i)
        printf("%d ",dis[i]);
    return 0;
}
草稿代码
复制代码

 

退一步说,现在来说算法吧。

概括性的说大致也就几种经典算法,根据题意和数据范围来选择。

1.Floyd算法

复制代码
for(int k=0;i<n;++k)
for(int i=0,i<n;++i)
for(int j=0;j<n;++j)
mp[i][j]=min(mp[i][j],mp[i][k]+mp[k][j]); 
Floyd
复制代码
Floyd算法适用于APSP(All Pairs Shortest Paths,多源最短路径),是一种动态规划算法,稠密图效果最佳,边权可正可负。此算法简单有效,由于三重循环结构紧凑,对于稠密图,效率要高于执行|V|次Dijkstra算法,也要高于执行|V|次SPFA算法。
优点:容易理解,可以算出任意两个节点之间的最短距离,代码编写简单。
缺点:时间复杂度比较高,不适合计算大量数据。
只需要按照k递增的顺序进行枚举,就能在n3的时间内求解,又第一维的状态可以采用滚动数组进行优化,所以空间复杂度为 n2
2.Spfa算法
复制代码
    queue<int>q;
    mt(in);//负权处理 
    q.push(s);
    while(q.size())
    {
        int x=q.front(),y;
        q.pop();
        vis[x]=0;//还可以松弛操作 
        for(int i=head[x];i;i=mp[i].next)
        {
            y=mp[i].to;
            if(dis[y]<dis[x]+mp[i].dis)
            {
                dis[y]=dis[x]+mp[i].dis;
                if(!vis[y])
                {
                    vis[y]=1;//已使用 
                    q.push(y);
                    if(++in[y]>n)return ;
                }
            }
        }
    }
(链式前向星遍历)关键部分
复制代码

SPFA(Shortest Path Faster Algorithm)算法是求单源最短路径的一种算法,它是Bellman-ford的队列优化算法。

很多时候,给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。SPFA的复杂度大约是O(kE),k是每个点的平均进队次数(一般的,k是一个常数,在稀疏图中小于2)。

但是,SPFA算法稳定性较差,在稠密图中SPFA算法时间复杂度会退化。

另外,SPFA算法还可以判断图中是否有负权环,即一个点入队次数超过N。

3.Dijkstra算法

复制代码
dis[s]=0;
    q.push((ii){0,s});
    while(q.size())
    {
        ii now=q.top();
        q.pop();
        int x=now.pos,y;
        if(vis[x])continue;
        vis[x]=1;
        for(int i=head[x];i;i=mp[i].next)
        {
            y=mp[i].to;
            printf("%d->mp[%d].to==%d\n",x,i,y);
            if(dis[y]>dis[x]+mp[i].dis)
            {
                dis[y]=dis[x]+mp[i].dis;
                if(!vis[y])
                {
                    q.push((ii){dis[y],y});
                }
            }
        }
    }
(链式前向星遍历)关键部分
复制代码

Dijkstra 算法求的是正权图的单源最短路问题,对于权值有负数的情况就不能用 Dijkstra 求解了,因为如果图中存在负环,可以从起点走到负环处一直将权值变小,从而使得 Dijkstra 带优先队列优化的算法就会进入一个死循环。

 

其实,spfa和dijkstra算法都一样,只是vis处理相反,作用也就不太一样了。

posted @   Renhr  阅读(76)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!
点击右上角即可分享
微信分享提示