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

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

此题妙哉!

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

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


题目链接

[NOIP2009 提高组] 最优贸易 - 洛谷

题意概述

有一张 nn 个点 mm 条边的图,其中有些边是双向边,有些边是单向边。每个点 ii 都有一个点权 pip_i,要求在这张图上找到一条从 1 到 nn 的路径,使路径上能经过两个点 s,ts,t(先经过 ss 再经过 tt),并且 ptpsp_t-p_s 最大。

思路分析

首先,

要求在这张图上找到一条从 1 到 nn 的路径,使路径上能经过两个点 s,ts,t(先经过 ss 再经过 tt),并且 ptpsp_t-p_s 最大。

这个条件可以转化为:

有一个中继节点 kk,是 1 到 nn 的路径上一点,同时,ss1k1-k 路径上点权最小的点,ttknk-n 路径上点权最大的点

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

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

  1. 中继节点 kk

  2. 1k1-k 路径上点权最小的点;

  3. knk-n 路径上点权最大的点。

分步来看!

中继节点 kk

可以发现,由于 1n1-n 有多条路径,所以除了 1 和 nn 之外的所有点都可能成为中继节点(废话),那么我们枚举这个中继节点好了。

1k1-k 路径上点权最小的点

我们知道,SPFA 可以求最短路,那么它能不能求路径上点权最小的点呢?

实际上完全可以。

仿照 SPFA 求最短路的做法,我们定义 dis1idis1_i 表示的是从节点 1 到节点 ii 的所有路径中,能够经过的权值最小的节点的权值。(这就是本题的第一个重要思想)

dis1idis1_i 的计算方法与单源最短路径的计算方法类似,只需要把 dis1x+w(x,y)dis1_x+w(x,y) 更新 dis1ydis1_y 改成 min(dis1x,py)\min(dis1_x,p_y) 更新 dis1ydis1_y 即可。

所以我们只需要以 1 为起点跑一遍这样的 SPFA 即可求出 1n1-n 路径上以任何一个其它的点为中继节点得到的点权最小的点。

knk-n 路径上点权最大的点

显然我们可以仿照上面的做法,用 dis2idis2_i 表示从节点 iinn 的所有路径中,能够经过的权值最大的节点的权值。然后更新也可以类比上面。

但是有一个问题。

我们应该以谁为起点跑 SPFA 呢?

xx 为起点吗?显然不对啊。xx 都不确定。

那么什么是确定的呢?终点 nn 啊。

所以就用到了本题的第二个重要思想——建反图

我们可以建立一张反图:在原图基础上把所有边的方向取反后得到的图。

然后再从 nn 为起点跑一遍 SPFA。从而求出所有的 dis2idis2_i

最后,枚举每个中继节点 ii,答案即为所有 iidis2idis1idis2_i-dis1_i 的最小值。

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

求解步骤

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

  2. ii 为起点在原图上跑一遍“最短点权“的 SPFA,求出所有 dis1idis1_i

  3. nn 为起点在反图上跑一遍“最短点权”的 SPFA,求出所有 dis2idis2_i

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

关键点

  1. 题意转化;

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

  3. 想到建立反图;

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

易错点

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

  2. 代码实现上,记得除 1 外的 dis1dis1 开始要初始化为 INF,1 的 dis1dis1 初始化为 p1p_1

    而除 nn 外所有的 dis2dis2 要初始化为 0;nndis2dis2 初始化为 pnp_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 @   向日葵Reta  阅读(353)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示