AcWing 1129 热浪

题目描述:

德克萨斯纯朴的民众们这个夏天正在遭受巨大的热浪!!!

他们的德克萨斯长角牛吃起来不错,可是它们并不是很擅长生产富含奶油的乳制品。

农夫John此时身先士卒地承担起向德克萨斯运送大量的营养冰凉的牛奶的重任,以减轻德克萨斯人忍受酷暑的痛苦。

John已经研究过可以把牛奶从威斯康星运送到德克萨斯州的路线。

这些路线包括起始点和终点一共有 T 个城镇,为了方便标号为 1 到 T。

除了起点和终点外的每个城镇都由 双向道路 连向至少两个其它的城镇。

每条道路有一个通过费用(包括油费,过路费等等)。

给定一个地图,包含 C 条直接连接 2 个城镇的道路。

每条道路由道路的起点 Rs,终点 Re 和花费 Ci 组成。

求从起始的城镇 Ts 到终点的城镇 Te 最小的总费用。

输入格式

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

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

输出格式

一个单独的整数表示从 Ts 到 Te 的最小总费用。

数据保证至少存在一条道路。

数据范围

1≤T≤2500,
1≤C≤6200,
1≤Ts,Te,Rs,Re≤T,
1≤Ci≤1000

输入样例:

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

输出样例:

7

分析: 

本题考查最简单的无负权的单源最短路,鉴于很久没刷图论题了,借本题回忆下单源最短路的几种算法,算法基础课中关于单源最短路的详细题解见AcWing 849 Dijkstra求最短路 IAcWing 850 Dijkstra求最短路 IIAcWing 853 有边数限制的最短路AcWing 851 spfa求最短路,分别介绍了朴素版的dijkstra算法、堆优化版的dijkstra算法、BellmanFord算法以及spfa算法。下面用这四种算法来解决本题。

方法一:朴素版dijkstra(O(n^2))

简述下朴素dijkstra算法的步骤:每次在还没被标记的点集里找出里起点距离最近的点,标记为已访问,同时用该点的距离去更新周围的点,由于每次都有一个顶点被标记为已访问,所以最多循环n次所有的点都被标记成已访问了。已经被标记的顶点离起点的距离是最小的,不会被更新了。

细节上要注意的有,一般朴素版dijkstra算法采用邻接矩阵实现,时间复杂度是O(n^2)。算法执行前要将三个数组都初始化一下,将存储图的邻接矩阵的初始值初始化为INF(这个很容易忽略,不初始化的话不存在边的顶点之间的边权就默认是0了),将距离数组d初始为为INF,只将起点的距离初始化为0,将访问标记数组st初始化为false。另外,无向图需要存两遍边。

分析下为什么每次找到还没被标记的点集中d最小的点一定不会被更新了,因为已经被标记的点集已经对周围的点都执行了松弛操作,设已经被标记的点集是S,未被标记的点集中有两点a和b,离点集S最近的点是a,假设此时a的d不是最小的,也就是说后面d还能被其他未被标记的点松弛,假设是b点,则d[a] > d[b] + dist[a][b],又d[a] < d[b],dist[a][b]是正权边,所以相互矛盾,故a的d一定是最小的。

总体来说本题的顶点数不是很大,用朴素版dijkstra算法解决效率还不错。

#include <iostream>
#include <cstring>
using namespace std;
const int N = 2505;
int g[N][N],d[N],s,e,n,m;
bool st[N];
int dijkstra(){
    memset(d,0x3f,sizeof d);
    memset(st,false,0);
    d[s] = 0;
    for(int i = 0;i < n;i++){//遍历n次
        int t = -1;
        for(int j = 1;j <= n;j++){
            if(!st[j] && (t == -1 || d[j] < d[t]))  t = j;
        }
        st[t] = true;
        if(t == e)  break;
        for(int j = 1;j <= n;j++){
            if(!st[j] && d[t] + g[t][j] < d[j]) d[j] = d[t] + g[t][j];//松弛操作
        }
    }
    return d[e];
}
int main(){
    scanf("%d%d%d%d",&n,&m,&s,&e);
    int a,b,c;
    memset(g,0x3f,sizeof g);
    for(int i = 0;i < m;i++){
        scanf("%d%d%d",&a,&b,&c);
        g[a][b] = g[b][a] = c;
    }
    cout<<dijkstra()<<endl;
    return 0;
}

