[图论] [思维] P7831 Travelling Merchant

posted on 2024-03-12 13:33:47 | under | source

前言:nb题,下面是看了题解后的解析。

考虑二分,不可行,考虑 \(\rm dp\),定义 \(f_i\) 表示点 \(i\) 作为起点时最小初始资金。

然后有转移,对于有向边 \(edge(u,v)\),有 \(\max(r,f_v+p)\to f_u\)。容易想到暴力地像 \(\rm SPFA\) 一样求解,但是也会被像 \(\rm SPFA\) 一样卡掉。

考虑 \(\rm dijkstra\) 的贪心思想,即找出 \(f\) 值已经确定的点,用它更新其它点(与其相邻的),并将该点删去。

问题来到寻找已确定点。从基础情况——简单环入手:可发现对于全场 \(r\) 值最大边 \(edge(u,v)\),其中 \(f_u\) 的下界是 \(r\),并且上界也是 \(r\),于是 \(f_u\) 确定为 \(r\)。这条边没有存在意义了(直接删掉不会有后效性),便可破环为链回推。

对边双(多个简单环)呢?可以改进下寻找方式,每次删去 \(r\) 最大的边,并用 \(r\) 更新 \(f_u\)。直到出现第一个出度为 \(0\) 的点,它便是已确定的点。删去它然后回推,之后若与其相邻的点出度变为 \(0\),则将它也是已确定的点(很像拓扑排序)。于是就删掉了其中一个环。依次类推即可。

对有向图呢?显然可以将边双的方式推广。但是过程中貌似不能保证初始资金为当前 \(\max r\) 时一定可一直走下去,因为可能走到出度为 \(0\) 的点。

不过显然出度为 \(0\) 的点本身是无解的,那么我们将其删去,又能得到其它出度为 \(0\) 的点。不断删去,最终就得到一个新边双。

分析下这样做的复杂度:每条边只被松弛一边。而寻找 \(r\) 最大的边部分,由于不会新增边,于是可以先排序并记录边是否被删去,删去跳过即可。复杂度 \(O(m\log m)\),瓶颈在于排序。

有关正确性:说句不负责的话,\(\rm dijkstra\) 为啥对它就为啥对,根本思路基于 \(\rm dijkstra\)

代码

#include<bits/stdc++.h>
using namespace std;

#define int long long
const int N = 4e5 + 5, inf = 1e15;
int n, m, u, v, r, p;
int vis[N], chu[N], ans[N];
struct edge {int u, v, r, p, id;} e[N];
vector<edge> to[N];
queue<int> q;

inline bool cmp(edge A, edge B) {return A.r > B.r;}
signed main(){
//	freopen("P7831_1.in", "r", stdin);
	cin >> n >> m;
	for(int i = 1; i <= m; ++i) 
		scanf("%lld%lld%lld%lld", &e[i].u, &e[i].v, &e[i].r, &e[i].p);
		
	sort(e + 1, e + 1 + m, cmp);
	for(int i = 1; i <= m; ++i)	e[i].id = i, to[e[i].v].push_back(e[i]), ++chu[e[i].u];
	for(int i = 1; i <= n; ++i) {ans[i] = inf; if(!chu[i]) q.push(i);}
	while(!q.empty()){
		int v = q.front(); q.pop();
		for(auto E : to[v]){
			vis[E.id] = 1, --chu[E.u];
			if(!chu[E.u]) q.push(E.u);
		}
	}
	for(int i = 1; i <= m; ++i){
		if(vis[i]) continue;
		vis[i] = 1, ans[e[i].u] = min(ans[e[i].u], e[i].r), --chu[e[i].u];
		if(!chu[e[i].u]) q.push(e[i].u);
		while(!q.empty()){
			int v = q.front(); q.pop();
			for(auto E : to[v]){
				if(vis[E.id]) continue;
				vis[E.id] = 1, --chu[E.u];
				if(ans[v] != inf) ans[E.u] = min(ans[E.u], max(E.r, ans[v] - E.p));
				if(!chu[E.u]) q.push(E.u);
			}	
		}	
	}
	//这里的写法比较笨,实际上循环外的拓朴排序是可以去掉的,只要更改循环内顺序即可
	//但是如果这样写就不能直接 if(vis[i]) continue;,否则可能出现玄学问题,推测为未及时更新 
	for(int i = 1; i <= n; ++i) printf("%lld ", ans[i] == inf ? -1 : ans[i]);
	return 0;
}
posted @ 2026-01-13 11:21  Zwi  阅读(0)  评论(0)    收藏  举报