AcWing 341. 最优贸易(spfa + dp)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

前置理论:

在题解这道题之前先讲一下 dp和最短路的关系

其实最短路和dp的交集是很大的。dp问题运用集合分析完之后,把所有的方案归为若干类之后,每一类都用dp[i, j]状态表示(我们以二维为例子),我们将每一个状态看做图当中的一个点,如果dp[i, j]可以由某个状态更新的话,

如背包问题中的:dp[i, j] = max(dp[i-1, j], dp[i-1, j-vi] + wi)

dp[i, j]这个状态点可以dp[i-1, j]dp[i-1, j-vi]两个状态点转移而来,且边权重一条是0,一条是wi

那么,我们可以把状态与状态之间的关系看做是图中点和边之间的关系,这样的话,我们就将它转化成了一个最长路(因为求的是max)问题。

就好比上面说的背包问题,终点dp[n, m]起点有很多dp[0, 0], dp[0, 1], ..., dp[0, m],我们可以翻转来看,求终点所有起点最长路并取max

由此我们可以发现大部分的dp问题都可以转化成一个特殊的拓扑图最短路 or 最长路问题(dp问题每个状态依赖关系一般没有环,是个拓扑结构)。

所以,最短路和dp问题的交集就是拓扑图,当然,非拓扑图不能用dp来做

这就启发我们:当一个dp问题的依赖关系不具有拓扑序的话,那么可以用最短路的方式求得该问题的最优解

举个例子方便理解:假如有以下dp关系:
微信图片_20220224142318.png

在图上显示大概是这样子:微信图片_20220224142424.png

如要求f[5],就必须求f[4],要求f[4]也就必须求f[3],要求f[3]又必须求f[2],要求f[2]又必须求f[4],这时候我们就发现,求的过程中发生了死锁。不过我们如果将这个问题转化成最短路问题就发现,这个问题是有解的,f[5]本质上就是求1~5的一条最短路径,我们发现虽然其中存在环,但不是负环,那么其实是可以求出来的,且路径是:1-->2-->3-->4-->5,答案是4

不过当dp问题中依赖关系不存在拓扑序,也不一定非要按上述的以最短路的方式求解,用 高斯消元 也是可以的。不过最短路适用于求最优化的问题,如果只是给出一系列等式关系的话(无min/max),则可以用高斯消元求解

有了这些理论,我们再来看看下面的题目。


题目描述

本题是让我们在一张节点带有权值(而非边带权)的图上找出一条从1n的路径,使路径上能选出两个点 p,q(先经过p 后经过q),并且“节点q的权值减去节点p的权值”最大。

样例

输入

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

输出

5

由样例构出一张图:
微信图片_20220224144003.png

图中的每个节点上带有权值,对于每条边,单箭头变式单向边双箭头表示无向边