方法二:堆优化版dijkstra(O(mlogn))

堆优化版dijkstra算法相对于朴素版的dijkstra算法最大的改动就是找还未被标记的点集中离起点最近的点时时不再是遍历所有的点,而是用一个小根堆动态的去维护顶点的距离,这使得时间复杂度降低到了O(mlogn),然而堆优化版的代码上相对于朴素版的代码变化却远不止这一处。

首先是存储上的变化,如果依旧采用邻接矩阵存储,堆优化版的dijkstra每次松弛操作依旧是O(n)的,这样时间复杂度依旧是平方级别的,所以需要采用邻接表存储。邻接表存储的过程是:存储a到b权重为c的边,首先将第idx 条边的终点b用e[idx]存储下,然后权重c用w[idx]存储下,接着采用头插法将这条边插到顶点a邻接表的首位,先用ne[idx]保存原先h[a]的第一条边的编号,在将h[a]的第一条边更新为idx,同时idx++。最后两步的顺序不能改变,采用头插法是为了防止断链。

然后细节的变化,朴素版算法固定循环n次,而堆优化版的外循环要改成优先级队列非空时继续循环,优先级队列默认是大根堆,这里存入距离的相反数就可以当小根堆用了。堆优化版dijkstra最大的难点在于对标志数组st的理解,出队的顶点执行了松弛操作就会被标记为true,下次再出队就break,不会再去松弛了,这样做的目的只是为了避免冗余。每个顶点第一次作为堆中距离最小的元素出队后,便不会再被松弛,不会再次入队,但是,在它出队之前,是可能多次入队的,比如a第一次被b更新进入了堆里,然后又被c松弛再次入队,就会出现堆中有多个a的问题,但是a第一次出队取得的必然是a的最小距离,所以不影响,用st数组剪枝下可以基本上杜绝执行无用的松弛操作。

#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
typedef pair<int,int> PII;
const int N = 2505,M = 12500;
int d[N],s,en,n,m;
int idx,h[N],e[M],ne[M],w[M];
priority_queue<PII> pq;
bool st[N];
void add(int a,int b,int c){
    e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a] = idx++;
}
int dijkstra(){
    memset(d,0x3f,sizeof d);
    memset(st,false,0);
    d[s] = 0;
    pq.push({-d[s],s});
    while(pq.size()){
        int t = pq.top().second;
        pq.pop();
        if(st[t])   continue;
        st[t] = true;
        if(t == en)  break;
        for(int i = h[t];~i;i = ne[i]){
            int j = e[i];
            if(!st[j] && d[t] + w[i] < d[j]){
               d[j] = d[t] + w[i];//松弛操作 
               pq.push({-d[j],j});
            } 
        }
    }
    return d[en];
}
int main(){
    scanf("%d%d%d%d",&n,&m,&s,&en);
    int a,b,c;
    memset(h,-1,sizeof h);
    for(int i = 0;i < m;i++){
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c),add(b,a,c);
    }
    cout<<dijkstra()<<endl;
    return 0;
}

方法三:BellmanFord(O(nm))

如果说dijkstra算法是用已经是最短距离的点去更新周围的点,那么BellmanFord算法就是尝试遍历每条边去更新所有的点。如果限制最短路径长度不能超过k,那么第一次边的更新会更新到距离起点路径长度是1的点,第k次更新会更新到路径长度是k的点,为了防止串联需要备份d数组,何谓串联,就是a更新了b,b又更新了c,则再这次循环中a实际更新了更远的点,每次循环更新的路径长度边不是1了。当然,本题没有k条边限制,发生串联还有助于松弛更快,所以无须备份。

