题解: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
【核心思想】
-
问题分析:给定 \(N\) 颗恒星的 \(M\) 对亮度关系约束,求满足所有约束的亮度总和最小值。每种约束可以转化为差分约束形式 \(x_v \geq x_u + w\),即 \(u\) 到 \(v\) 有一条权值为 \(w\) 的有向边。这是一个差分约束系统问题,需要判断是否有解(是否存在正环)并求最小解。
-
算法选择:
- 差分约束建模:将5种约束类型转化为带权有向边
- Tarjan算法求SCC:检测强连通分量内是否存在正权边(正环)
- 缩点建DAG:将SCC缩点后构建有向无环图
- DAG上最长路DP:在DAG上求最长路得到各变量的最小值
-
关键步骤:
-
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]\)
-
-
时间/空间复杂度:
- 时间复杂度:\(O(N + M)\),Tarjan算法 \(O(N+M)\),缩点和DP均为 \(O(N+M)\)
- 空间复杂度:\(O(N + M)\),存储原图、DAG、以及各种辅助数组
-
差分约束与强连通分量的核心思想:
- 差分约束建模:将不等式约束转化为图论中的最短/最长路问题,\(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
浙公网安备 33010602011771号