链接:登录—专业IT笔试面试备考平台_牛客网
来源:牛客网
题目描述
给定一个包含nnn个点mmm条边的有向无环图,求出该图的拓扑序。若图的拓扑序不唯一,输出任意合法的拓扑序即可。若该图不能拓扑排序,输出−1-1−1。
输入描述:
第一行输入两个整数n,mn,mn,m ( 1≤n,m≤2⋅1051\le n,m \le 2\cdot 10^51≤n,m≤2⋅105),表示点的个数和边的条数。 接下来的mmm行,每行输入两个整数ui,viu_i,v_iui,vi (1≤u,v≤n1\le u,v \le n1≤u,v≤n),表示uiu_iui到viv_ivi之间有一条有向边。
输出描述:
若图存在拓扑序,输出一行nnn个整数,表示拓扑序。否则输出−1-1−1。
输入:
5 4
1 2
2 3
3 4
4 5
输出:
1 2 3 4 5
答案:
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 100;
int din[N];
queue<int >q;//队列用来存入度为零的点
vector<int >tp;//用来存出队的点,最终结果。
vector<int>e[N];//存图
int n,m;
void solve()
{
for(int i=1;i<=n;i++){
if(!din[i])q.push(i);
}
while(q.size()) {
int u=q.front();
tp.push_back(u);
q.pop();
for(auto it:e[u]){//auto it:e[u]是循环体的一种表示将e[u]中全部节点都执行一遍循环体
--din[it];//it代表节点
if(!din[it])
q.push(it);
}
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
e[u].push_back(v);
++din[v];
}
solve();
if(tp.size()!=n)puts("-1");
else{
for(int i=0;i<n;i++){
cout<<tp[i];
if(i!=n-1)cout<<" ";
}
}
return 0;
}
解释ai:
这段代码实现的是一个拓扑排序算法,用于处理有向无环图(DAG)的顶点排序问题。拓扑排序是针对一个DAG的所有顶点的线性排序,使得对于从顶点u到顶点v的每条有向边(u, v),u都在v之前。这个算法在很多领域都有应用,比如任务调度、课程安排等。
下面是代码的详细解析:
- 头文件和命名空间:
#include<bits/stdc++.h>
:这是一个非标准的头文件,包含了几乎所有C++标准库的内容,但在竞赛编程中常用以简化代码。using namespace std;
:使用标准命名空间,避免在调用标准库时重复写std::
。
- 类型定义:
ull
:无符号长整型(unsigned long long)。ll
:长整型(long long)。pii
:整型对(pair<int, int>)。
- 常量定义:
N
:顶点的最大数量。inf
:无穷大值,用于表示某些特殊状态。mod
:取模的值,常用于防止整数溢出或计算哈希值。
- 全局变量:
dx[]
和dy[]
:用于二维网格的行列移动,但在这段代码中未使用。din[]
:记录每个顶点的入度(即有多少条边指向该顶点)。q
:用于拓扑排序的队列,存储当前入度为0的顶点。tp
:存储拓扑排序结果的向量。e[]
:邻接表,存储图的信息。
- 函数定义:
solve()
:实现拓扑排序的核心函数。首先将所有入度为0的顶点加入队列,然后不断从队列中取出顶点,将其加入结果向量tp
,并减少其相邻顶点的入度。如果相邻顶点的入度变为0,则将其加入队列。
- 主函数(
main
):- 输入顶点数
n
和边数m
。 - 读取每条边的起点和终点,构建图并计算每个顶点的入度。
- 调用
solve()
函数进行拓扑排序。 - 如果拓扑排序的结果向量
tp
中的顶点数不等于顶点总数n
,说明图中存在环,输出"-1";否则,输出拓扑排序的结果。
- 输入顶点数
注意:这段代码假设输入的图是合法的DAG,即不存在环。如果存在环,拓扑排序无法进行,因此会输出"-1"。此外,这段代码在处理边时只增加了终点的入度,没有建立从起点到终点的直接连接(因为拓扑排序只关心入度),这是邻接表的一种常见用法。
在C++中,auto it: e[u]
这种写法实际上是 for
循环中的一部分,但它本身是不完整的。完整的写法应该是 for(auto it : e[u])
,这是C++11及以后版本中引入的基于范围的 for
循环(range-based for loop)的语法。
这个循环的作用是遍历容器(如 std::vector
、std::list
、std::deque
等)或任何提供了 begin()
和 end()
成员函数(或全局函数)的序列的所有元素。在你的上下文中,e[u]
应该是一个容器(比如 std::vector<int>
),它存储了与顶点 u
相邻的所有顶点的索引或标识符。
因此,for(auto it : e[u])
的意思是:“对于 e[u]
容器中的每一个元素(我们称之为 it
),都执行一次循环体中的代码”。这里的 it
是自动类型推导的,它的类型与 e[u]
容器中的元素类型相同。
在你的代码中,e
是一个 std::vector<std::vector<int>>
类型的邻接表,其中 e[u]
表示顶点 u
的所有邻接顶点列表。因此,for(auto it : e[u])
就是在遍历顶点 u
的所有邻接顶点,并且对于每一个邻接顶点 it
,都会执行循环体内的代码,即减少该顶点的入度,并检查是否需要将其加入队列 q
中。
在您的代码片段中,for(auto it:e[u])
循环遍历了顶点 u
的所有邻接顶点(即所有从 u
出发可以到达的顶点)。对于 u
的每一个邻接顶点 it
,代码执行了两个关键操作:
-
--din[it];
:这行代码将顶点it
的入度(din[it]
)减一。这是因为我们刚刚从顶点u
到顶点it
移除了一条边(在拓扑排序的上下文中,这通常意味着我们“处理”了顶点u
,并考虑到了它指向的所有顶点),所以顶点it
的入度应该减少。 -
if(!din[it]) q.push(it);
:这行代码检查顶点it
的入度是否变为了 0。如果是,那么顶点it
现在没有任何入边(即没有其他顶点指向它),这意味着它可以在拓扑排序的结果序列中被考虑。因此,我们将顶点it
加入队列q
中,以便稍后在拓扑排序的结果序列中进一步处理它。
这个过程是拓扑排序算法的核心。拓扑排序算法依赖于一个事实:在一个有向无环图(DAG)中,如果我们不断地移除入度为 0 的顶点以及从它们出发的所有边,那么最终我们将会移除图中的所有顶点(前提是图是连通的,或者我们分别处理图的所有连通分量)。这个过程中顶点的移除顺序就是一个有效的拓扑排序。
总结来说,这段代码通过不断地从图中移除入度为 0 的顶点(以及从它们出发的边),来构建图的拓扑排序。如果最终我们能够移除图中的所有顶点,那么图就是一个DAG,并且我们得到了一个有效的拓扑排序;如果过程中我们发现某个顶点无法被移除(即其入度始终不为 0),那么图中就存在环,无法进行拓扑排序。