在 DAG 上按拓扑序传播精确分数流量,每个点把当前污水均分给所有出边,最后统计所有汇点的分数结果。
OJ: luogu
题目 ID: P7113
难度:普及+/提高
标签:图论拓扑排序数学模拟
日期: 2026-06-19 23:32
题意
这题给了一张没有环的有向图。
- 前
m个点是污水接收口,每个点一开始各有1吨污水 - 每个点会把自己当前的污水平均分给所有出边
- 没有出边的点就是最终排水口
要求输出每个最终排水口最后流出的污水量,而且必须用最简分数表示。
样例图
这张图展示样例中的排水关系:
digraph G {
rankdir=LR;
1 -> 2;
1 -> 3;
1 -> 5;
2 -> 4;
2 -> 5;
3 -> 5;
3 -> 4;
}
1 号点先把 1 吨水均分成三份,分别流向 2,3,5。
其中流到 2 和 3 的水还会继续被均分一次,所以最后 4 收到 1/3,5 收到 2/3。
因为题目要求精确输出,所以这里不能用浮点数近似,必须直接维护分数。
思路
先看一个最直接的小数据暴力:
cpp
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 20;
struct Fraction {
long long num, den;
Fraction(long long numerator = 0, long long denominator = 1) {
num = numerator;
den = denominator;
normalize();
}
void normalize() {
if (den < 0) {
num = -num;
den = -den;
}
long long g = std::gcd(llabs(num), llabs(den));
if (g != 0) {
num /= g;
den /= g;
}
}
};
Fraction operator+(const Fraction &a, const Fraction &b) {
__int128 numerator = (__int128) a.num * b.den + (__int128) b.num * a.den;
__int128 denominator = (__int128) a.den * b.den;
return Fraction((long long) numerator, (long long) denominator);
}
Fraction operator/(const Fraction &a, long long x) {
return Fraction(a.num, a.den * x);
}
int n, m;
vector<int> graph[MAXN];
int outdeg[MAXN];
Fraction ans[MAXN]; // ans[i] : 最终流到汇点 i 的污水量
// 直接按题意递归分流。
void dfs(int u, Fraction cur) {
if (outdeg[u] == 0) {
ans[u] = ans[u] + cur;
return;
}
Fraction each = cur / outdeg[u];
for (int v : graph[u]) {
dfs(v, each);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
graph[i].clear();
ans[i] = Fraction(0, 1);
int d;
cin >> d;
outdeg[i] = d;
for (int j = 1; j <= d; j++) {
int v;
cin >> v;
graph[i].push_back(v);
}
}
for (int i = 1; i <= m; i++) {
dfs(i, Fraction(1, 1));
}
for (int i = 1; i <= n; i++) {
if (outdeg[i] == 0) {
cout << ans[i].num << ' ' << ans[i].den << '\n';
}
}
return 0;
}这个暴力完全按题意递归分流:
- 当前来到点
u,手里有cur吨污水 - 如果
u是汇点,就把cur加到答案里 - 否则把
cur平均分成outdeg[u]份,递归流向每个后继
这个写法很直观,但如果图很大、路径很多,就会重复遍历很多公共后缀。
题目已经保证整张图是 DAG,所以更高效的办法是按拓扑序传播流量。
做法如下:
- 用分数
num / den精确保存每个点当前累计到的污水量。 - 前
m个接收口初始都设成1/1。 - 求整张图的拓扑序。
- 按拓扑序处理每个点
u:- 如果
u不是汇点,就把water[u]均分成outdeg[u]份 - 每个后继
v都加上这一份
- 如果
- 最后所有出度为
0的点就是答案。
这里用到的关键实现是一个简单分数类:
- 加法时通分再约分
- 除以整数时直接把分母乘上这个整数
代码结构上和你算法书里的拓扑模板保持一致:先建图和统计入度,再用队列按拓扑序顺推。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
struct Fraction {
long long num, den;
Fraction(long long numerator = 0, long long denominator = 1) {
num = numerator;
den = denominator;
normalize();
}
void normalize() {
if (den < 0) {
num = -num;
den = -den;
}
long long g = std::gcd(llabs(num), llabs(den));
if (g != 0) {
num /= g;
den /= g;
}
}
};
Fraction operator+(const Fraction &a, const Fraction &b) {
__int128 numerator = (__int128) a.num * b.den + (__int128) b.num * a.den;
__int128 denominator = (__int128) a.den * b.den;
return Fraction((long long) numerator, (long long) denominator);
}
Fraction operator/(const Fraction &a, long long x) {
return Fraction(a.num, a.den * x);
}
int n, m;
vector<int> graph[MAXN];
int indeg[MAXN];
int outdeg[MAXN];
Fraction water[MAXN]; // water[i] : 当前流到结点 i 的总污水量
void read_input() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
graph[i].clear();
indeg[i] = 0;
outdeg[i] = 0;
water[i] = Fraction(0, 1);
}
for (int i = 1; i <= n; i++) {
int d;
cin >> d;
outdeg[i] = d;
for (int j = 1; j <= d; j++) {
int v;
cin >> v;
graph[i].push_back(v);
indeg[v]++;
}
}
for (int i = 1; i <= m; i++) {
water[i] = Fraction(1, 1);
}
}
void solve() {
queue<int> q;
int deg[MAXN];
memcpy(deg, indeg, sizeof(indeg));
for (int i = 1; i <= n; i++) {
if (deg[i] == 0) {
q.push(i);
}
}
while (!q.empty()) {
int u = q.front();
q.pop();
if (outdeg[u] > 0) {
Fraction each = water[u] / outdeg[u];
for (int v : graph[u]) {
water[v] = water[v] + each;
deg[v]--;
if (deg[v] == 0) {
q.push(v);
}
}
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
for (int i = 1; i <= n; i++) {
if (outdeg[i] == 0) {
cout << water[i].num << ' ' << water[i].den << '\n';
}
}
return 0;
}复杂度
设点数为 n,边数为 E。
- 拓扑排序扫描每个点、每条边各一次,复杂度
- 每次分数运算都只做常数次
gcd
总时间复杂度
总结
这题本质是 DAG 上的流量传播。识别出“无环 + 每条边均分”以后,直接按拓扑序把分数往后推即可。真正容易错的点只有一个:一定要用精确分数,不能用 double。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
