题解:洛谷 P6145 Timeline
【题目来源】
洛谷:P6145 [USACO20FEB] Timeline G - 洛谷
【题目描述】
Bessie 在过去的 \(M\) 天内参加了 \(N\) 次挤奶。但她已经忘了她每次挤奶是在哪个时候了。
对于第 \(i\) 次挤奶,Bessie 记得它不早于第 \(S_i\) 天进行。另外,她还有 \(C\) 条记忆,每条记忆形如一个三元组 \((a,b,x)\),含义是第 \(b\) 次挤奶在第 \(a\) 次挤奶结束至少 \(x\) 天后进行。
现在请你帮 Bessie 算出在满足所有条件的前提下,每次挤奶的最早日期。
保证 Bessie 的记忆没有错误,这意味着一定存在一种合法的方案,使得:
- 第 \(i\) 次挤奶不早于第 \(S_i\) 天进行,且不晚于第 \(M\) 天进行;
- 所有的记忆都得到满足;
【输入】
第一行三个整数 \(N,M,C\)。保证 \(1 \leq N,C \leq 10^5\),\(2 \leq M \leq 10^9\)。
接下来一行包含 \(N\) 个整数 \(S_1, S_2 , \ldots, S_n\),保证 \(\forall 1 \leq i \leq n\),都满足 \(1 \leq S_i \leq M\)。
下面 \(C\) 行每行三个整数 \(a,b,x\),描述一条记忆。保证 \(a \neq b\),且 \(1 \leq x \leq M\)。
【输出】
输出 \(N\) 行,每行一个整数,第 \(i\) 行的数表示第 \(i\) 次挤奶的最早日期。
【输入样例】
4 10 3
1 2 3 4
1 2 5
2 4 2
3 4 4
【输出样例】
1
6
3
8
【核心思想】
-
问题分析:给定 \(N\) 次挤奶,每次有最早日期约束 \(S_i\),以及 \(C\) 条记忆约束"第 \(b\) 次挤奶在第 \(a\) 次之后至少 \(x\) 天"。求每次挤奶的最早日期。本质是一个差分约束系统的最长路径问题——将不等式约束转化为图中的边,求满足所有约束的最小可行解。
-
算法选择:
- 超级源点 + SPFA求最长路:建立有向图,将约束转化为边,添加超级源点连接所有节点,用SPFA求从源点出发的最长路径,即为各点的最早日期
-
关键步骤:
- 建图:
- 对每个挤奶 \(i\),添加超级源点 \(0 \to i\) 的边,权值为 \(S_i\),表示 \(T_i \geq S_i\)
- 对每条记忆 \((a, b, x)\),添加有向边 \(a \to b\),权值为 \(x\),表示 \(T_b \geq T_a + x\)
- SPFA求最长路:
- 初始化 \(\text{dist}\) 为 \(-\infty\),源点 \(\text{dist}[0] = 0\)
- 松弛操作改为求最长路:若 \(\text{dist}[j] < \text{dist}[t] + w[i]\),则更新 \(\text{dist}[j]\)
- 队列维护待松弛节点,直到无更新
- 输出答案:\(\text{dist}[i]\) 即为第 \(i\) 次挤奶的最早日期
- 建图:
-
时间/空间复杂度:
- 时间复杂度:\(O((N + C) \cdot \alpha)\),SPFA平均接近线性,最坏 \(O(VE)\)
- 空间复杂度:\(O(N + C)\),存储图和距离数组
-
差分约束转最长路的核心思想:
- 不等式转边:将约束 \(T_b \geq T_a + x\) 转化为有向边 \(a \to b\)(权值 \(x\)),表示从 \(a\) 到 \(b\) 至少增加 \(x\)
- 超级源点统一初始化:添加虚拟源点 \(0\) 向所有节点连边(权值 \(S_i\)),将多个初始约束统一为单源最长路问题
- 最长路即最早时间:求最长路确保每个节点取满足所有入边约束的最小上界,即最早可行日期
- SPFA适用性:图规模 \(10^5\) 级别,SPFA在稀疏图上效率良好,且无需处理负权环(题目保证有解)
- 适用于时间调度、任务规划、不等式组求解类问题
【算法标签】
普及 #差分约束
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 100005, M = N * 2; // 最大顶点数和边数
int n, m, c; // n: 顶点数, m: 未使用, c: 有向边数量
int s[N]; // 每个顶点的权值
int h[N], e[M], w[M], ne[M], idx; // 链式前向星存储图
int cnt[N], dist[N]; // cnt未使用, dist: 最长距离数组
bool st[N]; // 标记顶点是否在队列中
/**
* 添加有向边
* @param a 起点
* @param b 终点
* @param c 权重
*/
void add(int a, int b, int c)
{
e[idx] = b; // 边指向的顶点
w[idx] = c; // 边的权重
ne[idx] = h[a]; // 指向原链表头
h[a] = idx++; // 更新头指针
}
/**
* SPFA算法求最长路径
* 从超级源点0开始,计算到所有顶点的最长路径
*/
void spfa()
{
// 初始化距离为负无穷
memset(dist, -0x3f, sizeof(dist));
queue<int> q; // SPFA队列
q.push(0); // 超级源点入队
st[0] = true; // 标记在队列中
dist[0] = 0; // 起点距离为0
while (!q.empty())
{
int t = q.front(); // 取出队首
q.pop();
st[t] = false; // 标记不在队列中
// 遍历t的所有邻接边
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i]; // 邻接顶点
// 松弛操作:求最长路径
if (dist[j] < dist[t] + w[i])
{
dist[j] = dist[t] + w[i]; // 更新最长距离
// 如果j不在队列中,入队
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
}
int main()
{
// 输入顶点数,m未使用,有向边数量c
cin >> n >> m >> c;
// 初始化邻接表
memset(h, -1, sizeof(h));
// 输入每个顶点的权值s[i]
for (int i = 1; i <= n; i++)
{
cin >> s[i];
// 添加超级源点到每个顶点的边
// 权重为s[i],表示从0出发可以直接获得s[i]的权值
add(0, i, s[i]);
}
// 输入c条有向边
for (int i = 1; i <= c; i++)
{
int a, b, x;
cin >> a >> b >> x;
add(a, b, x); // 添加有向边a→b,权重x
}
// 执行SPFA算法求最长路径
spfa();
// 输出从超级源点到每个顶点的最长路径长度
for (int i = 1; i <= n; i++)
{
cout << dist[i] << endl;
}
return 0;
}
【运行结果】
4 10 3
1 2 3 4
1 2 5
2 4 2
3 4 4
1
6
3
8
浙公网安备 33010602011771号