AcWing 341 最优贸易

本文深入解析SPFA算法在复杂图论问题中的应用,针对混合图中的最值路径求解,结合具体案例,阐述算法原理及实现细节,是算法竞赛与数据结构学习的优质参考资料。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目描述:

C国有 n 个大城市和 m 条道路,每条道路连接这 n 个城市中的某两个城市。

任意两个城市之间最多只有一条道路直接相连。

这 m 条道路中有一部分为单向通行的道路,一部分为双向通行的道路,双向通行的道路在统计条数时也计为1条。

C国幅员辽阔,各地的资源分布情况各不相同,这就导致了同一种商品在不同城市的价格不一定相同。

但是,同一种商品在同一个城市的买入价和卖出价始终是相同的。

商人阿龙来到C国旅游。

当他得知“同一种商品在不同城市的价格可能会不同”这一信息之后,便决定在旅游的同时,利用商品在不同城市中的差价赚一点旅费。

设C国 n 个城市的标号从 1~n,阿龙决定从1号城市出发,并最终在 n 号城市结束自己的旅行。

在旅游的过程中,任何城市可以被重复经过多次,但不要求经过所有 n 个城市。

阿龙通过这样的贸易方式赚取旅费:他会选择一个经过的城市买入他最喜欢的商品——水晶球,并在之后经过的另一个城市卖出这个水晶球,用赚取的差价当做旅费。

因为阿龙主要是来C国旅游,他决定这个贸易只进行最多一次,当然,在赚不到差价的情况下他就无需进行贸易。

现在给出 n 个城市的水晶球价格,m 条道路的信息(每条道路所连接的两个城市的编号以及该条道路的通行情况)。

请你告诉阿龙,他最多能赚取多少旅费。

注意:本题数据有加强。

输入格式

第一行包含 2 个正整数 n 和 m,中间用一个空格隔开,分别表示城市的数目和道路的数目。

第二行 n 个正整数,每两个整数之间用一个空格隔开,按标号顺序分别表示这 n 个城市的商品价格。

接下来 m 行,每行有 3 个正整数,x,y,z,每两个整数之间用一个空格隔开。

如果z=1,表示这条道路是城市 x 到城市 y 之间的单向道路;如果z=2,表示这条道路为城市 x 和城市 y 之间的双向道路。

输出格式

一个整数,表示答案。

数据范围

1≤n≤100000,
1≤m≤500000,
1≤各城市水晶球价格≤100

输入样例:

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

输出样例:

5

分析:

本题虽然只是考察spfa算法的基本应用,但是作为算法竞赛进阶指南上的题目,难度自然不小。题目中涉及的图是混合图,既有有向边又有无向边,求从1号点到n号点能赚的最大差价,最多只能买卖一次,每个城市可以多次经过。设从1号点走到n号点经过的点的序列为S,i是S序列中间某点,则f[i]表示从1经过i走到n能赚的最大差价,f[i] = max(dmax[i] - dmin[i]),其中dmin[i]表示从1走到i的所有路径中经过的最小大价格,dmax[i]表示从i走到n所有路径中经过的最大的价格。我们枚举一下i所有的情况,求下max就是本题的解了。看上去像是动态规划,然而却不能用动态规划来求解,动态规划一般用来求解DAG上最短路问题,但是对于本题,明显不是DAG,因此不能使用动态规划求解。

首先还是再来分析下题目抽象成的混合图,无向边可以看成两条有向边的组合,而求路径中经过的最值点加上一个点可以经过多次这样的设定是特别巧妙的,等解决了本题才会深刻的理解到这一点。题目条件的设定使得一个本来没有负权边的图产生了负权边的效果。下面作出一些假定来验证这个说法,如果不涉及有向边,全部是无向边的话,整个图应该是连通的,我们只需要因此遍历所有点的价格求出最大最小值,减一下就是答案了。而正因为存在有向边使得我们即使可以从1号点到达全图价格最低的点,也不能保证从价格最低的点一定能到达终点n,这就否定了简单的解法。如果全是有向边,不存在环的话,每个点可以多次经过的假设就没有意义了,直接dijkstra算法就能够解决。混合图加上每个点可多次经过加上求路径中的最值就彻底的否定了所有简单的解法了。

有一点是我们先要明确的,本题求的是路径中的最值而不是最短路径长度,dijkstra算法是可以处理一般的有向图、无向图中求起点到终点所有路径中最大最小边权的问题的,是什么导致了dijkstra算法在本题却失效了呢?我们假设有个无向图,1号点的价格是5,2号点的价格是3,从1号点到2号点有一条无向边,我们求从1号点经过所有路径回到1号点中遇见点最小的价格是多少时,用dijkstra算法求解,初始堆里只有起点1,发现松弛不了任何点,然后2号点加入点集,1号点此时已经出过堆了,不会被2号点松弛了,算法结束,求得的dmin[1] = 5,但是显然正确结果应该是3,存在无向边同时可多次访问点求最值就已经类似于在最短路问题中存在负权边了,负权边的存在使得dijkstra堆里面堆顶元素出堆后还可能被更新,使得dijkstra算法失效,这里从2号点可以到1号点,且2号点的价格低于1号点就说明1号点还能被后面的点松弛,也是同样的效果。

