单源最短路问题 Codevs 1557 热浪(含讲解)

简介

最短路径问题是一个经典的图上问题。
比如从北京到上海怎么走花费的时间最短。
什么是单源最短路?就是起点只有一个的最短路径问题。
下面通过一道例题说明最短路的各种姿势。

例题

题目描述 Description

德克萨斯纯朴的民眾们这个夏天正在遭受巨大的热浪!!!他们的德克萨斯长角牛吃起来不错,可是他们并不是很擅长生產富含奶油的乳製品。Farmer John此时以先天下之忧而忧,后天下之乐而乐的精神,身先士卒地承担起向德克萨斯运送大量的营养冰凉的牛奶的重任,以减轻德克萨斯人忍受酷暑的痛苦。

FJ已经研究过可以把牛奶从威斯康星运送到德克萨斯州的路线。这些路线包括起始点和终点先一共经过T (1 <= T <= 2,500)个城镇,方便地标号為1到T。除了起点和终点外地每个城镇由两条双向道路连向至少两个其它地城镇。每条道路有一个通过费用(包括油费,过路费等等)。

给定一个地图,包含C (1 <= C <= 6,200)条直接连接2个城镇的道路。每条道路由道路的起点Rs,终点Re (1 <= Rs <= T; 1 <= Re <= T),和花费(1 <= Ci <= 1,000)组成。求从起始的城镇Ts (1 <= Ts <= T)到终点的城镇Te(1 <= Te <= T)最小的总费用。

输入描述 Input Description

第一行: 4个由空格隔开的整数: T, C, Ts, Te

第2到第C+1行: 第i+1行描述第i条道路。有3个由空格隔开的整数: Rs, Re和Ci

输出描述 Output Description

一个单独的整数表示从Ts到Te的最小总费用。数据保证至少存在一条道路。

样例输入 Sample Input

7 11 5 4

2 4 2

1 4 3

7 2 2

3 4 3

5 7 5

7 3 3

6 1 1

6 3 4

2 4 3

5 6 3

7 2 1

样例输出 Sample Output

7

数据范围及提示 Data Size & Hint

5->6->1->4 (3 + 1 + 3)


看不懂||不想看题面?

简单说,就是你沿着公路走(告诉你走每条路需要的时间),从A城市走到B城市所用的最短时间。

简单的单源最短路板子题(题目等级钻石aaa)

注意本题中的边为双向边。

Floyd

①这个算法一般用来处理多源最短路问题。
②Floyd跑这个题会TLE。
③Floyd—>戳这里qaq

Dijkstra(迪杰斯特拉)

思想:

找到没有被标记过的,最短距离已经确定的,且在所有符合上述条件中的最短距离最小的点v。

开 bool used[] 表示该点是否被标记过,dis[]表示这个点到起点的距离。

