Karp 最小平均权圈:O(nm) 递推恰好 j 条边的最小权行走表 F[j][v],再按 min-max 见证分数公式一次扫描得到 μ*,整数精确舍入到 8 位小数。
OJ: roj
题目 ID: 1506
难度:提高
标签:图论动态规划最小环一本通python
创建: 2026-09-30 15:06
更新: 2026-10-07 11:01
形式化题目
给定
圈
即圈上所有边权之和除以边数。求全图所有圈的平均权最小值
样例:下图中只有两类圈(不计同一条圈的反向不存在,因为是有向图),展示它们的平均权:
graph LR
n1["点 1"] -->|"5"| n2["点 2"]
n2 -->|"5"| n3["点 3"]
n3 -->|"5"| n1
n2 -->|"3"| n4["点 4"]
n4 -->|"3"| n1
| 圈 | 各边权 | 总权 | 平均权 |
|---|---|---|---|
取较小者,输出
正解
思路
朴素做法及其瓶颈。 最直接的想法是枚举所有圈再算平均权,圈的数量是指数级的。退一步,按长度枚举圈:对每个起点
关键观察:只需考虑闭合行走。 任何闭行走(起点终点相同)都能分解成若干个圈:总权等于各圈权之和、总长等于各圈长之和,因此闭行走的平均权是各圈平均权的加权平均,不可能小于其中最小者;反过来圈本身就是闭行走。所以
而
Karp 递推表。 定义
起点任取相当于把
Karp 定理(min-max 见证分数公式)。
直观理解:
正确性草证(下界方向):设
用样例走一遍这张表(
| 点 1 | 点 2 | 点 3 | 点 4 | |
|---|---|---|---|---|
例如
| 终点 |
|||||
|---|---|---|---|---|---|
| 点 1 | |||||
| 点 2 | |||||
| 点 3 | |||||
| 点 4 |
对四个
实现细节:array('q') 紧凑存储(72 MB);"不可达"用大哨兵 printf 的远离零取整),避免浮点误差在第 9 位上翻转答案。另一种同阶常见做法是二分平均值 + 判负环,需要约 50 轮全量松弛,常数约为本做法的 50 倍。
代码
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-09-30 15:30
# update_at: 2026-09-30 15:30
import sys
from array import array
INF = 10**15 # 不可达哨兵:真实路径权值和 |w| 合计至多 3×10^10,远小于此
def relax(n: int, edges: list[tuple[int, int, int | float]], is_int: bool) -> list[array]:
"""Karp 递推表 F[j][v]:恰好 j 条边、以 v 结尾的最小权行走(起点任取)。"""
kind = "q" if is_int else "d"
rows: list[array] = [array(kind, [0] * n)] # F[0][v] = 0:空行走权为 0
prev = rows[0]
for _ in range(n):
# 上一行是哨兵时不剪枝:哨兵 + 负权仍落在哨兵带内,永远不会冒充真实值
cur = [INF] * n
for u, v, w in edges:
x = prev[u] + w
if x < cur[v]:
cur[v] = x
prev = array(kind, cur)
rows.append(prev)
return rows
def pick_witness(rows: list[array], n: int) -> tuple[int, int]:
"""按 Karp 公式挑出最小平均值的见证对 (v, k):min_v max_k (F[n][v]-F[k][v])/(n-k)。"""
fn = rows[n]
best = [x / n for x in fn] # k=0 候选:F[0][v]=0,故为 F[n][v]/n
best_k = [0] * n
for k in range(1, n):
row = rows[k]
step = 1.0 / (n - k)
for v in range(n):
q = (fn[v] - row[v]) * step # 两个候选分数间隔 ≥ 1/(d1·d2),浮点比较不会误序
if q > best[v]:
best[v] = q
best_k[v] = k
v0 = min(range(n), key=best.__getitem__) # 取不到 F[n][v] 的点 best 恒大,不会当选
return v0, best_k[v0]
def round8(num: int, den: int) -> str:
"""把整数分数 num/den 四舍五入到 8 位小数(0.5 远离零取整,对齐 printf)。"""
sign = "-" if num < 0 else ""
scaled = (2 * abs(num) * 10**8 + den) // (2 * den)
whole, frac = divmod(scaled, 10**8)
return f"{sign}{whole}.{frac:08d}"
def solve() -> None:
data = sys.stdin.buffer.read().split()
it = iter(data)
n, m = int(next(it)), int(next(it))
# 权值允许实数(样例 2 的 -2.9);纯整数输入走精确整数运算,保证 8 位小数不被浮点误差带偏
edges: list[tuple[int, int, int | float]] = []
is_int = True
for _ in range(m):
u, v = int(next(it)) - 1, int(next(it)) - 1
token = next(it)
if b"." in token:
is_int = False
w: int | float = float(token)
else:
w = int(token)
edges.append((u, v, w))
rows = relax(n, edges, is_int)
v0, k0 = pick_witness(rows, n)
num = rows[n][v0] - rows[k0][v0] # 见证分数的分子,整数输入下精确
den = n - k0
print(round8(num, den) if is_int else f"{num / den:.8f}")
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* 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-10-05 05:22
* update_at: 2026-10-05 05:22
*/
#include <cstdio>
typedef long long ll;
const int MAXN = 3005; // 顶点数上限(n <= 3000)
const int MAXM = 10005; // 边数上限(m <= 10^4)
const double INF = 1e18; // 不可达哨兵:真实行走权值和至多 3*10^10,远小于此
int n, m;
int eu[MAXM], ev[MAXM]; // 每条有向边的起点、终点(下标从 1 开始)
double ew[MAXM]; // 每条边的权值(题目允许实数,如样例 2 的 -2.9)
// Karp 递推表:F[j][v] = 恰好走 j 条边、以 v 结尾的最小权行走(起点任取)
double F[MAXN][MAXN];
// 逐行递推 F 表:F[0][v] = 0 相当于超级源点向所有点连 0 权边,起点维度被折叠
void karp_dp() {
for (int v = 1; v <= n; ++v) F[0][v] = 0;
for (int j = 1; j <= n; ++j) {
for (int v = 1; v <= n; ++v) F[j][v] = INF;
for (int e = 1; e <= m; ++e) {
int u = eu[e], v = ev[e];
if (F[j - 1][u] >= INF / 2) continue; // 上一行不可达,不参与松弛
double x = F[j - 1][u] + ew[e];
if (x < F[j][v]) F[j][v] = x;
}
}
}
// Karp 定理:mu* = min_v max_k (F[n][v] - F[k][v]) / (n - k)
// 分数扫描用 long double:分子至多 6*10^10,long double 的精度保证
// 两个候选分数(间隔至少 1/(d1*d2) >= 1.1*10^-7)的比较不会误序
long double karp_answer() {
long double ans = 1e18L;
for (int v = 1; v <= n; ++v) {
if (F[n][v] >= INF / 2) continue; // 没有长度恰为 n 的行走到达 v,跳过
long double best = -1e18L;
for (int k = 0; k < n; ++k) {
long double fn = F[n][v];
long double fk = F[k][v];
long double q = (fn - fk) / (n - k); // 候选分数;F[k][v]=INF 时极小,不会当选
if (q > best) best = q;
}
if (best < ans) ans = best;
}
return ans;
}
int main() {
scanf("%d %d", &n, &m);
for (int e = 1; e <= m; ++e) {
scanf("%d %d %lf", &eu[e], &ev[e], &ew[e]);
}
karp_dp();
long double ans = karp_answer();
printf("%.8Lf\n", ans);
return 0;
}复杂度
- 时间复杂度:
。递推 行、每行扫 条边,共约 次松弛;公式扫描 次浮点比较(两个候选分数间隔至少 ,远大于浮点误差,比较不会误序)。与 C++ 正解同阶;实测最大规模组( 、 )约 s,本地限时 s 内通过,OJ 时限 ms 下按约定允许 Python TLE。 - 空间复杂度:
。 表 个 int64 共约 MB,低于 MB 限制。
总结
- 平均权问题先换模型:闭行走可分解为圈,故
等于所有闭行走的最小平均权——这让"恰好 条边"的 DP 能覆盖全部候选,不必固定起点。 - Karp 表 + min-max 公式:
递推出 (起点任取),再按 扫一遍即得精确的 ,比"二分答案 + 判负环"少掉对数轮松弛。 - 工程细节决定精度:哨兵带与真实值分离,热循环可以不加分支;答案是分母
的有理数,用整数四舍五入输出 8 位小数,天然免疫浮点误差。