P7831 [CCO2021] Travelling Merchant

发布时间 2023-08-22 19:33:51作者: One_JuRuo

题目大意

给出一个有向图,每条边有两个权值,分别代表通过该路径的最小要求 \(r_i\),和通过后增加的值 \(p_i\)。问:从每个点出发,各需要至少多少初始值,才能不停走下去。

思路

首先,分析一下,如果设 \(f_i\) 为从 \(i\) 点出发需要的最少初始值。那么可以轻易的推出转移方程:\(f_i=\min(f_i,\max(f_j-p_{i,j},r_{i,j}))\) (存在有向路径 \(i\to j\))。

但是,因为有环的出现,没办法 dp,也没办法爆搜,所以我们需要从其他方面进行考虑。

很容易地想到一下两个性质:

  • 如果一个点没有办法进入一个环,那么他一定无解。

  • 如果一个点有解,那么 \(f_i\) 一定小于等于环中最大的 \(r_i\)。因为 \(p_i \geq 0\),所以初始值只可能变大或不变不可能变小,如果初始值等于最大的 \(r_i\),那它在这个环中哪条路径都可以走,自然满足条件。

如果正着找,需要搜索回溯,多半会 TLE。那为什么不反着建图,然后就可以用起点更新终点了(其实也可以不反着建图,就是更新要用终点更新起点)。

然后我们就可以用拓扑排序+贪心来做这道题啦。

先把所有最开始出度为 \(0\) 的点找到,然后去掉同时减去相连的点的出度,用这个方法可以找到所有无解的点,找到后也不用更新,就赋成原来的值,最后输出的时候特判就好,然后把所有相关的边和点都删掉。

然后把边按照 \(r_i\) 从大到小排序,每次取最大的边,用最开始推得转移方程更新对应点的值,然后删边、减出度,直到有个点出度为 \(0\) 了,就代表答案固定了,就可以入队了。

然后每次取队首,更新连接的点,然后删边、减出度,如果又有点出度为 \(0\),就又入队,直到队空,然后又开始找最大且没被删的边。

为什么要按照从大到小排序呢?如果最开始取了环中的较小边,某个点固定的答案,就可能不能满足较大边,而取最大边虽然最开始答案可能会比真实答案略大,但是最后一定是可以更新成正确答案的。

另外删边就直接用数组标记就好。

AC 代码

#include<bits/stdc++.h>
using namespace std;
const int inf=0x3f3f3f3f;
struct node{int u,v,id,r,p;node(int _u=0,int _v=0,int _id=0,int _r=0,int _p=0){u=_u,v=_v,id=_id,r=_r,p=_p;}}a[200005];
inline bool cmp(node a,node b){return a.r>b.r;}
int n,m,e[400005],ne[400005],id[400005],wr[400005],wp[400005],h[200005],idx=1,out[200005],ans[200005],x,y,z,k;
bool vis[200005];
queue<int>q;
inline void add(int a,int b,int i,int r,int p){e[idx]=b,id[idx]=i,wr[idx]=r,wp[idx]=p,ne[idx]=h[a],h[a]=idx++;}
int main()
{
	scanf("%d%d",&n,&m),memset(ans,0x3f,sizeof(ans));
	for(int i=1;i<=m;++i) scanf("%d%d%d%d",&x,&y,&z,&k),a[i]=node(x,y,i,z,k),add(y,x,i,z,k),out[x]++;//反向建边,还要记得存边用来排序,顺便统计出度
	for(int i=1;i<=n;++i) if(!out[i]) q.push(i);//找到出度为0的点
	sort(a+1,a+m+1,cmp);
	for(int i=1;i<=m;++i)
	{
		while(!q.empty())//更新答案
		{
			int u=q.front();q.pop();
			for(int i=h[u];i;i=ne[i])
			{
				if(!vis[id[i]])
				{
					vis[id[i]]=1,--out[e[i]];
					if(ans[u]!=inf) ans[e[i]]=min(ans[e[i]],max(wr[i],ans[u]-wp[i]));//此处特判是用于最开始的无解情况,无解情况就不能更新答案了
					if(!out[e[i]]) q.push(e[i]);
				}
			}
		}
		if(!vis[a[i].id]) vis[a[i].id]=1,ans[a[i].u]=min(ans[a[i].u],a[i].r),out[a[i].u]--;//每次找最大边,保证答案符合题意
		if(!out[a[i].u]) q.push(a[i].u);//出度为0了就要入队
	}
	for(int i=1;i<=n;++i) printf("%d ",(ans[i]!=inf)?ans[i]:-1);//输出记得特判-1
	return 0;
}