DAG
什么是DAG?
DAG (\(Directed\) \(Acycled\) \(Graph\))即 有向无环图,是一种 介于树与有向有环图之间的数据结构,是一种非常重要的工具。
DAG的性质
- 可以在DAG上实现动态规划
- 在处理关于DAG的问题时,需要依次处理每个点,在对一个点进行处理时,需要保证每一个指向它的点都已处理过
- DAG是具有层次关系的
- 有向无环图中必定有至少一个入度为 \(0\) 的点
证明:假设有一个有向无环图中没有入度为 \(0\) 的点,则对于每个点必定有一个点指向当前点,不管如何必定会形成闭环,与有向无环的前提相违背,故有向无环图中必定有至少一个入度为 \(0\) 的点。
代码实现
思路
先将入度为 \(0\) 的点入队,然后进行操作,然后将队首元素删除,顺便把自队首元素出来的边删掉,再次遍历是否有入度为 \(0\) 的点,再入队,再出队……以此类推。
代码
#include<bits/stdc++.h>
using namespace std;
vector<int> e[100001];//邻接表存图
int n,m,u,v,ind[100001];//ind[i]为i的入度
void tra(){//遍历图中点
queue<int> q;//用来存当前入度为0的点
for(int i=1;i<=n;i++)//初始入度为0的点(无前置点)先加入
if(ind[i]==0)q.push(i);
while(!q.empty()){//只要队列非空
int h=q.front();
cout<<h<<" ";
q.pop();//队首出队
//删除队首,队首指向的点入度减1,如果入度变为0就入队
for(int i=0;i<e[h].size();i++)
if(--ind[e[h][i]]==0)q.push(e[h][i]);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>u>>v;
e[u].push_back(v);//注意是有向图
ind[v]++;//v的入度加1
}
tra();
return 0;
}
例题
1.DAG最长路
给定一个 \(n\) 个点,\(m\) 个边的有向无环图,每次给出 \(u,v,dist\),表示 \(u,v\) 间有一条长度为 \(dist\) 的边(方向为\(u\rightarrow v\)),请求出图中的最长的一条路径。
法一
考虑动态规划,\(dp[i]\) 为到 \(i\) 结束的最长路径长。
代码如下:
#include<bits/stdc++.h>
using namespace std;
struct Edge{
int to,val;
};
vector<Edge> e[1000001];//邻接表存图
int n,m,u,v,t,ans,ind[1000001];//ind[i]为i的入度
int dp[1000005]={0};//dp[i]为到达i的最长路
void tra(){//遍历图中点
queue<int> q;//用来存当前入度为0的点
for(int i=1;i<=n;i++)//初始入度为0的点(无前置点)先加入
if(ind[i]==0){
q.push(i);//入度为0的点
dp[i]=0;//初始化为0
}
while(!q.empty()){//只要队列非空
int h=q.front();
q.pop();//队首出队
ans=max(dp[h],ans);
//删除队首,队首指向的点入度减1,如果入度变为0就入队
for(int i=0;i<e[h].size();i++){
if(--ind[e[h][i].to]==0)q.push(e[h][i].to);
dp[e[h][i].to]=max(dp[e[h][i].to],dp[h]+e[h][i].val);
//h点确定好了,用h点更新指向的每个点的答案
}
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>u>>v>>t;
e[u].push_back((Edge){v,t});//注意是有向图
ind[v]++;//v的入度加1
}
tra();
cout<<ans;
return 0;
}
法二
\(dp[i]\) 为从 \(i\) 出发的最长路径,使用记忆化优化
#include<bits/stdc++.h>
using namespace std;
struct Edge{
int to,val;
};
vector<Edge> e[1000001];//邻接表存图
int n,m,u,v,t,ans;
int dp[1000001]={0};//dp[i]为从i出发的最长路
int srh(int k){//返回从k出发的最长路线
if(dp[k]>0)return dp[k];//记忆化
dp[k]=0;//初始为0;
for(int i=0;i<e[k].size();i++)//通过直接通向e[k][i].to
dp[k]=max(dp[k],srh(e[k][i].to)+e[k][i].val);
return dp[k];
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>u>>v>>t;
e[u].push_back((Edge){v,t});//注意是有向图
}
for(int i=1;i<=m;i++)ans=max(ans,srh(i));
cout<<ans;
return 0;
}
2.最大食物链计数
仍然考虑动态规划,\(dp[i]\) 为从 \(i\) 出发,到出度为 \(0\) 的点的方法数。从出度为0的点往回递归,计算总方法数。
#include<bits/stdc++.h>
using namespace std;
vector<int> e[1000001];//邻接表存图
int n,m,u,v,t,ans,ind[1000001];
int dp[1000001]={0};//dp[i]为从i出发的最长路
int srh(int k){//返回从k出发的最长路线
if(dp[k]>0)return dp[k];//记忆化
if(!e[k].size())return dp[k]=1;//出度为0的点
dp[k]=0;//初始为0;
for(int i=0;i<e[k].size();i++)//通过直接通向e[k][i].to
dp[k]=(dp[k]+srh(e[k][i]))%80112002;
return dp[k];
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>u>>v;
e[u].push_back(v);//注意是有向图
ind[v]++;
}
for(int i=1;i<=n;i++)
if(!ind[i])ans=(ans+srh(i))%80112002;//只对符合题意的起点
cout<<ans;
return 0;
}
3.图上判环
给定一个 \(n\) 个点,\(m\) 个边的有向图,判断图中是否存在一个环。如果存在,则输出"Yes",否则输出"No"。给出 \(m\) 对 \(u,v\) 表示 \(u,v\)间有一条有向边(\(u\rightarrow v\))
只需使用 \(cnt\) 记录入度为 \(0\) 的点(遍历过程中入度变成 \(0\) 的也算),如果 \(cnt=n\) 就代表无环。
#include<bits/stdc++.h>
using namespace std;
vector<int> e[1000001];//邻接表存图
int n,m,u,v,t,cnt,ans,ind[1000001];//删除了cnt个
int dp[1000001]={0};//dp[i]为从i出发的最长路
void tra(){
queue<int> q;
for(int i=1;i<=n;i++)
if(!ind[i])q.push(i);
while(!q.empty()){
int h=q.front();
q.pop(),cnt++;
for(int i=0;i<e[h].size();i++)
if(--ind[e[h][i]]==0)q.push(e[h][i]);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>u>>v;
e[u].push_back(v);//注意是有向图
ind[v]++;
}
tra();
if(cnt==n)cout<<"No";//无环
else cout<<"Yes";
return 0;
}
一些值得注意的地方
- 上述算法复杂度大多是 \(O(m)\)(\(m\) 为边数)
- 每次只能处理队列中的点(编号小的优先)

浙公网安备 33010602011771号