题解:AcWing 368 银河

【题目来源】

AcWing:368. 银河 - AcWing题库

【题目描述】

银河中的恒星浩如烟海,但是我们只关注那些最亮的恒星。

我们用一个正整数来表示恒星的亮度,数值越大则恒星就越亮,恒星的亮度最暗是 \(1\)

现在对于 \(N\) 颗我们关注的恒星,有 \(M\) 对亮度之间的相对关系已经判明。

你的任务就是求出这 \(N\) 颗恒星的亮度值总和至少有多大。
【输入】

第一行给出两个整数 \(N\) 和 \(M\)

之后 \(M\) 行,每行三个整数 \(T,A,B\),表示一对恒星 \((A,B)\) 之间的亮度关系。恒星的编号从 \(1\) 开始。

如果 \(T=1\),说明 \(A\) 和 \(B\) 亮度相等。
如果 \(T=2\),说明 \(A\) 的亮度小于 \(B\) 的亮度。
如果 \(T=3\),说明 \(A\) 的亮度不小于 \(B\) 的亮度。
如果 \(T=4\),说明 \(A\) 的亮度大于 \(B\) 的亮度。
如果 \(T=5\),说明 \(A\) 的亮度不大于 \(B\) 的亮度。

【输出】

输出一个整数表示结果。

若无解,则输出 \(−1\)

【输入样例】

5 7
1 1 2
2 3 2
4 4 1
3 4 5
5 4 5
2 3 5
4 5 1

【输出样例】

11

【核心思想】

  1. 问题分析:给定 \(N\) 颗恒星的 \(M\) 对亮度关系约束,求满足所有约束的亮度总和最小值。每种约束可以转化为差分约束形式 \(x_v \geq x_u + w\),即 \(u\)\(v\) 有一条权值为 \(w\) 的有向边。这是一个差分约束系统问题,需要判断是否有解(是否存在正环)并求最小解。

  2. 算法选择

    • 差分约束建模:将5种约束类型转化为带权有向边
    • Tarjan算法求SCC:检测强连通分量内是否存在正权边(正环)
    • 缩点建DAG:将SCC缩点后构建有向无环图
    • DAG上最长路DP:在DAG上求最长路得到各变量的最小值
  3. 关键步骤

    • Step 1 - 差分约束建模

      • 建立超级源点 \(0\),向所有节点连权值为 \(1\) 的边(保证亮度至少为 \(1\)
      • \(T=1\)\(A=B\)):\(A \geq B + 0\)\(B \geq A + 0\)
      • \(T=2\)\(A < B\)):\(B \geq A + 1\)
      • \(T=3\)\(A \geq B\)):\(A \geq B + 0\)
      • \(T=4\)\(A > B\)):\(A \geq B + 1\)
      • \(T=5\)\(A \leq B\)):\(B \geq A + 0\)
    • Step 2 - Tarjan求SCC并检测正环

      • 使用Tarjan算法求强连通分量
      • 检查每个SCC内部是否存在正权边(权值 \(> 0\)
      • 若存在正权边,则存在正环,问题无解,输出 \(-1\)
    • Step 3 - 缩点建DAG

      • 将每个SCC缩成一个点
      • 保留SCC之间的边,构建DAG
    • Step 4 - DAG上最长路DP

      • 状态定义:\(dist[i]\) 表示从超级源点到SCC \(i\) 的最长距离(即该SCC内变量的最小值)
      • 按SCC编号逆序(拓扑序)进行DP
      • 状态转移:\(dist[k] = \max(dist[k], dist[i] + w)\)
    • Step 5 - 计算答案

      • 答案为所有SCC的贡献之和:\(\sum_{i=1}^{cnt} dist[i] \times siz[i]\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N + M)\),Tarjan算法 \(O(N+M)\),缩点和DP均为 \(O(N+M)\)
    • 空间复杂度:\(O(N + M)\),存储原图、DAG、以及各种辅助数组
  5. 差分约束与强连通分量的核心思想

    • 差分约束建模:将不等式约束转化为图论中的最短/最长路问题,\(x_v \geq x_u + w\) 对应边 \(u \to v\) 权值为 \(w\)
    • 正环检测:差分约束系统有解的充要条件是不存在正环(求最小解时),使用SCC检测正权边
    • 缩点简化:将互相可达的变量缩成一个SCC,SCC内所有变量值相同
    • 最长路求最小解:在DAG上求最长路,得到满足所有约束的最小赋值
    • 超级源点技巧:引入虚拟源点保证所有变量有下界(亮度至少为1)
    • 适用于不等式约束系统、变量关系推导、最值求解类问题

【算法标签】

强连通分量

【代码详解】

// 该题目测试数据与 1169 糖果 这题一样

#include <bits/stdc++.h>
using namespace std;

// 定义长整型别名,便于处理大数据
#define int long long

// 定义pair类型别名,用于存储边权对
typedef pair<int, int> PII;

// 定义数组最大容量
const int N = 100005;

