来源
P7113 [NOIP2020] 排水系统 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题意
一个有向无环图有n个结点,其中有m个入口分别都接收1吨污水。
污水进入每个结点后,会均等地从当前结点的每一个排出管道流向其他排水结点,而最终排水口将把污水排出系统。
没有排出管道的结点便可视为一个最终排水口。
求:在该城市的排水系统中,每个最终排水口会排出多少污水。
输入
5 1【n m】【接下来 n 行,第 i 行用于描述结点 i 的所有排出管道。】
3 2 3 5【结点1有3个排出管道,分别是2 3 5】
2 4 5【结点2有2个排出管道,分别是4 5】
2 5 4【结点3有2个排出管道,分别是5 4】
0【结点4为一个最终排水口】
0【结点5为一个最终排水口】
输出要求
按照编号从小到大的顺序,给出每个最终排水口排出的污水体积。其中体积使用分数形式进行输出,即每行输出两个用单个空格分隔的整数 p,q,表示排出的污水体积为 p / q 。
要求 p与 q 互素,q = 1 时也需要输出 q。
输出
1 3
2 3
注释版代码

#include<iostream> #include<cstdio> #include<cmath> #include<algorithm> #include<queue> #include<stack> #include<vector> #include<map> #define ll long long using namespace std; inline ll read(){ ll x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x*f; } int n,m,rushuiguanshu[100001],final[100001],over[100001]; /* 1. rushuiguanshu[i]:存储i结点有几个入水管。 对应至拓扑排序的定义则是:在整个工程中,作为当前工程的先决条件的工程是否已经全部完成。如果全部完成,则rushuiguanshu[i]=0;否则rushuiguanshu[i]大于0,代表i之前尚未完成的工程数。 2. final[i]:标记最终排出口。若结点i是图的最终排出口,则final[i]=1。 3. over[i]: 如果i入水管都被遍历过了,即从别处进入i的水量都得到了累加,则over[i]标记为1。即等效于rushuiguanshu[i]=0。(其实可以删掉这个) 对应至拓扑排序的定义则是:在整个工程中,有些子工程(活动)必须在其它有关子工程完成之后才能开始。如果over[i]=1,则表示可以开始在i的拓扑排序后续的工程,即以i为先决条件的工程已经具备了i这个条件。 ps:rushuiguanshu[i]=0等价于 over[i]=1, over[i]=0等价于 rushuiguanshu[i]>0 */ ll fenzi[100001],fenmu[100001]; ll gcd(ll x,ll y){ if(y==0) return x; return gcd(y,x%y); } void add(int i,ll x,ll y){ if(y==0) return; if(fenmu[i]==0){ fenzi[i]=x; fenmu[i]=y; return; } ll p1=fenzi[i]*y+fenmu[i]*x; ll p2=fenmu[i]*y;//通分分母 ll p3=gcd(p1,p2);//找到最大公约数 fenzi[i]=p1/p3;//分子除以最大公约数。得到约分后的分子 fenmu[i]=p2/p3;//分母除以最大公约数。得到约分后的分母 return; } vector<int> a[500001]; queue<int> q; void tp(){ for(int i=1;i<=n;i++) { if(!rushuiguanshu[i]){//找到没有入水管的结点,即图的总入口 over[i]=1;//没有入水口就是没有先决条件,则可以开始完成i的后续工程,则标记为1 q.push(i);//从污水总入口开始拓扑,所以将总入口入队 fenzi[i]=1,fenmu[i]=1;//在数组中初始化每个总入口的水流量,即水流量为1吨 } } /* 父节点入队后,以bfs序将水汇入子结点,即等价于拓扑排序所定义的对于子节点标记当前父节点的这 一先决条件已完成的功能 。所有父节点都被标记,即已经没有需要入水的父节点,则子节点的先决条 件都已完成,可以开始以该子节点为父节点的后续子孙节点的工程,而首先就是将该子节点入队,作 为父节点,进行对下一轮子节点的标记 */ while(!q.empty()){//这里不是循环,而是出队。每一次出队以p为前驱结点,进行下一层的bfs int p=q.front(); q.pop(); if(final[p])//最终排出口不会对某结点的流入造成影响,因为最终排出口没有出度。所以任何结点都不会有来自最终排出口的入度,所以也不用进行后续的分流等工作。 continue; for(int i=0;i<a[p].size();i++){//对于结点p进行bfs,即为p的每个后继节点加入分流 int cur=a[p][i];//cur就是从队列中取出当前分流将要汇入的结点a[p][i]的意思 add(cur,fenzi[p],fenmu[p]*(1ll*a[p].size())); //add(后继节点的下标,前驱节点的分子,前驱节点的分母*size) //因为有n个出水管且平均分配,所以分流的流量就是分流之前流量的1/x,所以乘1/x即可,所以就分母乘n就可以 //add(cur, fenzi[], fenmu[]*x)的作用:将前驱结点的流量分流后加入到后继结点中去 if(over[cur])//如果当前节点已经没有其他入水口/没有其他未完成的先决条件了,则不用继续等待入队,跳过 continue; rushuiguanshu[cur]--;//灭掉一个入水口 if(rushuiguanshu[cur]==0){//注意!!第一次if到结点5的时候,5还有2和3这两个入度,所以这里还不能进入if,也就是说,这时候的5并不会被入队! //那么通过结点1入队的有哪些结点呢?对了,有2和3这两个结点!因为他们都是入度=0的点啦! over[cur]=1;// cur没有入水口了,标记一下 q.push(cur);//结合前面if(over[cur]) 可以看出,入队的结点都是已经完成所有先决条件工作的结点,入队后则可以继续为下一层待完成的结点累计先决条件了。 } } } return; } int main() { //freopen("water.in","r",stdin); //freopen("water.out","w",stdout); n=read(),m=read(); for(int i=1;i<=n;i++){ int d=read();//第i个结点有d个排出口 if(d==0){ final[i]=1;//没有排出口的是最终排出口,标记为1 continue;//最终排出口没有次级排出口,不需要读入数据 } while(d--){ int v;//结点对应的每个排出口依次是v v=read(); a[i].push_back(v);//对于结点i,将v加入i的队列表当中,以实现bfs rushuiguanshu[v]++; } } tp(); for(int i=1;i<=n;i++){ if(final[i]){//只输出最终排出口 add(i,0,1);//分数的加法和约分 printf("%lld %lld\n",fenzi[i],fenmu[i]); } } return 0; }
无注释版代码

