有向无环图
定义
没有环的有向图,也称为DAG
有向无环图
性质
性质一
从任意一点进行遍历,必不会陷入死循环
性质二
入度为0的点信息确定,新的入度为0的点信息也确定
拓扑序
写法
1.将入度为0的数入列;
2.将出队的节点的出边所指向的点的入度-1,如果该节点的入度也为0,那么将该节点入队;
3.当所有的点都出队,那么DAG的遍历结束
这样的遍历顺序被称为拓扑序
例题
题面
洛谷P1807https://www.luogu.com.cn/problem/P1807
示例代码
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
const int M=3e6+5;
int n,m;
int h[N],v[M],w[M],nx[M],tot,du[N],dis[N];
void add(int x,int y,int z){
tot++;
v[tot]=y;//存出边指向的点
w[tot]=z;//存边的长度
nx[tot]=h[x];//指向下一组数据
h[x]=tot;
}
long long read() {
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9') {
if(c=='-') f=-1;
c=getchar();
}
while(c>='0'&&c<='9') {
x=x*10+c-'0';
c=getchar();
}
return x*f;
}//快读
void topsort(){
queue<int>q;//建立队列
for(int i=1;i<=n;i++){
dis[i]=-2e9;//赋初值为-2e9
if(du[i]==0) q.push(i);//如果入度为0那么入对
}
dis[1]=0;//1到1的距离设为0
while (!q.empty()){
int x=q.front();//获得队列头
q.pop();//队列头出队
for(int i=h[x];i;i=nx[i]){
du[v[i]]-=1;//出边所指向的点入度-1
if(dis[x]!=-2e9) dis[v[i]]=max(dis[v[i]],dis[x]+w[i]);
//若出队的点不为初始值,证明与1相通,那么更新该点到1的距离
if(du[v[i]]==0 ) q.push(v[i]);
//如果入度为0则加入队列
}
}
}
int main(){
n=read();
m=read();
for(int i=1;i<=m;i++){
int x,y,z;
x=read();
y=read();
z=read();
add(x,y,z);//邻接表存储
du[y]+=1;//出边所指向的点入读加1
}
topsort();
if(dis[n]<=-1e9){
cout<<-1;
return 0;
}
// 无法抵达则不会被更新
cout<<dis[n];//输出1到n的距离
return 0;
}
拓扑排序
在DAG中,使用拓扑序进行遍历,将结点的先后顺序输出就是拓扑排序;
这张DAG有两种拓扑排序结果:
2->5->1->3->4
5->2->1->3->4
写法
我们只需要把出队后的点进行存储,再输出后就可以记录拓扑排序
例题
题面
描述
给定一个 n 个点 m 条边的有向图,点的编号是 1 到 n,图中可能存在重边和自环。
请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 −1。
若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x 在 A 中都出现在 y 之前,则称 A 是该图的一个拓扑序列。
输入描述
第一行包含两个整数 n 和 m。
接下来 m 行,每行包含两个整数 x 和 y,表示存在一条从点 x
到点 y 的有向边 (x,y)。
输出描述
共一行,如果存在多个拓扑序列,则输出其中字典序最小的即可。
如果不存在拓扑序列,则输出 −1。
样例
输入
3 3
1 2
2 3
1 3
输出
1 2 3
代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m;
int h[N],v[N],nx[N],tot;
int dis[N],du[N];
priority_queue<int,vector<int>,greater<int> >q;
//优先队列(小根堆,也就是把队列内的数字从小往大排),因为是求最小字典序
vector<int>ans;//动态数组存储拓扑排序
void add(int x,int y){
tot++;
v[tot]=y;//存出边指向的点
nx[tot]=h[x];//指向下一组数据
h[x]=tot;
}
void bfs(){
for(int i=1;i<=n;i++){
dis[i]=-2e9;//赋初值为-2e9
if(du[i]==0) q.push(i);//如果入度为0那么入对
}
while(!q.empty()){
int x=q.top();//获得队列头
q.pop();//队列头出队
ans.push_back(x); //将节点放入拓扑排序的内容中
for(int i=h[x];i;i=nx[i]){
du[v[i]]--;//将出边指向的点的入度-1
if(du[v[i]]==0) q.push(v[i]);//如果该点的入度为0则入队
}
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
add(x,y);//邻接表存储
du[y]++;//出边指向的点入度加1
}
bfs();
if(ans.size()!=n){
cout<<-1;
return 0;
}//如果拓扑排序访问过的数量小于或大于总点数,那么则没有拓扑排序
for(int i=0;i<ans.size();i++) cout<<ans[i]<<' ';
//输出最小字典序的拓扑排序
return 0;
}
拓扑序的性质
拓扑排序:1-2-3-4-5-6
性质一
任何一个数都无法更新在它位置之前的数(如5无法更新1,2,3,4;6无法更新1,2,3,4,5)
性质二
任何一个数都可以更新在它位置之后的数(如3可以更新3,4,5,6;1可以更新2,3,4,5,6)
例题
题面
洛谷P3074https://www.luogu.com.cn/problem/P3074
代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5;
const int M=5e4+5;
int n,m,ans;
int h[N],v[M],nx[M],tot,a[N],t[N];
int du[N];
queue<int>q;
long long read() {
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9') {
if(c=='-') f=-1;
c=getchar();
}
while(c>='0'&&c<='9') {
x=x*10+c-'0';
c=getchar();
}
return x*f;
}//快速读入
void add(int x,int y){
tot++;
v[tot]=y;//存出边指向的点
nx[tot]=h[x];//指向下一组数据
h[x]=tot;
}
void bfs(){
for(int i=1;i<=n;i++){
t[i]=a[i];//复制数组
if(du[i]==0) q.push(i),ans=max(ans,a[i]);
//入度为0入队,最大值也有可能是只挤一头奶牛的时间
}
while(!q.empty()){
int x=q.front();//获得队列头
q.pop(); //队列头出队
for(int i=h[x];i;i=nx[i]){
t[v[i]]=max(t[v[i]],t[x]+a[v[i]]);
//求挤完这头奶牛时要花的最短时间
ans=max(ans,t[v[i]]);
//求挤完某头奶牛时所花的时间
du[v[i]]--;
//将出边指向的点的入度-1
if(du[v[i]]==0) q.push(v[i]);
//如果该点的入度为0则入队
}
}
}
int main(){
n=read();
m=read();
for(int i=1;i<=n;i++) a[i]=read();
for(int i=1;i<=m;i++){
int x,y;
x=read();
y=read();
add(x,y);//邻接表存储
du[y]++;//出边指向的点入度加1
}
bfs();
printf("%d",ans);//输出要花的时间
return 0;
}
拓扑序解决层次问题
把节点分成若干层次,可以描述某一节点事件执行的优先级。
拓扑排序能够保证优先级高的节点在优先级低的节点的前端。
层次 | 节点编号 |
---|---|
1 | 1 |
2 | 2,3 |
3 | 4,5,6 |