[传智杯 #2 决赛] 传送门

GitHub跳转原题关系图返回列表

Floyd 后枚举传送门端点,逐点对比较原路和两个传送方向。

OJ: luogu

题目 ID: P6464

难度:普及+/提高-

标签:Floyd枚举全源最短路python

创建: 2026-07-17 03:00

更新: 2026-10-20 15:32

题意

选择两个点安装距离为 0 的双向传送门,最小化所有无序点对最短路之和。

思路

传送门等价于在 a、b 之间加一条权为 0 的双向边。最直接的做法是枚举 C(n,2) 个端点对,每次加边后重跑 Floyd,复杂度 O(n^5)。

这个算法不需要:因为只有一条边被改动,重算的 99% 是重复的。

为什么不必重跑 Floyd

一条新最短路只有两种形态,分开看就能直接写出答案:

不走传送门。 原图的边全在,旧路径依然可用,所以最短路不会变长,距离仍是原矩阵里的 d[x][y]。这一支不需要任何计算,这就是最优子结构在起作用:子问题的最优值在「不使用新边」的前提下保持不变,整张表可以复用。

走传送门。 这时传送门用几次才是真正的卡点。

零权让「多次使用」退化成「一次」

如果一条路径两次经过传送门,比如 ... → a → b → ... → a → b → ...,那么中间那段「从 b 回到 a」的行程可以整段删掉,长度不会增加——因为传送门本身的代价是 0。所以任何用到传送门的路径都可以化简成只用一次。

这一条是有前提的:新边权必须非负。本题传送门权为 0,所以重复使用无收益。换成「传送一次要花 c」且 c ≥ 0 仍成立;若 c < 0,绕环可以用越多次越短,路径会无限变短,「至多用一次」直接失效。

传送门是双向的,所以「用一次」有两个方向:

d′[x][y]=min⁡(d[x][y]⏟不用, d[x][a]+d[b][y]⏟经 a→b, d[x][b]+d[a][y]⏟经 b→a) d'[x][y] = \min\big(\underbrace{d[x][y]}_{\text{不用}},\ \underbrace{d[x][a] + d[b][y]}_{\text{经 } a \to b},\ \underbrace{d[x][b] + d[a][y]}_{\text{经 } b \to a}\big)

三个候选对应三种互斥情形,取最小即得新距离。每对 (x,y) 只花 O(1),不必重跑 Floyd。

单条边才能一次松弛到底

上面的公式只做了一轮 min。这足够,是因为只加一条边:新边权为 0、且不产生新的中间点,因此不存在「先用新边、再回头用新边」这种需要多轮迭代的依赖。若同时加入一整批新边,或多对端点各自加边且互相影响,单轮 min 就盖不全了,必须重新跑 Floyd。

三个层次缺一不可

层次 内容 作用
最优子结构 不用新边的路径长度仍等于原 d[x][y] d 表可直接复用
零权退化 权为 0 时多次使用可化简为一次 候选只有 2 个方向
单边增量 只加一条边,一轮松弛即到不动点 O(1) 公式可代替重跑

前两层让你能把答案写成三个候选,第三层让你不用迭代。少了任何一条,这个 O(n^4) 的写法就不成立。

边界:点对 (a,b) 本身也要算

传送门建成后 d'[a][b] = 0,而 (a,b) 也是总和中一项(样例里“1 → 4 的最短路是 0”就计入了总和)。只枚举 a < b 且求和时用 x < y,(a,b) 自然会被包含;但如果为了避开退化情形而把 a、b 从点对枚举里排除,就会少算这一项,答案偏大。

Python 知识

  • 二维列表直接保存百点规模距离矩阵。
  • 内层缓存行引用减少多重索引:row = distance[x] 之后只做一次二维访问。
  • min 接收三个候选,和上面的公式一一对应。
  • 枚举端点时只取 second > first,点对只取 y > x,用对称性把枚举量减半。

代码

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-07-17 01:40
# update_at: 2026-10-20 15:32
#
# main.py:传送门。只能装一对、把两栋楼之间距离变成 0,求安装后所有无序点对最短路之和的最小值。
# 全源最短路部分沿用 rbook 代码模板 floyed-py(《Floyd-Warshall 全源最短路》):
#   点编号 1..n,floyd(n, edges) 返回 (n+1) 阶矩阵,INF 表示不可达,平行边只保留最小权值。
#
# 传送门等价于在 a、b 之间加一条权为 0 的双向边。不必为每个候选重跑 Floyd:
#   不用这条新边:原图边全在,距离仍是原矩阵的 d[x][y](最优子结构,d 表直接复用);
#   用这条新边:权为 0,所以多次经过可化简成一次,且传送门双向,只有两个方向。
# 于是新距离就是三个候选取最小:
#   d'[x][y] = min(d[x][y], d[x][a] + d[b][y], d[x][b] + d[a][y])
# 每对 (x,y) 只花 O(1),整体 O(n^3) 一次 Floyd + O(n^4) 枚举。
# 注意:(a,b) 这一对本身也要计入总和,装传送门后它是 0。

import sys

INF = 1 << 60  # 哨兵,约 1.15e18,比任何合法路径都大

type Grid = list[list[int]]


def floyd(n: int, edges: list[tuple[int, int, int]]) -> Grid:
    """返回 n+1 阶距离矩阵;dist[0][*] 与 dist[*][0] 恒为 INF,不使用。"""
    dist: Grid = [[INF] * (n + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        dist[i][i] = 0  # 自己到自己距离 0

    for u, v, w in edges:
        if w < dist[u][v]:  # 平行边保留最小权值
            dist[u][v] = w

    for k in range(1, n + 1):
        for i in range(1, n + 1):
            if dist[i][k] == INF:
                continue  # i 到中转点 k 不可达,松弛无意义
            for j in range(1, n + 1):
                if dist[k][j] == INF:
                    continue
                nd = dist[i][k] + dist[k][j]
                if nd < dist[i][j]:
                    dist[i][j] = nd
    return dist


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n, m = next(data), next(data)

    edges: list[tuple[int, int, int]] = []
    for _ in range(m):
        u, v, w = next(data), next(data), next(data)
        # 无向边两个方向都要建;模板里按有向边处理,所以这里各加一次
        edges.append((u, v, w))
        edges.append((v, u, w))

    dist = floyd(n, edges)  # dist[i][j]:原图中 i 到 j 的最短路

    answer = INF
    for first in range(1, n + 1):
        for second in range(first + 1, n + 1):
            from_second = dist[second]
            from_first = dist[first]
            total = 0
            for x in range(1, n + 1):
                row = dist[x]  # 缓存行引用,内层只做一次二维访问
                left = row[first]  # d[x][a],内层循环里不变
                right = row[second]  # d[x][b],同理
                for y in range(x + 1, n + 1):
                    # 不用传送门 / 经 a->b / 经 b->a,三者取最小
                    total += min(row[y], left + from_second[y], right + from_first[y])
            if total < answer:
                answer = total

    print(answer)


if __name__ == "__main__":
    solve()

原有 C++ 版本仍保留:

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-17 01:40
 * update_at: 2026-10-20 14:55
 */
// 传送门:只能装一对、把两栋楼之间距离变成 0,求安装后所有无序点对最短路之和的最小值。
// 传送门等价于在 a、b 之间加一条权为 0 的双向边。不必为每个候选重跑 Floyd:
//   不用这条新边:原图边全在,距离仍是原矩阵的 d[x][y](最优子结构,d 表直接复用);
//   用这条新边:权为 0,所以多次经过可化简成一次,且传送门双向,只有两个方向。
// 于是新距离就是三个候选取最小:
//   d'[x][y] = min(d[x][y], d[x][a] + d[b][y], d[x][b] + d[a][y])
// 每对 (x,y) 只花 O(1),整体 O(n^3) 一次 Floyd + O(n^4) 枚举。
// 注意:(a,b) 这一对本身也要计入总和,装传送门后它是 0。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 105;         // 点数上界 n <= 100
const long long INF = (1LL << 60);

int n, m;
long long dist_node[MAXN][MAXN];  // 原图任意两点最短路;dist_node[i][i] = 0

void floyd() {
    for (int middle = 1; middle <= n; middle++) {
        for (int start = 1; start <= n; start++) {
            // 若 start 到 middle 都不可达,后面整轮都不用试
            if (dist_node[start][middle] == INF) continue;
            long long base = dist_node[start][middle];
            for (int finish = 1; finish <= n; finish++) {
                long long candidate = base + dist_node[middle][finish];
                if (candidate < dist_node[start][finish]) {
                    dist_node[start][finish] = candidate;
                }
            }
        }
    }
}

void read_input() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            dist_node[i][j] = (i == j) ? 0 : INF;
        }
    }
    for (int i = 1; i <= m; i++) {
        int u, v;
        long long w;
        cin >> u >> v >> w;
        // 题面保证无重边无自环,直接赋值即可
        dist_node[u][v] = w;
        dist_node[v][u] = w;
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    read_input();
    floyd();

    long long answer = INF;
    for (int first = 1; first <= n; first++) {
        for (int second = first + 1; second <= n; second++) {
            long long total = 0;
            for (int x = 1; x <= n; x++) {
                for (int y = x + 1; y <= n; y++) {
                    // 不用传送门 / 经 first->second / 经 second->first,三者取最小
                    long long best = dist_node[x][y];
                    long long through_first = dist_node[x][first] + dist_node[second][y];
                    long long through_second = dist_node[x][second] + dist_node[first][y];
                    if (through_first < best) best = through_first;
                    if (through_second < best) best = through_second;
                    total += best;
                }
            }
            if (total < answer) answer = total;
        }
    }

    cout << answer << '\n';
    return 0;
}

复杂度

一次 Floyd 是 O(n^3),枚举端点是 C(n,2) ≈ n^2/2,每个端点对枚举点对又是 C(n,2),合计约 2C(n,2)^2 ≈ n^4/2。

n = 100 时约 2.5 × 10^7 次 min 运算:C++ 轻松通过,Python 约 10 秒(本题无严格时限压力)。如果不能用“至多用一次”把重跑 Floyd 省掉,那才是 O(n^5) = 10^10,绝无可能通过。

总结

加一条权为 0 的边后,新最短路要么不用它、要么只用一次:不用时距离不变(最优子结构复用原表),用时只有两个方向。零权是“至多用一次”的前提,单条边是“一轮松弛即可”的前提,三条合起来才把 O(n^5) 降到 O(n^4)。