【题解】P1073 [NOIP2009 提高组] 最优贸易(图论,最短路,SPFA)

【题解】P1073 [NOIP2009 提高组] 最优贸易

此题妙哉!

虽然说这只是道绿题,但是还是卡了我好久,主要是这道题里面有两个思想我完全想不到。我太菜了

写一下思路来记录一下这道绝妙的题目吧。


题目链接

[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\) 路径上点权最大的点

那么这一点对我们做题有什么启发呢?

我们可以发现,假如我们找到了以下三个,那么这个问题就解决了:

  1. 中继节点 \(k\)

  2. \(1-k\) 路径上点权最小的点;

  3. \(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\) 的最小值。

于是这道题目就完美解决!

求解步骤

  1. 对于输入的所有边,用两个 basic_string 分别作为正反图存边的信息。(注意双向边正反都要存一遍)

  2. \(i\) 为起点在原图上跑一遍“最短点权“的 SPFA,求出所有 \(dis1_i\)

  3. \(n\) 为起点在反图上跑一遍“最短点权”的 SPFA,求出所有 \(dis2_i\)

  4. 枚举每个 \(i\),则 \(ans=\min(dis2_i-dis1_i)\)

关键点

  1. 题意转化;

  2. 想到将点权转化为边权来跑 SPFA;

  3. 想到建立反图;

其中 2 3 也是本题最重要的两个思想。

易错点

  1. 思路上,此题不能用 dijkstra 来求,因为不满足 dijkstra 的贪心性质;

  2. 代码实现上,记得除 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] 即可。
妙哉!
*/ 

posted @ 2022-05-22 07:48  向日葵Reta  阅读(282)  评论(2编辑  收藏  举报