#include<iostream> #include<cstdio> #include<cmath> #include<algorithm> #include<queue> #include<stack> #include<vector> #include<map> #define ll long long using namespace std; inline ll read(){ ll x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x*f; } int n,m,in[100001],out[100001],book[100001]; ll fenzi[100001],fenmu[100001]; ll gcd(ll x,ll y){ if(y==0) return x; return gcd(y,x%y); } void add(int i,ll x,ll y){ if(y==0) return; if(fenmu[i]==0){ fenzi[i]=x; fenmu[i]=y; return; } ll p1=fenzi[i]*y+fenmu[i]*x; ll p2=fenmu[i]*y;//通分分母 ll p3=gcd(p1,p2);//找到最大公约数 fenzi[i]=p1/p3;//分子除以最大公约数。得到约分后的分子 fenmu[i]=p2/p3;//分母除以最大公约数。得到约分后的分母 return; } vector<int> a[500001]; queue<int> q; void tp(){ for(int i=1;i<=n;i++) { if(!in[i]){ book[i]=1; q.push(i); fenzi[i]=1,fenmu[i]=1; } } while(!q.empty()){ int p=q.front(); q.pop(); if(out[p]) continue; for(int i=0;i<a[p].size();i++){ int cur=a[p][i]; add(cur,fenzi[p],fenmu[p]*(1ll*a[p].size())); if(book[cur]) continue; in[cur]--; if(in[cur]==0){ book[cur]=1; q.push(cur); } } } return; } int main() { //freopen("water.in","r",stdin); //freopen("water.out","w",stdout); n=read(),m=read(); for(int i=1;i<=n;i++){ int d=read(); if(d==0){ out[i]=1; continue; } while(d--){ int v; v=read(); a[i].push_back(v); in[v]++; } } tp(); for(int i=1;i<=n;i++){ if(out[i]){ add(i,0,1); printf("%lld %lld\n",fenzi[i],fenmu[i]); } } return 0; }
代码填空
图解
题目里的tricks
1. 题目并没有给出入水口,要通过rushuiguanshu[]找出来,然后入队开始bfs。
从图中也能看出,没有入水管道的只有1号结点。
2. 在拓扑的基础之上引入了流量的概念,但是流量顺应了拓扑的结构,并且依赖于拓扑来进行计算
3. 拓扑在bfs之上的变体:通过rushuiguanshu[]不为0,控制了bfs的走向。入水管数不为0,则:
不能入队,即不能从该结点开始bfs。即满足了如下定义:在整个工程中,有些子工程(活动)必须在其它有关子工程完成之后才能开始,也就是说,一个子工程的开始是以它的所有前序子工程的结束为先决条件的。
在这里,由于后续结点的分流来自于当前结点,故当前结点的总流入量没有计算完的话,是无法计算后面结点的流入量的。
而结点可以进行bfs,则等价于当前结点的总流入量已经全部计算完成。
例如:
作为图的内部结点,rushuiguanshu[]一开始不为0,直到rushuiguanshu[]为0后入队,over[]也标记为1,表明入水管都被遍历过了
4. over数组其实完全可以被rushuiguanshu[]代替...
get姿势
1. 二维队列数组
定义:vector<int> a[500001];
使用:for(int i=0;i<a[p].size();i++)
2. 拓扑排序是变体的bfs。实现其控制的核心在于,存储每个结点的入度的数组--,直到0才可以入队bfs。而这个操作对应了拓扑排序的定义:在整个工程中,有些子工程(活动)必须在其它有关子工程完成之后才能开始。
最后丢一张稍复杂的图给你品~
原图:
图片来源:
https://www.jianshu.com/p/b1dd76f666da
拓扑时的边和点:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】