由于BellmanFord算法需要遍历边集,所以用一个结构体数组来存储所有的边,结构体的成员包括边的起点、终点以及边权,无向图需要存储两次。然后算法的流程就是执行n-1次利用边权去更新顶点的操作,由于第k次循环距离起点路径长度是k的点都已经被更新成了最小距离,所以最多n-1次就更新完了整个图。万一第n次还有点的距离被更新了呢?就说明存在负权回路了,没有最短距离,如果没有负权回路,只是有负权边则不影响求最短路径,比如a到b的边权是-1,没有负权回路完全不用担心每次执行边的松弛a都会让b的距离减小,因为a的距离更新一定次数后是不变的,所以加上边权的值不论被更新多少次都是不变的。

BellmanFord算法较为简单,没有标志数组,几行代码就实现了,时间复杂度是O(nm)。

#include <iostream>
#include <cstring>
using namespace std;
typedef pair<int,int> PII;
const int N = 2505,M = 12500;
int d[N],s,en,n,m,idx;
struct Node{
    int x,y,w;
}edge[M];
int bellman_ford(){
    memset(d,0x3f,sizeof d);
    d[s] = 0;
    for(int i = 1;i < n;i++){
        for(int j = 0;j < idx;j++){
            if(d[edge[j].y] > d[edge[j].x] + edge[j].w)   d[edge[j].y] = d[edge[j].x] + edge[j].w;
        }
    }
    return d[en];
}
int main(){
    scanf("%d%d%d%d",&n,&m,&s,&en);
    int a,b,c;
    for(int i = 0;i < m;i++){
        scanf("%d%d%d",&a,&b,&c);
        edge[idx++] = {a,b,c};
        edge[idx++] = {b,a,c};
    }
    cout<<bellman_ford()<<endl;
    return 0;
}

方法四:spfa(O(m))

BellmanFord的弊端在于每次都要遍历所有的边,即使这次遍历并不能松弛顶点的距离。实际上,只有上一轮中被松弛的顶点才会去松弛相邻的顶点,因此可以将每轮松弛的顶点用一个队列存下来,只去松弛队列中顶点相邻的顶点,一般情况下spfa的复杂度是O(m),但是可能常数比较大,效率略低于堆优化版dijkstra。

梳理下spfa算法的流程:首先将初始顶点加入队列,取队头元素并将队头元素出队,尝试用队头元素去松弛周围的顶点,被松弛的顶点将继续加入到队列中。另外,已经在队列里的元素不用再次入队了,更新下它的距离即可,下次出队会按照最新的距离去执行松弛操作的。这里的st数组表示顶点是否在队列中,入队时置true,出队时置false,可以反复入队出队。而dijkstra是否中的st数组表示的是该顶点是否已经出过优先级队列,已经出队的d就是最小了,不必再次入队了,这就是两个算法st数组意义的不同之处。

#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
typedef pair<int,int> PII;
const int N = 2505,M = 12500;
int d[N],s,en,n,m;
int idx,h[N],e[M],ne[M],w[M];
queue<int> q;
bool st[N];
void add(int a,int b,int c){
    e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a] = idx++;
}
int spfa(){
    memset(d,0x3f,sizeof d);
    memset(st,false,0);
    d[s] = 0;
    q.push(s);
    st[s] = true;
    while(q.size()){
        int u = q.front();
        q.pop();
        st[u] = false;
        for(int i = h[u];~i;i = ne[i]){
            int j = e[i];
            if(d[j] > d[u] + w[i]){
                d[j] = d[u] + w[i];
                if(!st[j]){
                   q.push(j);
                   st[j] = true; 
                }
            }
        }
    }
    return d[en];
}
int main(){
    scanf("%d%d%d%d",&n,&m,&s,&en);
    int a,b,c;
    memset(h,-1,sizeof h);
    for(int i = 0;i < m;i++){
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c),add(b,a,c);
    }
    cout<<spfa()<<endl;
    return 0;
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值