【题解】P1073 [NOIP2009 提高组] 最优贸易(图论,最短路,SPFA)
【题解】P1073 [NOIP2009 提高组] 最优贸易
此题妙哉!
虽然说这只是道绿题,但是还是卡了我好久,主要是这道题里面有两个思想我完全想不到。我太菜了
写一下思路来记录一下这道绝妙的题目吧。
题目链接
题意概述
有一张 \(n\) 个点 \(m\) 条边的图,其中有些边是双向边,有些边是单向边。每个点 \(i\) 都有一个点权 \(p_i\),要求在这张图上找到一条从 1 到 \(n\) 的路径,使路径上能经过两个点 \(s,t\)(先经过 \(s\) 再经过 \(t\)),并且 \(p_t-p_s\) 最大。
思路分析
首先,
要求在这张图上找到一条从 1 到 \(n\) 的路径,使路径上能经过两个点 \(s,t\)(先经过 \(s\) 再经过 \(t\)),并且 \(p_t-p_s\) 最大。
这个条件可以转化为:
有一个中继节点 \(k\),是 1 到 \(n\) 的路径上一点,同时,\(s\) 是 \(1-k\) 路径上点权最小的点,\(t\) 是 \(k-n\) 路径上点权最大的点。
那么这一点对我们做题有什么启发呢?
我们可以发现,假如我们找到了以下三个,那么这个问题就解决了:
-
中继节点 \(k\);
-
\(1-k\) 路径上点权最小的点;
-
\(k-n\) 路径上点权最大的点。
分步来看!
中继节点 \(k\)
可以发现,由于 \(1-n\) 有多条路径,所以除了 1 和 \(n\) 之外的所有点都可能成为中继节点(废话),那么我们枚举这个中继节点好了。
\(1-k\) 路径上点权最小的点
我们知道,SPFA 可以求最短路,那么它能不能求路径上点权最小的点呢?
实际上完全可以。
仿照 SPFA 求最短路的做法,我们定义 \(dis1_i\) 表示的是从节点 1 到节点 \(i\) 的所有路径中,能够经过的权值最小的节点的权值。(这就是本题的第一个重要思想)
\(dis1_i\) 的计算方法与单源最短路径的计算方法类似,只需要把 \(dis1_x+w(x,y)\) 更新 \(dis1_y\) 改成 \(\min(dis1_x,p_y)\) 更新 \(dis1_y\) 即可。
所以我们只需要以 1 为起点跑一遍这样的 SPFA 即可求出 \(1-n\) 路径上以任何一个其它的点为中继节点得到的点权最小的点。
\(k-n\) 路径上点权最大的点
显然我们可以仿照上面的做法,用 \(dis2_i\) 表示从节点 \(i\) 到 \(n\) 的所有路径中,能够经过的权值最大的节点的权值。然后更新也可以类比上面。
但是有一个问题。
我们应该以谁为起点跑 SPFA 呢?
以 \(x\) 为起点吗?显然不对啊。\(x\) 都不确定。
那么什么是确定的呢?终点 \(n\) 啊。
所以就用到了本题的第二个重要思想——建反图。
我们可以建立一张反图:在原图基础上把所有边的方向取反后得到的图。
然后再从 \(n\) 为起点跑一遍 SPFA。从而求出所有的 \(dis2_i\)。
最后,枚举每个中继节点 \(i\),答案即为所有 \(i\) 中 \(dis2_i-dis1_i\) 的最小值。
于是这道题目就完美解决!
求解步骤
-
对于输入的所有边,用两个 basic_string 分别作为正反图存边的信息。(注意双向边正反都要存一遍)
-
以 \(i\) 为起点在原图上跑一遍“最短点权“的 SPFA,求出所有 \(dis1_i\)。
-
以 \(n\) 为起点在反图上跑一遍“最短点权”的 SPFA,求出所有 \(dis2_i\)。
-
枚举每个 \(i\),则 \(ans=\min(dis2_i-dis1_i)\)。
关键点
-
题意转化;
-
想到将点权转化为边权来跑 SPFA;
-
想到建立反图;
其中 2 3 也是本题最重要的两个思想。
易错点
-
思路上,此题不能用 dijkstra 来求,因为不满足 dijkstra 的贪心性质;
-
代码实现上,记得除 1 外的 \(dis1\) 开始要初始化为 INF,1 的 \(dis1\) 初始化为 \(p_1\);
而除 \(n\) 外所有的 \(dis2\) 要初始化为 0;\(n\) 的 \(dis2\) 初始化为 \(p_n\)。
代码实现
//luoguP1073
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<string>
using namespace std;
const int maxn=1e5+10;
int n,m;
int p[maxn],dis1[maxn],dis2[maxn],ans;
basic_string<int>e1[maxn];
basic_string<int>e2[maxn];
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
void SPFA()
{
queue<int>q;
q.push(1);
for(int i=2;i<=n;i++)dis1[i]=(1ll<<29)-1;
dis1[1]=p[1];
while(!q.empty())
{
int x=q.front();
q.pop();
for(int y:e1[x])
{
if(dis1[y]>min(dis1[x],p[y]))
{
dis1[y]=min(dis1[x],p[y]);
q.push(y);
}
}
}
while(!q.empty())q.pop();
q.push(n);
dis2[n]=p[n];
while(!q.empty())
{
int x=q.front();
//cout<<x<<endl;
q.pop();
for(int y:e2[x])
{
//cout<<y<<endl;
//cout<<"ex "<<dis2[x]<<" "<<p[y]<<endl;
if(dis2[y]<max(dis2[x],p[y]))
{
dis2[y]=max(dis2[x],p[y]);
q.push(y);
}
}
}
}
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++)p[i]=read();
for(int i=1;i<=m;i++)
{
int x,y,z;
x=read();y=read();z=read();
if(z==1){e1[x]+=y;e2[y]+=x;}
else
{
e2[x]+=y;e2[y]+=x;
e1[x]+=y;e1[y]+=x;
}
}
//for(int i=1;i<=n;i++)cout<<e1[i].size()<<" "<<e2[i].size()<<endl;
SPFA();
for(int i=1;i<=n;i++)
{
//cout<<dis2[i]<<" "<<dis1[i]<<endl;
ans=max(ans,dis2[i]-dis1[i]);
}
cout<<ans<<endl;
}
/*两个 basic_string,两次 SPFA。
定义 dis1[i] 表示的是从 1 走到 i 的所有路径中能够经过的权值最小的节点的权值。
dis2[i] 表示的是从 i 走到 n 的所有路径中能够经过的权值最大的节点的权值
那么答案就为 min(dis2[i]-dis1[i]);
破点为边,在点权上做 SPFA。
做两次,分别求得 dis1[i] 和 dis2[i] 即可。
妙哉!
*/