Teleporter

Teleporter

换根DP,树 + 传送门 + 限制传送次数 k 的最短路求和

1. 问题重述

给定一棵 n 个节点的树(边权为正),以及 m 条零代价的传送边(双向)。定义 d(u, k) 为从 u 出发,使用传送边不超过 k 次,到达节点 1 的最小时间代价。要求对于每个 k = 0,1,...,n,输出 ∑u d(u,k)。

2. 状态定义

令 dp[k][u] 表示从 u 出发,最多使用 k 次传送,到达 1 的最短时间。

  • 当 k = 0 时,不能使用传送,dp[0][u] = dist_tree(u, 1),可通过一次 DFS 求得。

3. 转移方程(关键转化)

考虑至少使用一次传送的情况。假设第一次传送发生在节点 x,通过传送边 (x, y) 到达 y,之后还剩 k-1 次传送机会。则
dp[k][u] = min( dp[0][u], min_{传送边 (x,y)} ( dist(u, x) + dp[k-1][y] ) )

为了高效计算,定义辅助数组 h[x]:
h[x] = min_{传送边 (x,y)} dp[k-1][y]
特别地,h[1] = 0(表示已经到达 1,无需再传送)。
那么转移简化为:
dp[k][u] = min_{x} ( dist(u, x) + h[x] )
即:每个节点 x 都有一个“源点代价” h[x],从 u 出发沿树边走到任意 x,总代价为树距 + h[x],取最小值。

4. 如何对每个 k 快速计算所有 dp[k][u]?

对于固定的 k,我们已经知道 dp[k-1][*],因此可以 O(m) 计算出所有 h[x](扫描所有传送边,取 min)。

现在问题转化为:给定一棵带边权的树,每个节点 x 有一个初始值 h[x](可能为 INF),求每个节点 u 到所有节点的 (dist(u, x) + h[x]) 的最小值。这是一个经典的树上多源最短路问题,可以通过换根 DP 在 O(n) 时间内解决,而无需 Dijkstra(因为图是树)。

5. 换根 DP 细节

我们定义:

  • down[u]:在 u 的子树内(包括 u)所有 x 中,min( dist(u, x) + h[x] )
  • up[u]:在 u 的子树外(即父节点方向)所有 x 中,min( dist(u, x) + h[x] )

最终 dp[k][u] = min(down[u], up[u])。

5.1 第一次 DFS(自底向上)

递归计算 down[u]:
down[u] = h[u] (选择自己)
对于每个儿子 v,边权 w:
down[u] = min(down[u], down[v] + w)
因为 down[v] 已经表示从 v 到其子树内某点的最优值,加上 w 即是从 u 到该点的距离。

5.2 第二次 DFS(自顶向下,换根)

我们需要为每个儿子 v 计算 up[v]。
对于父节点 u,首先求出所有儿子 v 的 (down[v] + w) 的最小值 min1 和次小值 min2,以及取得最小值的儿子 id。

当处理儿子 v 时,除 v 自身子树外,其他可能的外部源点来自:

  • u 的父方向(up[u])
  • u 自身(h[u])
  • u 的其他儿子子树(即 min1 或 min2,取决于 v 是否为 id)

因此,从 u 到 v 的外部最优代价 fath = min( up[u], h[u], other ),其中 other = (v == id) ? min2 : min1。
然后 up[v] = w + fath,即从 v 走到 u,再沿外部最优路径。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = 4e18;

int n, m;
vector<vector<pair<int, ll>>> g;
vector<pair<int, int>> tele;
vector<ll> pre_dp, dp;
vector<ll> h;
vector<ll> down, up;

void dist_dfs(int u, int p, ll dist) {
    pre_dp[u] = dist;
    for(auto &e : g[u]) {
        int v = e.first; ll w = e.second;
        if(v == p) continue;
        dist_dfs(v, u, dist + w);
    }
}
void dfs_down(int u, int p) {
    down[u] = h[u];
    for(auto&[v, w] : g[u]) {
        if(v == p) continue;
        dfs_down(v, u);
        down[u] = min(down[u], down[v] + w);
    }
}
void dfs_up(int u, int p) {
    //找到次短路,用于维护最小值所在的子树
    ll min1 = INF, min2 = INF;
    int id = -1;
    for(auto&[v, w] : g[u]) {
        if(v == p) continue;
        ll val = down[v] + w;
        if(val < min1) {
            min2 = min1; min1 = val; id = v;
        }
        else if(val < min2) min2 = val;
    }

    for(auto&[v, w] : g[u]) {
        if(v == p) continue;
        ll other = (v == id) ? min2 : min1;
        ll fath = min({up[u], h[u], other});
        //比较 祖父节点,自身传送源点,来自子树的距离
        up[v] = w + fath;
        dfs_up(v, u);
    }
}

void solve() {
    cin >> n >> m;
    g.assign(n + 1, {});
    tele.assign(m + 1, {});
    for(int i = 1; i < n; i ++) {
        int u, v; ll w; cin >> u >> v >> w;
        g[u].push_back({v, w});
        g[v].push_back({u, w});
    }
    for(int i = 0; i < m; i ++) cin >> tele[i].first >> tele[i].second;
    
    dp.assign(n + 1, 0);
    pre_dp.assign(n + 1, 0);
    h.assign(n + 1, -INF);
    down.assign(n + 1, -INF);
    up.assign(n + 1, -INF);

    //处理k=0的情况
    dist_dfs(1, -1, 0);
    ll ans = 0;
    for(int u = 1; u <= n; u ++) ans += pre_dp[u];
    cout << ans << '\n';

    for(int k = 1; k <= n; k ++) {
        fill(h.begin(), h.end(), INF); h[1] = 0;
        //更新势函数
        for(auto&[p, q] : tele) {
            h[p] = min(h[p], pre_dp[q]);
            h[q] = min(h[q], pre_dp[p]);
        }
        up[1] = INF;
        dfs_down(1, -1);
        dfs_up(1, -1);

        ll sum = 0;
        for(int u = 1; u <= n; u ++) {
            dp[u] = min(down[u], up[u]);
            sum += dp[u];
        }
        cout << sum << '\n';
        pre_dp.swap(dp);
    }
}

int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
    int _=1; //cin >> _;
    while(_--) solve();
    return 0;
}
posted @ 2026-09-04 15:02  thenrify  阅读(15)  评论(0)    收藏  举报