把选点问题改写为前缀计数的差分约束系统,三类不等式建图后用 SPFA 求最长路,逐分量最小可行解的终点值即最少选点数。
OJ: roj
题目 ID: 1509
难度:普及+/提高-
标签:图论最短路差分约束SPFA
创建: 2026-09-30 15:17
更新: 2026-10-07 11:01
形式化题目
给定
求
样例示意
下表用题面样例检验候选集合
| 区间 | 要求 | 个数 | |
|---|---|---|---|
正解
思路
朴素做法为什么不行。 最直接的思路是枚举点集:
第一步:把"选点"翻译成前缀计数。 定义
集合
- 区间要求:
,即 ; - 前缀不减:
; - 每个整数至多选一次:
( 边与 边夹逼出增量恰好 )。
而
第二步:识别为差分约束系统。 三类不等式全部形如
| 题目中的事实 | 关于前缀计数的不等式 | 图上的边 |
|---|---|---|
| 区间 |
||
| 前缀不减 | ||
| 每个整数至多选一次 |
读表时注意三条边各自的角色:链上一右一左两条边把每个位置的增量夹在
下图把样例的前缀链和两类边画出来(编号时整体
graph LR
m1["S(-1)"] -->|"权 0"| s0["S(0)"]
s0 -->|"权 0"| s1["S(1)"]
s1 -->|"权 0"| s2["S(2)"]
s2 -->|"权 0"| s3["S(3)"]
s3 -.->|"权 -1"| s2
s0 -.->|"区间 1..3 权 1"| s3
链上每对相邻点之间都同时存在“向右权
第三步:为什么"最小选点数"等于最长路。 系统里全是 dist 全初始化为
可行:最长路按定义满足每条边 ,三类不等式逐一成立; 逐分量最小:任取可行解 ,对从超级源点出发的任意路径逐边累加 (起点处 ),可知 不小于所有路径的权和,取最大值得 —— 被最长路从下面托住。
即最长路是"被所有可行解压在下面、自己又满足全部约束"的解,它就是分量最小的可行解,其终点值
第四步:为什么算得完(无正环)。 最长路图里若有权为正的环,距离会无限抬升。本题不会:环必须向右走(区间边、
以样例验证:
实现上只建点 du,保证用的是该点的最新距离;被抬升且不在队中的点重新入队。
代码
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:40
# update_at: 2026-09-30 15:40
import sys
from collections import deque
def spfa_longest(adj: list[list[tuple[int, int]]]) -> list[int]:
"""差分约束的最长路:dist[v] 是满足 dist[v] >= dist[u] + w 的最小可行解。
所有 dist 初值 0 且全节点一起入队,等价于超级源点向每个点连 0 权边,
一轮即可从所有点出发;c_i <= b_i - a_i + 1 保证图中无正环,队列必然收敛。
"""
dist = [0] * len(adj)
in_queue = [True] * len(adj)
queue = deque(range(len(adj)))
while queue:
u = queue.popleft()
in_queue[u] = False
du = dist[u] # 弹出时才读,保证用的是 u 的最新距离
for v, w in adj[u]:
if du + w > dist[v]:
dist[v] = du + w
if not in_queue[v]:
in_queue[v] = True
queue.append(v)
return dist
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
intervals = [(next(data), next(data), next(data)) for _ in range(n)]
hi = max(b for _, b, _ in intervals) # 只建到最右端点:更右的位置选了也不贡献任何区间
# 编号 = 坐标 + 1,让坐标 -1 也有节点:S(-1) 是全 0 的前缀基准
adj: list[list[tuple[int, int]]] = [[] for _ in range(hi + 2)]
for a, b, c in intervals:
adj[a].append((b + 1, c)) # 节点 a 即坐标 a-1:S(b) >= S(a-1) + c
for v in range(1, hi + 2):
adj[v - 1].append((v, 0)) # S(坐标 v) >= S(坐标 v-1):前缀不减
adj[v].append((v - 1, -1)) # S(坐标 v-1) >= S(坐标 v) - 1:每个整数至多选一个
dist = spfa_longest(adj)
print(dist[hi + 1] - dist[0]) # S(max_b) - S(-1) = [0, max_b] 里被选中的整数个数
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-04 22:40
* update_at: 2026-10-05 05:28
*/
// 差分约束 + SPFA 最长路:
// 设 S(x) = Z 中落在 [0, x] 的整数个数,则
// 1. 区间要求:S(b_i) >= S(a_i - 1) + c_i
// 2. 前缀不减:S(x) >= S(x - 1)
// 3. 每个整数至多选一次:S(x - 1) >= S(x) - 1
// 三类不等式全是 >= 形式,各建一条边后求最长路,即得逐分量最小的可行解。
// |Z| 的最小值 = S(max b) - S(-1)。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 50005; // 节点数:坐标 -1 ~ 50000,编号 = 坐标 + 1
const int MAXM = 200005; // 边数:n 条区间边 + 每对相邻点两条链边
typedef long long ll;
// 链式前向星存图
int head[MAXN], to[MAXM], nxt[MAXM], edge_cnt;
int weight[MAXM]; // 边权:区间边为 c_i,链边为 0 或 -1
int n; // 区间个数
int max_b; // 最右端点,编号上界为 max_b + 1
int dist[MAXN]; // 最长路距离,即分量最小的可行前缀计数
bool in_queue[MAXN]; // 该节点当前是否在 SPFA 队列中
// 加一条 u -> v、权 w 的边。
void add_edge(int u, int v, int w) {
edge_cnt++;
to[edge_cnt] = v;
weight[edge_cnt] = w;
nxt[edge_cnt] = head[u];
head[u] = edge_cnt;
}
// SPFA 求最长路:dist 全部初始化为 0 并全部入队,等价于超级源点连 0 权边。
// 题目保证 c_i <= b_i - a_i + 1,图中无正环,队列必然收敛。
void spfa_longest() {
queue<int> q;
for (int i = 0; i <= max_b + 1; i++) {
dist[i] = 0;
in_queue[i] = true;
q.push(i);
}
while (!q.empty()) {
int u = q.front();
q.pop();
in_queue[u] = false;
int du = dist[u]; // 弹出时才读取,保证用的是 u 的最新距离
for (int i = head[u]; i != 0; i = nxt[i]) {
int v = to[i];
if (du + weight[i] > dist[v]) {
dist[v] = du + weight[i];
if (!in_queue[v]) {
in_queue[v] = true;
q.push(v);
}
}
}
}
}
void solve() {
scanf("%d", &n);
max_b = 0;
for (int i = 1; i <= n; i++) {
int a, b, c;
scanf("%d %d %d", &a, &b, &c);
// 区间边:a_i - 1 -> b_i 权 c_i;编号 = 坐标 + 1,免去 a_i = 0 的特判
add_edge(a, b + 1, c);
if (b > max_b) max_b = b;
}
// 相邻点之间的两条链边,把每个前缀的增量夹在 {0, 1} 里
for (int v = 1; v <= max_b + 1; v++) {
add_edge(v - 1, v, 0); // S(坐标 v) >= S(坐标 v - 1)
add_edge(v, v - 1, -1); // S(坐标 v - 1) >= S(坐标 v) - 1
}
spfa_longest();
// 最少选点数 = S(max b) - S(-1);比最右端点更右的数不属于任何区间,不必考虑
printf("%d\n", dist[max_b + 1] - dist[0]);
}
int main() {
solve();
return 0;
}复杂度
- 时间复杂度:建图
,SPFA 理论最坏 ,本题无正环且结构是"链 + 跨段边",实践中每个点只需少数几轮抬升,接近 。规模 、 ,与 C++ 正解(差分约束 + SPFA/Bellman-Ford)同阶。实测 6 组真实数据各 s;本地构造最大规模( 、坐标 随机两组) s / s,OJ 时限 ms 下 Python 无 TLE 风险。 - 空间复杂度:
,邻接表约 个元组, dist、in_queue各项;实测峰值 MB,远低于 MB 限制。
总结
- 构造问题改写成前缀计数:
,选点的构造被完全吸收进 的形状,"至少 个"变成 。 - 三类事实 = 三类边:区间要求是跨段权
的边,前缀不减是向右权 的边,"至多选一次"是向左权 的边;全是 不等式 → 差分约束系统。 - 最小选点数 = 最长路终点:最长路既是可行解又逐分量最小,
;实现上全 初始化并全体入队,等价于超级源点。 - 收敛性靠
上界:环权 ,无正环,SPFA 必然终止——数据范围里的 不只是"保证有解",也是算法不跑飞的前提。 - 识别模式:见到"一批
/ 约束 + 在所有可行解里求极值",优先建差分约束图;求最大配最短路,求最小配最长路。