European Championship 2025 E - Porto Vs. Benfica
题意说的很清楚了,这里就不再过多阐述。
有个很直接了当的算法就是,枚举每一个点,并且枚举它相邻的一条边,从 \(1\) 跑到这个点的最短路加上这个点不经过这条边到 \(n\) 的最短路之和。
设 \(dist(u, \, v)\) 表示 \(u, \, v\) 之间的最短路,\(g(u, \, v)\) 表示从 \(u\) 开始不经过边 \((u, \, v)\) 到达 \(n\) 的最短路,一个合法的答案显然为 \(dist(1, \, u) + \max\{g(u, \, v)\}\)。
显然警察希望割边的时候是费用最大,所以我们用 \(g(u)\) 表示所有 \(g(u, \, v)\) 的最大值,如果设 \(f(u)\) 表示从 \(u\) 点开始,满足题意的答案的最小值,那么如果 \(u, \, v\) 相连,枚举所有 \(u\) 的临接点 \(v\),我们有 \(f(u) = \min\limits_{(u, \, v) \in G}\{\max\{f(v) + 1, \, g(u)\}\}\),由于我们想要更新的 \(f(u)\) 尽可能小,所以我们可以贪心的拿 \(f(v)\) 更小的点去更新,当一个未使用的点是最小的时候,我们称这个点是确定的,我们用这个最小的点去更新其他未确定的点,方法类似 Dijkstra,因此这一步是好做的,问题是如何得到 \(g\) 的值。
考虑从 \(n\) 开始的一颗 BFS 树,这颗树的树边是最短路径必须经过的,所以警察割边一定只会割树边,所以我们希望走一个横叉边来尽可能达到最短,显然叶子节点的值一定为所有能走的横叉边中价值的最小值,为了不让向上更新时出现不合法的情况,我们给每个点记录 \((d, \, v)\) 的二元组,表示从当前节点 \(u\) 走向 \(v\) 花费为 \(d\),显然我们可以用一个小根堆存储这些二元组,答案就恰好为堆顶。
如果 \(u\) 在树上含有儿子 \(v\),我们显然可以走向 \(v\) 再走横叉边,所以需要把儿子的 \((d, \, v)\) 更新到父亲,价值为 \((d + 1, \, v)\),此时做启发式合并,用标记永久化维护,我们向上合并的时候,可能 \((d, \, v)\) 中 \(v\) 会变成 \(u\) 子树的一个节点,这是不合法的,我们可以利用并查集维护此信息,当堆顶不合法时弹掉即可。
综上,复杂度主要来源于启发式合并,总时间复杂度 \(O(n\log^2{m})\)。
参考代码
#include<bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 2e5 + 10, INF = 0x3f3f3f3f;
int n, m;
int dist[N], f[N], g[N], p[N], delta[N];
vector<int> edge[N], tree[N];
map<PII, int> in_tree;
priority_queue<PII, vector<PII>, greater<PII>> h[N];
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
void bfs(int S) {
vector<int> st(n + 1);
queue<int> q;
q.push(S), st[S] = 1;
while (q.size()) {
int u = q.front();
q.pop();
for (auto v : edge[u]) {
if (st[v]) continue;
dist[v] = dist[u] + 1;
tree[u].push_back(v);
in_tree[{u, v}] = in_tree[{v, u}] = 1;
st[v] = 1, q.push(v);
}
}
}
void dfs(int u) {
for (auto v : edge[u]) {
if (!in_tree.count({u, v})) {
h[u].push({dist[v] + 1, v});
}
}
for (auto v : tree[u]) {
dfs(v);
delta[v] ++ ;
p[find(v)] = p[find(u)];
if (h[u].size() < h[v].size()) swap(h[u], h[v]), swap(delta[u], delta[v]);
while (h[v].size()) h[u].push({h[v].top().first + delta[v] - delta[u], h[v].top().second}), h[v].pop();
}
while (h[u].size() && find(u) == find(h[u].top().second)) h[u].pop();
if (h[u].size()) g[u] = h[u].top().first + delta[u];
else g[u] = INF;
}
void dijkstra(int S) {
vector<int> st(n + 1);
memset(f, 0x3f, sizeof f);
priority_queue<PII, vector<PII>, greater<PII>> heap;
f[S] = 0, heap.push({f[S], S});
while (heap.size()) {
int u = heap.top().second;
heap.pop();
if (st[u]) continue;
st[u] = 1;
for (auto v : edge[u]) {
if (f[v] > max(g[v], f[u] + 1)) {
f[v] = max(g[v], f[u] + 1);
heap.push({f[v], v});
}
}
}
}
void solve() {
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) p[i] = i;
for (int i = 1; i <= m; i ++ ) {
int a, b;
cin >> a >> b;
edge[a].push_back(b);
edge[b].push_back(a);
}
bfs(n);
dfs(n);
dijkstra(n);
if (f[1] != INF) cout << f[1] << "\n";
else cout << "-1\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int T = 1;
// cin >> T;
while (T -- ) solve();
return 0;
}