把每条路线看成「首尾车次」之间的一条无向边做 01 分数规划:二分四舍五入后的答案 r,把边权换成 2s−(2r−1) 后用 SPFA 判正环,图无环输出 −1。
OJ: roj
题目 ID: 3215
难度:提高
标签:图论01分数规划负环判定SPFA
创建: 2026-10-02 02:17
更新: 2026-10-10 11:30
形式化题目
有五种车次,速度只由开头的字母决定:S 为 1000,G 为 500,D 为 300,T 为 200,K 为 150。一条行车路线由若干车次用 - 连接而成,它的速度和是各车次速度之和(例如 T120-D135-S1 的速度和为 200+300+1000=1500,与编号大小无关)。
给出
求所有高铁环的值的最大值,四舍五入到最接近的整数输出;若不存在任何环,输出
数据范围:
样例解释:三条路线 T120-D135-S1(和 1500)、S1-G12(和 1500)、G12-K856-T120(和 850)。T120 同时出现在第一条的开头与第三条的末尾,S1、G12 分别把三条路线串起来,构成环,环的值为
正解
思路
第一步:把路线缩成一条边。 一条路线能被接到哪里、又能接谁,完全由它的末尾车次和开头车次决定,中间车次只贡献速度和。因此把每个车次看成一个点,每条行车路线看成首尾两点之间的一条无向边:边权为该路线的速度和
graph LR
T120 -- "1500 (T120-D135-S1)" --- S1
S1 -- "1500 (S1-G12)" --- G12
G12 -- "850 (G12-K856-T120)" --- T120
于是"一个高铁环"就是这张图中的一个环,"环的值"就是环上边权的平均值:
第二步:朴素做法与瓶颈。 最直接的想法是枚举每个环算平均值——环可能极长、数量指数级,完全不可行。这是经典的01 分数规划模型:最大化"权值和 ÷ 个数"。
第三步:01 分数规划转判定。 设某个环
把每条边的权换成
第四步:处理"四舍五入"这个坑。 最优平均值为
所以不需要对
第五步:把“
其中
第六步:判正环的实现。 图可能不连通,所以把所有点的初始距离设为
代码
/**
* 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-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// 每条高铁路线建成一条有向边:开头车次 -> 末尾车次
// 边上存 a = (n+1)*(2*速度和) + 1
int n; // 车次种类数(节点数)
vector<pair<int, ll> > adj[5005];
// 判断是否存在平均值 >= r - 0.5 的环:对边权 a - (n+1)*(2r-1) 判严格正环
bool has_cycle_avg_ge(int r) {
// 边权 a - (n+1)*(2r-1)
ll offset = (ll)(n + 1) * (2LL * r - 1);
// 复制一份偏移后的图判断正环
// 直接改全局 adj 不方便,这里重新做一次 SPFA
vector<ll> dist(n, 0);
vector<int> edge_cnt(n, 0);
vector<char> in_queue(n, 1);
deque<int> q;
for (int i = 0; i < n; i++) q.push_back(i);
while (!q.empty()) {
int u = q.front(); q.pop_front();
in_queue[u] = 0;
ll du = dist[u];
for (size_t i = 0; i < adj[u].size(); i++) {
int v = adj[u][i].first;
ll a = adj[u][i].second;
ll nd = du + a - offset;
if (nd > dist[v]) {
dist[v] = nd;
edge_cnt[v] = edge_cnt[u] + 1;
if (edge_cnt[v] >= n) return true;
if (!in_queue[v]) {
if (!q.empty() && nd > dist[q.front()]) q.push_front(v);
else q.push_back(v);
in_queue[v] = 1;
}
}
}
}
return false;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int m;
cin >> m;
cin.ignore();
map<string, int> node_id;
vector<pair<string, string> > raw; // 每条路线 (开头车次, 末尾车次)
vector<int> raw_s; // 每条路线的速度和
for (int line = 0; line < m; line++) {
string s;
getline(cin, s);
// 拆成 codes
vector<string> codes;
string cur;
for (size_t i = 0; i <= s.size(); i++) {
if (i == s.size() || s[i] == '-') {
codes.push_back(cur);
cur.clear();
} else {
cur.push_back(s[i]);
}
}
int sum = 0;
for (size_t i = 0; i < codes.size(); i++) {
char c = codes[i][0];
if (c == 'S') sum += 1000;
else if (c == 'G') sum += 500;
else if (c == 'D') sum += 300;
else if (c == 'T') sum += 200;
else if (c == 'K') sum += 150;
}
raw.push_back(make_pair(codes[0], codes[codes.size() - 1]));
raw_s.push_back(sum);
if (node_id.find(codes[0]) == node_id.end())
node_id[codes[0]] = (int)node_id.size();
if (node_id.find(codes[codes.size() - 1]) == node_id.end())
node_id[codes[codes.size() - 1]] = (int)node_id.size();
}
n = (int)node_id.size();
ll scale = n + 1;
for (size_t i = 0; i < raw.size(); i++) {
int u = node_id[raw[i].first];
int v = node_id[raw[i].second];
adj[u].push_back(make_pair(v, scale * 2LL * raw_s[i] + 1));
}
int lo = 1, hi = 20001;
while (lo < hi) {
int mid = (lo + hi) / 2;
if (has_cycle_avg_ge(mid)) lo = mid + 1;
else hi = mid;
}
int ans = (lo > 1) ? lo - 1 : -1;
cout << ans << "\n";
return 0;
}#!/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-05 21:47
# update_at: 2026-10-02 02:23
import sys
from collections import deque
SPEED = {"S": 1000, "G": 500, "D": 300, "T": 200, "K": 150} # 五种车次的速度
def build_graph(lines_txt: list[str]) -> tuple[int, list[list[tuple[int, int]]]]:
"""把每条行车路线建成一条有向边,返回 (点数, 邻接表)。
连接规则是有向的:前一条路线的末尾车次接到后一条的开头车次,
所以路线对应"开头车次 → 末尾车次"的有向边,高铁环必须顺着箭头走。
边上存 a = (n+1)·(2×速度和) + 1,n 是点数;放大加一是为了让
"权值和 ≥ 0 的环"精确等价于"放大后权值和 > 0 的正环"(见判定函数)。
"""
node_id: dict[str, int] = {}
edges: list[tuple[str, str, int]] = []
for line in lines_txt:
codes = line.split("-")
s = sum(SPEED[code[0]] for code in codes) # 车次速度只看字母,与编号无关
edges.append((codes[0], codes[-1], s))
node_id.setdefault(codes[0], len(node_id))
node_id.setdefault(codes[-1], len(node_id))
n = len(node_id)
scale = n + 1 # 环的权值和是整数,放大 n+1 倍后每条边加 1 不会改变 ≥ 0 的判定
adj: list[list[tuple[int, int]]] = [[] for _ in range(n)]
for head, tail, s in edges:
u, v = node_id[head], node_id[tail]
adj[u].append((v, scale * 2 * s + 1))
return n, adj
def has_cycle_avg_ge(n: int, adj: list[list[tuple[int, int]]], r: int) -> bool:
"""判断是否存在平均值 ≥ r − 0.5 的高铁环(SPFA 判正环)。
环 C 满足 avg(C) ≥ r − 0.5 ⟺ Σ(2s−(2r−1)) ≥ 0。记 scale = n+1,由于
Σ(2s−(2r−1)) 是整数,"≥ 0" ⟺ "scale×它 + |C| > 0",后者恰好是对
边权 a−scale·(2r−1) 判严格正环——SPFA 的计数法只能找严格正环,
这个放大加一的恒等变换把边界情形(平均值恰为 k+0.5)也判对。
"""
offset = (n + 1) * (2 * r - 1)
dist = [0] * n # 虚拟超级源(连向所有点)出发的最长路,初值全 0
edge_cnt = [0] * n # 当前最长路用掉的边数,达到 n 说明 predecessor 链成环
in_queue = bytearray(n)
queue = deque(range(n)) # 所有点同时入队,等价于一个超级源点
for u in range(n):
in_queue[u] = 1
while queue:
u = queue.popleft()
in_queue[u] = 0
du = dist[u]
au = adj[u]
for v, a in au:
nd = du + a - offset
if nd > dist[v]:
dist[v] = nd
edge_cnt[v] = edge_cnt[u] + 1
if edge_cnt[v] >= n: # 链上有 n 条边 ⇒ 前驱链成环 ⇒ 严格正环
return True
if not in_queue[v]:
# SLF 优化:比队首大就插队首,正环能让 dist 更快抬升
if queue and nd > dist[queue[0]]:
queue.appendleft(v)
else:
queue.append(v)
in_queue[v] = 1
return False
def rounded_best_avg(n: int, adj: list[list[tuple[int, int]]]) -> int:
"""整数二分四舍五入后的答案 r:判定条件是存在平均值 ≥ r − 0.5 的环。"""
lo, hi = 1, 20001 # 平均值 ≤ 单条路线速度和上限 20000,hi 是取不到的上界
while lo < hi:
mid = (lo + hi) // 2
if has_cycle_avg_ge(n, adj, mid): # 判"平均值 ≥ mid − 0.5"
lo = mid + 1
else:
hi = mid
# lo 是第一个判定不成立的 r,最大可行值 = lo − 1;
# lo 仍为 1 说明 r=1(阈值 0.5)都不成立,图中没有任何环
return lo - 1 if lo > 1 else -1
def solve() -> None:
rows = sys.stdin.buffer.read().decode().replace("\r", "").split("\n")
m = int(rows[0])
n, adj = build_graph(rows[1 : m + 1])
print(rounded_best_avg(n, adj))
if __name__ == "__main__":
solve()复杂度
- 建图
,点数 、边数 。 - SPFA 判正环单次最坏
,二分约 轮,总复杂度 ——与 C++ 标准解法同阶。加了 SLF 优化后,本题实际数据上 Python 也能在限内跑完。 - 空间
。
总结
- 建模:行车路线 → 首尾车次之间的无向边(权 = 速度和),高铁环 → 图中的环,注意"环的值"的分母是路线条数,与一条路线里有几个车次无关。
- 算法:01 分数规划 + SPFA 判正环,
。 - 两个易错点:
- 四舍五入——不要对平均值做浮点二分,改为直接二分答案
、判“平均值 ”; 是整数,同乘 再加环长即可把 “ ”精确化为 SPFA 能判的“严格正环”,避开所有边界与精度问题。 - 无环输出
——把二分下界压到 ,让"判定成立的最大 "为 时恰好对应无环。
- 四舍五入——不要对平均值做浮点二分,改为直接二分答案
- Python 实现要点:SPFA 主循环里把
dist[u]、adj[u]提到局部变量,手动内层循环代替enumerate,这是 Python 判环能否过时限的关键。
图示解析
整体解法路线
flowchart LR
A["M 条行车路线"] --> B["缩点连边<br>每条路线 = 首尾车次间的无向边<br>权 = 速度和"]
B --> C["01 分数规划<br>二分四舍五入后的答案 r"]
C --> D["判定:边权 aₑ=(n+1)·2s+1 换成<br>aₑ−(n+1)(2r−1) 后有无严格正环<br>(精确等价于存在平均值 ≥ r−0.5 的环)"]
D -- "有" --> E["r 还能更大<br>r = r + 1"]
E --> D
D -- "无" --> F["r 就是答案<br>(r=1 说明无环,输出 −1)"]
二分判定在样例上的取值
以样例(三条路线构成环,平均
| 二分到的 |
判定阈值 |
边权 |
结论 |
|---|---|---|---|
| 否(边权全为 |
答案更小,往左 | ||
| 否 | 答案更小,往左 | ||
| 否 | 答案更小,往左 | ||
| 是(环均值 |
答案更大,往右 | ||
| 否( |
答案更小,往左 | ||
| 是( |
收敛 |
二分收敛到最大的可行