// 全局变量声明
int n;                      // 变量个数
int m;                      // 约束条件个数

vector<PII> e[N];           // 原图邻接表,存储有向边及其权值
vector<PII> e2[N];          // 缩点后的 DAG 邻接表

// ---------- Tarjan 相关 ----------
int dfn[N];                 // 时间戳:节点首次被访问的顺序
int low[N];                 // 能追溯到的最早栈中节点时间戳
int tot;                    // 时间戳计数器

int stk[N];                 // 手写栈,存储当前 DFS 路径上的节点
int instk[N];               // 标记节点是否在栈中
int top;                    // 栈顶指针

// ---------- 强连通分量(SCC)相关 ----------
int scc[N];                 // 每个节点所属的强连通分量编号
int cnt;                    // 强连通分量的总数
int siz[N];                 // 每个强连通分量包含的节点数

// ---------- 最长路 DP 相关 ----------
int dist[N];                // dist[i]: 从超级源点到 SCC i 的最长距离

// ================= Tarjan 算法 =================
void tarjan(int x)
{
    dfn[x] = low[x] = ++tot;    // 初始化时间戳
    stk[++top] = x;             // 当前节点入栈
    instk[x] = 1;               // 标记为在栈中

    // 遍历 x 的所有出边
    for (auto y : e[x])
    {
        if (!dfn[y.first])      // y 尚未访问
        {
            tarjan(y.first);    // 递归处理
            low[x] = min(low[x], low[y.first]);   // 用子树更新 low
        }
        else if (instk[y.first]) // 发现返祖边
        {
            low[x] = min(low[x], dfn[y.first]);   // 用回边更新 low
        }
    }

    // 若 x 是强连通分量的根节点
    if (dfn[x] == low[x])
    {
        int y;
        ++cnt;                  // 新建一个强连通分量
        do
        {
            y = stk[top--];     // 弹出栈顶节点
            instk[y] = 0;       // 取消栈中标记
            scc[y] = cnt;       // 将节点归属到当前 SCC
            siz[cnt]++;         // 当前 SCC 节点数加1
        }
        while (y != x);         // 直到弹出 x 为止
    }
}

// ================= 主函数 =================
signed main()
{
    // 读取变量个数和约束条件个数
    cin >> n >> m;

    // 建立超级源点 0,向所有变量连一条权值为 1 的边
    // 意义:所有变量的最小值至少为 1
    for (int i = 1; i <= n; i++)
        e[0].push_back({i, 1});

    // 读入 m 个约束条件
    while (m--)
    {
        int t, u, v, w;     // t: 约束类型, u,v: 变量编号
        cin >> t >> u >> v;

        if (t == 1)         // u == v
        {
            e[v].push_back({u, 0});     // v >= u + 0
            e[u].push_back({v, 0});     // u >= v + 0
        }
        else if (t == 2)    // u < v
        {
            e[u].push_back({v, 1});     // v >= u + 1
        }
        else if (t == 3)    // u >= v
        {
            e[v].push_back({u, 0});     // u >= v + 0
        }
        else if (t == 4)    // u > v
        {
            e[v].push_back({u, 1});     // u >= v + 1
        }
        else                // t == 5, u <= v
        {
            e[u].push_back({v, 0});     // v >= u + 0
        }
    }

    // 对每个未访问的节点执行 Tarjan 算法
    for (int i = 0; i <= n; i++)
        if (!dfn[i])
            tarjan(i);

    // ========= 检查 SCC 内是否有正权边 =========
    bool success = true;
    for (int i = 0; i <= n; i++)
    {
        for (auto j : e[i])
        {
            int k = j.first;            // 目标节点
            int a = scc[i];             // i 所属 SCC
            int b = scc[k];             // k 所属 SCC

            if (a == b)                 // 在同一 SCC 中
            {
                if (j.second > 0)       // 如果有正权边,说明存在正环
                {
                    success = false;    // 无解
                    break;
                }
            }
            else                        // 在不同 SCC 中,加入 DAG
            {
                e2[a].push_back({b, j.second});
            }
        }
        if (!success)
            break;
    }

    // ========= 输出结果 =========
    if (!success)
    {
        cout << -1 << endl;             // 存在正环,无解
    }
    else
    {
        // 在 DAG 上求最长路(逆序 DP)
        for (int i = cnt; i >= 0; i--)  // 从大到小遍历 SCC 编号
            for (auto j : e2[i])
            {
                int k = j.first;        // 后继 SCC 编号
                dist[k] = max(dist[k], dist[i] + j.second); // 最长路转移
            }

        // 计算最终答案:每个 SCC 的最长距离乘以该 SCC 的大小
        int res = 0;
        for (int i = 1; i <= cnt; i++)
            res += dist[i] * siz[i];

        cout << res << endl;
    }

    return 0;
}

【运行结果】

5 7
1 1 2
2 3 2
4 4 1
3 4 5
5 4 5
2 3 5
4 5 1
11
posted @ 2026-06-27 14:21  团爸讲算法  阅读(9)  评论(0)    收藏  举报