将牛的相对顺序、好感距离上限与反感距离下限转化为差分约束系统,通过两阶段 SPFA 分别判断全局负环与求 1 到 N 的最短路。
OJ: roj
题目 ID: 1512
难度:提高
标签:图论差分约束最短路SPFA
创建: 2026-09-30 15:31
更新: 2026-10-07 11:01
形式化题目
给定
- 顺序约束:对于所有
,满足 ; - 上限约束:给出
条限制,每条指定两个变量 (不妨设 ),满足 ; - 下限约束:给出
条限制,每条指定两个变量 (不妨设 ),满足 。
求:
- 若该不等式系统无可行解,输出
-1; - 若在满足所有约束的情况下,
可以任意大,输出 -2; - 否则输出
的最大可能值。
正解
思路
本题求变量差值的最大值,且所有约束均为形如
1. 建图转换
在单源最短路中,三角形不等式为:
变形后即为:
因此,每个不等式
- 顺序约束:
,从节点 向节点 连一条边权为 的边。 - 好感限制:
(保证 ),从节点 向节点 连一条边权为 的边。 - 反感限制:
(保证 ),从节点 向节点 连一条边权为 的边。
2. 两阶段 SPFA 判定与求解
因为图可能存在多个不连通的分量,且
-
第一阶段(全局负环检测): 若整个不等式组中存在相互矛盾(即图中存在负权环),则无解。 为了检测整张图中是否存在负权环,引入超级源点(或初始化时将所有节点
的初始距离设为 并全部推入队列),运行一次 SPFA。若有任意节点入队次数达到 ,说明存在负权环,直接输出 -1。 -
第二阶段(求最大距离): 在排除负环后,以
号节点为单源起点(令 ,其余为 )运行 SPFA。 - 若
,说明从 出发无法到达 , 不受任何上界路径约束,可以任意大,输出 -2; - 否则,
就是在满足所有约束条件下 的最大值,输出 。
- 若
代码
Python 版:
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:31
# update_at: 2026-09-30 15:31
import sys
from collections import deque
INF = 10**18
def spfa(n: int, adj: list[list[tuple[int, int]]], source: int | None) -> tuple[bool, list[int]]:
"""用 SPFA 求从源点出发的最短路并检测负环。
若 source 为 None,则初始将所有点入队(等价于建立超级源点向各点连边权 0 的边),
用于全局判负环。
返回 (has_negative_cycle, dist)。
"""
dist = [0 if source is None else INF] * (n + 1)
in_queue = [True if source is None else False] * (n + 1)
count = [0] * (n + 1)
queue = deque(range(1, n + 1) if source is None else [source])
if source is not None:
dist[source] = 0
in_queue[source] = True
while queue:
u = queue.popleft()
in_queue[u] = False
d_u = dist[u]
for v, w in adj[u]:
cand = d_u + w
if cand < dist[v]:
dist[v] = cand
if not in_queue[v]:
count[v] += 1
if count[v] >= n:
return True, dist
in_queue[v] = True
queue.append(v)
return False, dist
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
tokens = list(data)
if not tokens:
return
it = iter(tokens)
n, ml, md = next(it), next(it), next(it)
# 建图:adj[u] 存储出边 (v, w),表示不等式 dist[v] <= dist[u] + w
adj: list[list[tuple[int, int]]] = [[] for _ in range(n + 1)]
# 1. 相对顺序限制:x[i] <= x[i+1] => x[i] - x[i+1] <= 0 => 从 i+1 到 i 连边权 0
for i in range(1, n):
adj[i + 1].append((i, 0))
# 2. 友好关系:x[b] - x[a] <= d (a < b) => 从 a 到 b 连边权 d
for _ in range(ml):
a, b, d = next(it), next(it), next(it)
if a > b:
a, b = b, a
adj[a].append((b, d))
# 3. 反感关系:x[b] - x[a] >= d (a < b) => x[a] - x[b] <= -d => 从 b 到 a 连边权 -d
for _ in range(md):
a, b, d = next(it), next(it), next(it)
if a > b:
a, b = b, a
adj[b].append((a, -d))
# 第一阶段:全局负环检测(无解判定)
has_neg_cycle, _ = spfa(n, adj, None)
if has_neg_cycle:
print(-1)
return
# 第二阶段:以 1 号牛为起点求单源最短路
_, dist = spfa(n, adj, 1)
if dist[n] == INF:
print(-2)
else:
print(dist[n])
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-10-05 05:37
* update_at: 2026-10-05 05:37
*/
// 排队布局:差分约束 + SPFA
// 设 x[i] 为第 i 头牛的坐标,三类约束统一写成 x[v] - x[u] <= w,从 u 向 v 连一条权为 w 的有向边:
// 顺序约束:x[i] <= x[i+1],即 x[i] - x[i+1] <= 0,从 i+1 向 i 连权 0;
// 好感约束:x[b] - x[a] <= d,从 a 向 b 连权 d;
// 反感约束:x[b] - x[a] >= d,即 x[a] - x[b] <= -d,从 b 向 a 连权 -d。
// 先让所有点初值为 0 全部入队跑一次 SPFA 判全局负环:有负环则无解,输出 -1;
// 再以 1 号牛为源跑最短路:到不了 n 号牛说明距离可任意大,输出 -2,否则 dist[n] 就是最大距离。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 1005; // 节点数上限 N <= 1000
const int MAXM = 30005; // 边数上限 (N-1) + M_L + M_D <= 20999
const ll INF = 1000000000000000000LL; // 无穷大,不用 LLONG_MAX 以免加法溢出
// 链式前向星存图,边的含义是 x[to] - x[u] <= weight
int head[MAXN], to[MAXM], nxt[MAXM], edge_cnt;
ll weight[MAXM]; // 边权可能为负,必须用 ll
ll dist[MAXN]; // 单源最短路距离
bool in_queue[MAXN]; // 节点当前是否在队列中
int push_cnt[MAXN]; // 节点因松弛而入队的次数,达到 n 次说明存在负环
int n, ml, md;
// 加一条 u -> v、权 w 的有向边。
void add_edge(int u, int v, ll w) {
edge_cnt++;
to[edge_cnt] = v;
weight[edge_cnt] = w;
nxt[edge_cnt] = head[u];
head[u] = edge_cnt;
}
// SPFA:source == 0 表示全局判负环(所有点初值 0 并一起入队,等价于建超级源点),
// 否则以 source 为源点求最短路。返回 true 表示图中存在负环。
bool spfa(int source) {
queue<int> q;
for (int i = 1; i <= n; i++) {
in_queue[i] = false;
push_cnt[i] = 0;
if (source == 0) {
dist[i] = 0; // 全局判环时所有点都是源点
in_queue[i] = true;
q.push(i);
} else {
dist[i] = INF;
}
}
if (source != 0) {
dist[source] = 0;
in_queue[source] = true;
q.push(source);
}
while (!q.empty()) {
int u = q.front();
q.pop();
in_queue[u] = false;
for (int e = head[u]; e != 0; e = nxt[e]) {
int v = to[e];
if (dist[u] + weight[e] < dist[v]) {
dist[v] = dist[u] + weight[e];
if (!in_queue[v]) {
push_cnt[v]++;
if (push_cnt[v] >= n) return true; // 同一节点入队 n 次,必有负环
in_queue[v] = true;
q.push(v);
}
}
}
}
return false;
}
int main() {
scanf("%d%d%d", &n, &ml, &md);
for (int i = 1; i < n; i++) {
add_edge(i + 1, i, 0);
}
for (int i = 0; i < ml; i++) {
int a, b;
ll d;
scanf("%d%d%lld", &a, &b, &d);
if (a > b) swap(a, b);
add_edge(a, b, d);
}
for (int i = 0; i < md; i++) {
int a, b;
ll d;
scanf("%d%d%lld", &a, &b, &d);
if (a > b) swap(a, b);
add_edge(b, a, -d);
}
if (spfa(0)) {
printf("-1\n");
return 0;
}
spfa(1);
if (dist[n] == INF) {
printf("-2\n");
} else {
printf("%lld\n", dist[n]);
}
return 0;
}复杂度
- 时间复杂度:图中有
个节点, 条边。SPFA 的平均运行时间为 ( 为常数),最坏情况下为 。在本题数据规模下运行耗时通常在 20ms 以内,远低于时限要求。 - 空间复杂度:存储邻接表、距离数组和队列状态需要
的额外空间,内存开销在数兆字节级别。
总结
差分约束的核心在于统一不等式方向:
- 求变量差的最大值时,统一转化为
的形式建图求最短路; - 判断系统整体可行性时,必须做全局负环检测(全点初始入队);在系统有解的前提下,再以指定点为单源起点求最短路。
图示解析
以样例输入为例:
- 好感约束:
, - 反感约束:
- 顺序约束:
, ,
建图后各节点之间的边权关系如下图所示:
graph LR
1((1))
2((2))
3((3))
4((4))
1 -- "10" --> 3
2 -- "20" --> 4
3 -- "-3" --> 2
2 -- "0" --> 1
3 -- "0" --> 2
4 -- "0" --> 3
计算最短路时:
- 从
到 最短路径为 ,距离为 ; - 从
到 边权为 ,故 到 的最短路为 ; - 从
到 边权为 ,故 到 的最短路为 。 - 因此
的最大可能值为 。