(先int v = -1; 然后 for 一遍所有的点, if(!used[i] && (v == -1 || d[v] > d[i] ) v = i;)

这样的话如果for没有找到v点,说明所有的点都被找过了,那么这时v=-1,我们可以if(v == -1)break;

如果找到了v点,那么v!=-1,这时标记v点(used[v] = true),然后更新v点相邻的点。

就是for一遍点,然后 dis[i] = min(dis[i], dis[v] + cost[v][i]);

这个语句解释一下就是:将i点到起点的距离更新为 【起点到v点 + v点到i点】和 d[i] 之中的较小值。

这样最后的dis[]就是各个点到起点的最小距离。

因为Dijkstra是从距起点最短的点开始更新的,所以在Codevs 1557 热浪 中,不会被

6 6 1 2
1 2 10000
2 3 5
3 4 4
4 5 3
5 6 2
6 1 1

这样的数据卡掉。

温馨(雾 提醒:自己动手跑一遍比空想效果更好哦=v=

注意: 有负权不能用Dijkstra 本质为贪心的它看不到远处的负权。

#include<cstdio>
#include<iostream>
#include<string>
#include<cstring>
#include<algorithm>
#include<stack>
#include<queue>
using namespace std;
const int maxn=2505;
int a,b,n,m,k,l,ans,dis[maxn],cost[maxn][maxn];
bool used[maxn];
void Dijkstra(int s)
{
    dis[s]=0;
    while(1)
    {
        int v = -1;

        for(int i=1;i<=n;i++)
            if(!used[i]&&(v==-1||dis[v]>dis[i]))
                v = i;

        if(v==-1)break;

        used[v] = true;

        for(int i=1;i<=n;i++)
            dis[i]=min(dis[i],dis[v]+cost[v][i]);

    }
    return;
}
int main()
{

    scanf("%d%d%d%d",&n,&m,&a,&b);
    memset(cost,63,sizeof(cost));
    memset(dis,63,sizeof(dis));
    for(int i=1;i<=m;i++)
    {
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        if(cost[x][y]>z)
        {                                                           //Dijkstra
            cost[x][y]=z;
            cost[y][x]=z;
        }
    }
    Dijkstra(a);
    printf("%d",dis[b]);
    return 0;
}
优化

普通的Dijkstra有几个问题:
①邻接矩阵(即cost[x][y])所用空间太大。
②随机的遍历顺序显然可以优化。

对于①我们可以通过邻接矩阵来搞,下面我会给出代码,但不作详细的讲解。

对于②,我们来想一下,如果我们从某一个点扩散出来的所有点中距起点最短的那个开始搜索,这样只是改变了搜索顺序,是不影响结果的正确性的。与从大到小搜索一比较,我们就少了很多操作(即更新该点dis[]的操作)。在不影响正确性的情况下减少操作数,显然可以得到时间上的优化。

为了完成②,我们需要用到优先队列(堆)。

代码
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int maxn=10005;
int dis[maxn],nxt[maxn<<1],head[maxn],m,n,k,ts,te,tot,ans;
bool used[maxn];
struct Edge{
    int  from,to,cost;
}edge[maxn<<1];//用来存边

struct Point{
    int dis,num;
};//用来实现堆优化

//下面四行是开一个堆(STL)
bool operator < (Point a,Point b)
{
    return a.dis > b.dis;//如果需要堆中元素从大到小排序,就把这里的“>”改成“<”。
}
priority_queue <Point> q;

void build(int from,int to,int cost)//建立邻接表
{
    edge[++tot]=(Edge){from,to,cost};
    nxt[tot]=head[from];
    head[from]=tot;
}

void Dijkstra(int s)//s为起点
{
    memset(dis,0x7f,sizeof(dis));
    dis[s]=0;//起点到起点的距离当然是0了
    q.push((Point){0,s});
    while(!q.empty())
    {
        int num=q.top().num;q.pop();
        if(used[num])continue;//每个点只入堆一次
        used[num]=true;
        for(int i=head[num];i;i=nxt[i])//遍历从该点延伸出去的所有边
        {
            if(dis[edge[i].from]+edge[i].cost < dis[edge[i].to])
            {
                dis[edge[i].to]=dis[edge[i].from]+edge[i].cost;
                q.push((Point){dis[edge[i].to],edge[i].to});
            }

        }

    }

}
int main()
{
    scanf("%d%d%d%d",&n,&m,&te,&ts);
    for(int i=1;i<=m;i++)
    {
        int from,to,cost;
        scanf("%d%d%d",&from,&to,&cost);
        build(from,to,cost);
        build(to,from,cost);//双向建边
    }
    Dijkstra(te);
    printf("%d",dis[ts]);
    return 0;
}

SPFA

思想:

通常用于有向图。
定义黑点(已经可以确定其到起点的最短距离并且一定不会更改的点,即它前面的点全都是黑点)、
蓝点(未确定或未完全确定最短距离并且与黑点相连的点)、
白点(未搜过且不与黑点相连的点,即与蓝点相连接的点)。这些有色点只是在脑子中的概念,实际打代码时不用真的标记颜色。

上面的话比较绕,建议自己画个图试一下,绝对有帮助。

用dis[]存某个点到起点的最短距离,
用bool used[]判断某个点现在是否在队列中。

首先将起始点(黑点)入队。
然后将队首元素弹出,让与它相邻的点(蓝点)入队,并更新这些点的dis[]。
再弹出队首元素,让与它相邻的点入队,并更新这些点的dis[]。

重复此过程,当队列为空或者某个点入队了n次(代表有负环)时,算法结束。

这时我们得到了一个dis[]数组,它储存了每个点到起点的最短距离。

是不是比Dijkstra简单?

SPFA的优越性在于:它能够处理负权!!!
比如这道题就不能用Dijkstra,只能用SPFA。

代码
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
using namespace std;
const int maxn = 100000 + 5 ;
int head[maxn] , next[maxn] , dis[maxn] ,num[maxn],tot = 0,m,n,ts,te,k;
bool used[maxn];
struct edge
{
    int f,t,v;
}l[maxn];
void build(int f, int t , int  v)//还是临界表,不会的快学吧=v=
{
    l[++tot] = (edge){f,t,v};
    next[tot] = head[f];
    head[f] = tot;
}
queue<int>q;
void spfa(int s)
{
    while(!q.empty())q.pop();
    q.push(s);
    used[s] = true;
    dis[s] = 0;
    num[s] ++;
    while(!q.empty())
    {
        int u = q.front();
        q.pop();
        used[u] =  0;
        for(int i = head[u] ; i != -1 ; i = next[i])
        {
            int v = l[i].t;
            if(dis[v] > dis[u] + l[i].v)
            {
                dis[v] = dis[u] +l[i].v;
                if(!used[v])q.push(v);
                num[v]++;
                if(num[v] > n + 1) 
                {
                    k = 1;
                    return ;
                }
                used[v] = 1;
            }
        }
    }
}
int main()
{
    memset(dis,0x7f,sizeof(dis));
    memset(head, -1,sizeof(head));
    int a,b,c;
    cin>>n>>m>>ts>>te;
    for(int i = 1 ; i <= m ; i ++)
    {
        cin>>a>>b>>c;
        build(a,b,c);
        build(b,a,c);
    }
    spfa(ts);
    cout<<dis[te];
    return 0;
}

至此,两种最常用的单源最短路算法就讲解结束了。

学得精的同学们可以做一下这道题(Codevs 1021 玛丽卡)。

最后祝您,身体健康,再见。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值