题解:洛谷 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

【核心思想】

  1. 问题分析:给定 \(N\) 次挤奶,每次有最早日期约束 \(S_i\),以及 \(C\) 条记忆约束"第 \(b\) 次挤奶在第 \(a\) 次之后至少 \(x\) 天"。求每次挤奶的最早日期。本质是一个差分约束系统的最长路径问题——将不等式约束转化为图中的边,求满足所有约束的最小可行解。

  2. 算法选择

    • 超级源点 + SPFA求最长路:建立有向图,将约束转化为边,添加超级源点连接所有节点,用SPFA求从源点出发的最长路径,即为各点的最早日期
  3. 关键步骤

    • 建图
      • 对每个挤奶 \(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\) 次挤奶的最早日期
  4. 时间/空间复杂度

    • 时间复杂度:\(O((N + C) \cdot \alpha)\),SPFA平均接近线性,最坏 \(O(VE)\)
    • 空间复杂度:\(O(N + C)\),存储图和距离数组
  5. 差分约束转最长路的核心思想

    • 不等式转边:将约束 \(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
posted @ 2026-08-29 09:39  团爸讲算法  阅读(9)  评论(0)    收藏  举报