European Championship 2025 E - Porto Vs. Benfica

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;
}

posted @ 2025-03-31 20:44  YipChip  阅读(30)  评论(0)    收藏  举报