[图论] [思维] 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;
}

浙公网安备 33010602011771号