如上图,1走到5,判断 先在某个城市买再到另一个城市卖,最多能赚多少钱?(每个城市都可以经过多次

我们可以选择这样走:1-->2-->3-->5(买入,减少1-->4(卖出,增加6-->5(又回到5

按照这样的方式,我们可以最高赚得5,对于5这个城市我们经过了两次

思路:

一、把握全局

按照dp的思路,从集合的角度来分析,如何能将答案求出来。

本题问的是:所有从1n的路径当中,先买再卖最大收益

我们用一个椭圆来表示所有从1n的路径集合,我们要求出集合里 每一个方案中差价的最大值

我们先来进行集合划分,将所有方案划分为若干不同的方案。

集合划分有两个原则:不重、不漏,但是本题要求的是最大值max,所有我们 并不要求不重复只要求不遗漏 即可,因为 两个子集有重复的话并不影响求出来的最大值max
(本题划分的子集就是有重复的,但是我们一定可以将整个答案覆盖

我们 以城市作为分界点 进行划分,如用1代表第一个集合,表示在城市1前买进,在城市1后卖出,第二个集合则代表在城市2前买进,在城市2后卖出,以此类推。(注意,在分界点这个城市,也可以进行买和卖

画在椭圆上就是这样子:
微信图片_20220224145814.png

按照这样的形式我们一定可以将所有方案枚举一遍,显然不同类之间会有重复

举个例子:我们如果按照这样的方案:1(买进)-->2-->3-->4(卖出),显然 城市1、2、3、4都可以作为分界点

但有重叠是没有关系的,任何一种方案都必然存在一个城市分界点,就算是在同一个城市买卖,那么分界点也是同一个城市,不会对答案造成影响。

我们把分得的 每一类都分别求得一个最大值最后取max,就一定可以得到我们想要的 整个集合的最大值


二、逐个击破

那么如何分别求取每一类最大值呢?

我们 以分界点k为例,求一下 在城市k前买,在城市k后卖,所有路线 差价的最大值 是多少。

我们可以运用“分段”的思想,先 求从1走到k过程中买入的最小值,即为dmin[k],它表示 所有从1走到k的路径中,点权重的最小值,之后 求从k走到n的过程中卖出的最大值,即为dmax[k]

求出两者后,那么以城市k为分界点 赚差价的最大值 就是:dmax[k]-dmin[k]

关于dmin[k]dmax[k]的求法

我们知道 1走到k有很多种走法k走到n也有很多种走法没有任何限制,且两边独立互不相干,只要使得 左半边取最小,右半边取最大 即可。

dmin[k]为例子,我们看一下一共可以 从哪些点走到k,比如我们 可以从s1、s2、...、st走到k,我们也可以用如下椭圆表示整个集合
微信图片_20220224154417.png

每一类都求得最小值,比如:从s1走到kmin{dmin[s1], wk},…,从st走到kmin{dmin[st], wk},我们 将所有类最小值都求出来后,将 所有结果取min,即可得到 整个集合的最小值

dmin[k]表达式:dmin[k] = min{dmin[s1], dmin[s2], ..., dmin[st], wk},其 本质就是一个dp的过程,但是我们发现这个问题中的依赖关系是 可能有环 的(题目中没有提到无环),因此不能用dp来做了,因此我们 转化成一道最短路问题(min)求解


三、最终做法

而对于最短路,我们可以想到两种做法,dijkstraspfa,不过到底用哪一个我们要从两个算法的原理出发

我们先分析dijkstra,它的核心在于:每次从堆中取出最小值的时候,如果我们能断定 取出来的最小值已确定且不会被其它点更新时,那就可以使用dijkstra算法,否则不可以使用。

可以证明的是,本题就不能用dijkstra算法

我们用spfa来写本题。求出每一个dmin的值,对于dmax是一样的道理,只需 将所有的边反向建立反向图从终点n出发反向求最大值即可。

我们终于分析出来了最终的做法

spfa每一个dmindmax 求出来后,枚举所有的分界点k,计算 dmax[k]-dmin[k]的最大值 即为答案。

算法

spfa + dp

时间复杂度:

O ( n + k m ) O(n+km) O(n+km)

代码

#include<bits/stdc++.h>

using namespace std;
const int N = 1e5+10, M = 2e6+10;//要建立两张无向图,因此M设为2e6
#define inf 0x3f3f3f3f
int dmin[N], dmax[N];
int lh[N], rh[N], e[M], ne[M], idx;//建正、反两张h表
int w[N];//存每个城市水晶球价格
bool st[N];
int n, m;

void add(int h[], int a, int b){
        e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void spfa(int h[], int dist[], bool ok)
{
        //要注意两个memset中sizeof的对象
        memset(dist, 0x3f, sizeof dmin);
        if(ok==true) memset(dist, -0x3f, sizeof dmax);
        queue<int> q;

        if(ok==false)
        {
                dist[1] = w[1];
                q.push(1), st[1] = true;
        }
        else
        {
                dist[n] = w[n];
                q.push(n), st[n] = true;
        }

        while(q.size())
        {
                int t = q.front();
                q.pop();
                st[t] = false;
                for(int i=h[t]; ~i; i=ne[i]){
                        int j = e[i];
                        if(!ok&&dist[j]>min(dist[t], w[j]) || ok&&dist[j]<max(dist[t], w[j]))
                        {
                                if(!ok) dist[j] = min(dist[t], w[j]);
                                else dist[j] = max(dist[t], w[j]);
                                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(lh, -1, sizeof lh), memset(rh, -1, sizeof rh);
        for(int i=0;i<m;++i)
        {
                int x, y, z;
                scanf("%d%d%d", &x, &y, &z);
                add(lh, x, y), add(rh, y, x);//先建一条边
                if(z==2){//如果是双向边我们再反向建一次
                        add(lh, y, x), add(rh, x, y);
                }
        }

        spfa(lh, dmin, false), spfa(rh, dmax, true);//正向做求最小值,反向求最大值

        int res = -1;
        for(int i=1;i<=n;++i)//枚举每个分界点
        {
                res = max(res, dmax[i] - dmin[i]);
        }
        printf("%d\n", res);

        return 0;
}


在写spfa函数的时候debug了很久,发现了一个使用memset不严谨导致的错误,避雷:

 memset(dist, 0x3f, sizeof dmin);
 if(ok==true) memset(dist, -0x3f, sizeof dmax);

对于这个函数,传入的dist是一段内存的首地址,因此 如果将memset函数写成memset(dist,0x3f,sizeof dist);会导致错误。

因为memsetsizeof的机制是返回传入首地址后一段连续的字节数。这里如果传入dist的话只会清空int*A即八字节的大小。因此如果想要清空整个数组的话,应该传入所开的全局变量dmin/dmax

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值