说了条件的巧妙后,还是回归主题来说下如何求解dmin[i],即从1号点到i的所有路径中经过的最低价格,最短路问题中存在负权边可以用spfa算法来解决,只需要将松弛的条件改成比较大小即可。正当你打算直接手撕spfa时,又发现依旧存在问题。我们知道,spfa算法是对BellmanFord算法的改进,优化之处在于只将被松弛过的边加入队列,进入下一轮的松弛中。还是那个无向图,1号点的价格是5,2号点的价格是3,从1号点到2号点有一条无向边。来执行下spfa算法,起点1加入队列,发现价格比2号点高,没法去更新2号点,所以2号点没能进入队列,算法结束,依旧没能求出dmin[1]的正确结果。但是我们并不能因此就认为spfa算法也是无效的,事实上,只要小小的改动下便可以解决这个问题。正常spfa算法的问题在于初始价格大的点没法松弛初始价格小的点,我们一开始就将除起点外所有点的价格都初始化为INF,这样就可以让所有能到达的点都有机会进入队列了,在松弛时,d[i]的值取决于上一点d[u]和w[i]、d[i]三者中的最小值。这样我们就知道了如何用spfa算法求dmin[i]的值了。

知道了求dmin[i],那么dmax[i]的值,也就是从i到n所有路径中经过的最高价格,是否同理可得呢?没那么简单。以下图为例:

从A出发目标是B点,我们之前spfa的解法实际上是从起点出发尝试去遍历所有能到达的点,如果求dmax[A]时,从A出发执行spfa的话,求得A能够到达的最大价格是5,也就是C点,dmax[A] = 5有什么问题嘛,当然有,我们的目标是B点,到达C后回不到B点了,这就违背了题意了,那为什么dmin不会求错呢,事实上所谓从1出发到i的最小价格,我们实际上求的是从i出发到达i的最小价格,因为从起点出发,在到达i时才会更新dmin[i],也就不存在不可达的问题了,那么求dmax时我们既然知道sfpa算法可以求从固定点到可变点的最值,不妨就倒着遍历,从终点B开始执行spfa、这样dmax[A]就是3了。或者说,我们定义dmax[i]就定义为从终点倒着能走到i的所有路径中经过的最大价格。当然,为了倒着遍历,我们需要建立逆邻接表。再次总结下,本题的spfa可以求的是从固定点到不定点i的最值。

总结下本题的思路:读入数据,建立邻接表、逆邻接表,然后从1号点按照邻接表执行spfa求出所有点的dmin,再从n号点按照逆邻接表执行spfa求出所有点的dmax,最后遍历下所有i,找出dmax[i] - dmin[i]的最大值就是本题的解了。

本着讲解透彻的原则,最后再将本题涉及到的细节提一下。首先是边数上限M我们要设为多少,题目是50w条边,不知道多少条是无向边,因此M至少是100w,又因为还要建立逆邻接表,因此M需要超过200w。题目的空间要求是64MB,一个int类型的数据是4B,最多可以申请16M也就是16 * 10^6个数据的空间,也就是大概存1600w个int类型的数据,空间是足够的。另外,逆邻接表的建立只需要增加一个h数组,也就是链表头数组,ne数组和e数组都是按照边的编号idx存在,邻接表和逆邻接表可以共用同一个数组,因为两个邻接表的存在,add函数也就应该加入一个链表头数组的形参了。最后,因为两次spfa设计的距离数组和链表都是不同的,因此距离数组d和链表头数组h都应该作为spfa的形参,在spfa函数内一定要注意初始化d数组时大小不能写成sizeof d,而要写成sizeof dmax,一开始没在意这个细节还卡了我一会才AC,原因就涉及到指针与数组的区别了,作为形参的数组会退化为指针,sizeof一下只能取得指针的大小,得不到整个数组的大小。总的代码如下:

#include <iostream>
#include <queue>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100005,M = 2000005;
int idx,h[N],rh[N],ne[M],e[M];
queue<int> q;
bool st[N];
int n,m,dmin[N],dmax[N],w[N];
void add(int *hh,int a,int b){
    e[idx]=b,ne[idx]=hh[a],hh[a]=idx++;
}
void spfa(int *h,int *d,int type){
    if(type){//求max
        memset(d,0,sizeof dmax);
        q.push(n);
        d[n] = w[n];
        st[n] = true;
    }
    else{//求min
        memset(d,0x3f,sizeof dmin);
        q.push(1);
        d[1] = w[1];
        st[1] = 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(type){
                int dist = max(w[j],d[u]);
                if(d[j] < dist){
                    d[j] = dist;
                    if(!st[j]){
                       q.push(j);
                       st[j] = true;
                    }  
                }
            }
            else{
               int dist = min(w[j],d[u]);
               if(d[j] > dist){
                    d[j] = dist;
                    if(!st[j]){
                       q.push(j);
                       st[j] = true;
                    }
                } 
            }
        }
    }
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)   scanf("%d",&w[i]);
    memset(h,-1,sizeof h);
    memset(rh,-1,sizeof rh);
    int x,y,z;
    while(m--){
        scanf("%d%d%d",&x,&y,&z);
            add(h,x,y),add(rh,y,x);
            if(z == 2)  add(h,y,x),add(rh,x,y);
    }
    spfa(h,dmin,0);
    spfa(rh,dmax,1);
    int res = 0;
    for(int i = 1;i <= n;i++)   res = max(res,dmax[i] - dmin[i]);
    printf("%d\n",res);
    return 0;
}