前置理论:
在题解这道题之前先讲一下 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
关系:
在图上显示大概是这样子:
如要求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
),则可以用高斯消元求解。
有了这些理论,我们再来看看下面的题目。
题目描述
本题是让我们在一张节点带有权值(而非边带权)的图上找出一条从1
到n
的路径,使路径上能选出两个点 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
由样例构出一张图:
图中的每个节点上带有权值,对于每条边,单箭头变式单向边,双箭头表示无向边。
如上图,从1
走到5
,判断 先在某个城市买,再到另一个城市卖,最多能赚多少钱?(每个城市都可以经过多次)
我们可以选择这样走:1-->2-->3-->5
(买入,减少1
)-->4
(卖出,增加6
)-->5
(又回到5
)
按照这样的方式,我们可以最高赚得5
,对于5
这个城市我们经过了两次。
思路:
一、把握全局
按照dp
的思路,从集合的角度来分析,如何能将答案求出来。
本题问的是:所有从1
到n
的路径当中,先买再卖的最大收益。
我们用一个椭圆来表示所有从1
到n
的路径集合,我们要求出集合里 每一个方案中差价的最大值。
我们先来进行集合划分,将所有方案划分为若干不同的方案。
集合划分有两个原则:不重、不漏,但是本题要求的是最大值max
,所有我们 并不要求不重复,只要求不遗漏 即可,因为 两个子集有重复的话并不影响求出来的最大值max
。
(本题划分的子集就是有重复的,但是我们一定可以将整个答案覆盖)
我们 以城市作为分界点 进行划分,如用1
代表第一个集合,表示在城市1
前买进,在城市1
后卖出,第二个集合则代表在城市2
前买进,在城市2
后卖出,以此类推。(注意,在分界点这个城市,也可以进行买和卖)
画在椭圆上就是这样子:
按照这样的形式我们一定可以将所有方案枚举一遍,显然不同类之间会有重复,
举个例子:我们如果按照这样的方案: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
,我们也可以用如下椭圆表示整个集合:
每一类都求得最小值,比如:从s1
走到k
为min{dmin[s1], wk}
,…,从st
走到k
为min{dmin[st], wk}
,我们 将所有类最小值都求出来后,将 所有结果取min
,即可得到 整个集合的最小值。
dmin[k]
表达式:dmin[k] = min{dmin[s1], dmin[s2], ..., dmin[st], wk}
,其 本质就是一个dp
的过程,但是我们发现这个问题中的依赖关系是 可能有环 的(题目中没有提到无环),因此不能用dp
来做了,因此我们 转化成一道最短路问题(min)求解。
三、最终做法
而对于最短路,我们可以想到两种做法,dijkstra
和spfa
,不过到底用哪一个我们要从两个算法的原理出发。
我们先分析dijkstra
,它的核心在于:每次从堆中取出最小值的时候,如果我们能断定 取出来的最小值已确定且不会被其它点更新时,那就可以使用dijkstra
算法,否则不可以使用。
可以证明的是,本题就不能用dijkstra
算法。
我们用spfa
来写本题。求出每一个dmin
的值,对于dmax
是一样的道理,只需 将所有的边反向,建立反向图,从终点n
出发反向求最大值即可。
我们终于分析出来了最终的做法:
用spfa
将 每一个dmin
和dmax
求出来后,枚举所有的分界点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);
会导致错误。
因为memset
的sizeof
的机制是返回传入首地址后一段连续的字节数。这里如果传入dist
的话只会清空int*A
即八字节的大小。因此如果想要清空整个数组的话,应该传入所开的全局变量dmin/dmax