深度优先搜索
图的深度优先遍历(Depth First Search,DFS)是一种遍历图的算法,其基本思想是从起始顶点开始,不断访问邻接顶点,直到无法继续访问为止,然后回溯到之前的顶点,继续访问其未被访问过的邻接顶点。
具体过程可以描述如下:
- 从起始顶点开始,将其标记为已访问。
- 访问起始顶点的一个邻接顶点(任意一个未被访问过的邻接顶点)。
- 如果邻接顶点已经被访问过,则回溯到之前的顶点,继续访问其未被访问过的邻接顶点;
- 如果所有邻接顶点都已经被访问过,则回溯到之前的顶点,继续访问其未被访问过的邻接顶点。
重复步骤2和3,直到所有顶点都被访问过为止。
在实现过程中,可以使用栈来存储待访问的顶点,每次访问一个顶点时,将其所有未被访问过的邻接顶点压入栈中,然后从栈中弹出一个顶点进行访问。
需要注意的是,在实现过程中要避免重复访问顶点,可以使用标记数组来记录每个顶点是否已经被访问过。
深度搜索是一种重要的图算法,广泛应用于各种应用场景中。以下列举几个深度搜索常见的应用场景:
图像处理
在图像处理中,深度搜索可以用来寻找连通区域、分割目标等。例如,在数字化医学影像中,深度搜索可以帮助医生自动识别和分割出患者肿瘤的位置,从而提高医疗诊断和治疗的效率。
数据挖掘
在数据挖掘中,深度搜索可以用于发现数据之间的关系、识别模式等。例如,在社交网络分析中,深度搜索可以用来查找用户之间的关系,确定用户群组、推测用户兴趣等,从而为用户推荐更加精准的内容和服务。
网络安全
在网络安全领域,深度搜索可以用于检测恶意代码、漏洞等。例如,通过对计算机系统进行深度搜索,可以快速定位系统中存在的风险,提高安全性。
游戏开发
在游戏开发中,深度搜索可以用于人工智能的实现,帮助游戏角色更加智能地行动和决策。例如,在围棋游戏中,深度搜索可以用于计算每一步对棋局的影响,并决定下一步最优的走法。
自然语言处理
在自然语言处理中,深度搜索可以用于分词、语义分析、命名实体识别等。例如,在机器翻译中,深度搜索可以通过扫描整个句子,找到与当前单词相关的所有信息,从而更好地理解原文的意思,并生成更加准确的翻译结果。
总之,深度搜索作为一种重要的图算法,在许多领域都有着广泛的应用。无论是在科学研究、工程设计还是商业应用中,深度搜索都可以发挥重要的作用,帮助我们更好地理解和利用复杂的数据结构。
对下图进行深度优先搜索,写出搜索结果。注意:从A出发。
从顶点A出发,进行深度优先搜索的结果为:A,B,C,D,E。
假设我们从顶点1开始进行深度优先遍历,遍历过程如下:
从顶点A开始,访问顶点A,并将顶点A标记为已访问。
从顶点A出发,遍历与其相邻的未访问过的顶点B,访问顶点B,并将顶点B标记为已访问。
从顶点B出发,遍历与其相邻的未访问过的顶点C,访问顶点C,并将顶点C标记为已访问。
从顶点C出发,遍历与其相邻的未访问过的顶点D,访问顶点D,并将顶点D标记为已访问。
从顶点D出发,遍历与其相邻的未访问过的顶点E,访问顶点E,并将顶点E标记为已访问。
从顶点E出发,发现与其相邻的所有顶点都已经访问过了,返回到顶点D。
从顶点D出发,发现与其相邻的所有顶点都已经访问过了,返回到顶点C。
从顶点C出发,发现与其相邻的所有顶点都已经访问过了,返回到顶点B。
从顶点B出发,发现与其相邻的另一个顶点A已经访问过了,返回到顶点A。
完成遍历。
对于一个连通图,深度优先遍历的递归过程如下:
void dfs(int i) { //图用邻接矩阵存储
//访问顶点i;
visited[i]=1;//标记已访问
for(int j=1; j<=n; j++)
if(!visited[j] && a[i][j]) dfs(j);
}
以上dfs(i)的时间复杂度为O(n^2)。
对于一个非连通图,调用一次dfs(i),即按深度优先顺序依次访问了顶点i所在的(强)连通分支,所以只要在主程序中加上:
for(int i=1; i<=n; i++) //深度优先搜索每一个未被访问过的顶点
if(!visited[i]) dfs(i);
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
vector<int> g[MAXN];
bool vis[MAXN];
void dfs(int u) {
vis[u] = true;
cout << u << " ";
for (int i = 0; i < g[u].size(); i++) {
int v = g[u][i];
if (!vis[v]) dfs(v);
}
}
int main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(2);
return 0;
}
输入:5 5
1 3
2 5
5 6
6 7
3 4
输出:2 5 6 7
广度优先搜索(宽度优先搜索)
广度优先搜索(BFS)是一种用于图形搜索的算法,它从起始顶点开始,逐层地向外扩展搜索。具体来说,BFS会先访问起始顶点,然后依次访问与起始顶点直接相邻的所有顶点,接着访问与这些顶点直接相邻的未被访问过的顶点,以此类推,直到找到目标顶点或者遍历完整张图。
下面是BFS的过程:
- 选择任意一个顶点作为起点,将其加入队列中。
- 从队列中取出一个顶点,并访问它。
- 将与该顶点相邻且未被访问过的所有顶点加入队列中。
- 重复步骤2和3,直到队列为空。
在BFS过程中,可以使用一个数组记录每个顶点的状态:未被访问过、已经被访问过但是还未被扩展、已经被访问过且已经被扩展。这样可以避免重复访问同一个顶点。
广度搜索在很多领域都有应用,以下是一些常见的应用场景:
迷宫问题:广度搜索可用于求解迷宫问题,从起点开始广度遍历整个迷宫,直到找到终点为止。
图像分割:广度搜索可用于图像分割,将图像的所有像素分为不同的连通块。
搜索引擎:广度搜索可以用于搜索引擎中的网页抓取和索引,从一个网页开始,广度遍历其他相关的网页。
社交网络分析:广度搜索可用于社交网络分析,通过广度遍历社交网络中的节点和边缘,探索用户之间的关系。
电子游戏AI:广度搜索可用于电子游戏AI中的路径规划和NPC行为决策。
对下图从A出发进行宽度优先搜索,写出搜索结果。 时间复杂度是O(n^2).
假设有一个图,其中包含以下节点及其之间的连接关系:
A – B – E
| | |
C – D – F
从节点 A 开始进行广度优先搜索,搜索过程如下:
将 A 加入队列中,标记 A 为已访问。
队列:A
已访问:A
从队列中取出 A,遍历 A 的邻居节点 B 和 C。将 B 和 C 加入队列中,并标记为已访问。
队列:B, C
已访问:A, B, C
从队列中取出 B,遍历 B 的邻居节点 E 和 D。将 E 和 D 加入队列中,并标记为已访问。
队列:C, E, D
已访问:A, B, C, E, D
从队列中取出 C,遍历 C 的邻居节点 D。由于 D 已经被访问过,不需要加入队列中。
队列:E, D
已访问:A, B, C, E, D
从队列中取出 E,遍历 E 的邻居节点 F。将 F 加入队列中,并标记为已访问。
队列:D, F
已访问:A, B, C, E, D, F
从队列中取出 D,遍历 D 的邻居节点 F。由于 F 已经被访问过,不需要加入队列中。
队列:F
已访问:A, B, C, E, D, F
从队列中取出 F,遍历 F 的邻居节点。没有节点需要访问。
队列:
已访问:A, B, C, E, D, F
最终遍历的顺序为 A -> B -> C -> E -> D -> F。
对上无向图进行深度优先遍历,从A开始:
第1步:访问A。
第2步:访问B(A的邻接点)。 在第1步访问A之后,接下来应该访问的是A的邻接点,即"B,D,F"中的一个。但在本文的实现中,顶点ABCDEFGH是按照顺序存储,B在"D和F"的前面,因此,先访问B。
第3步:访问G(B的邻接点)。 和B相连只有"G"(A已经访问过了)
第4步:访问E(G的邻接点)。 在第3步访问了B的邻接点G之后,接下来应该访问G的邻接点,即"E和H"中一个(B已经被访问过,就不算在内)。而由于E在H之前,先访问E。
第5步:访问C(E的邻接点)。 和E相连只有"C"(G已经访问过了)。
第6步:访问D(C的邻接点)。
第7步:访问H。因为D没有未被访问的邻接点;因此,一直回溯到访问G的另一个邻接点H。
第8步:访问(H的邻接点)F。
因此访问顺序是:A -> B -> G -> E -> C -> D -> H -> F
【练习】
以下是一个广度搜索在社交网络中的例题:
假设有如下社交网络(使用邻接矩阵表示):
A B C D E F G H I J K L
A 0 1 0 1 1 0 0 0 0 0 0 0
B 1 0 1 0 1 1 0 0 0 0 0 0
C 0 1 0 0 0 0 1 1 0 0 0 0
D 1 0 0 0 0 0 0 0 0 0 0 0
E 1 1 0 0 0 0 0 0 0 0 0 0
F 0 1 0 0 0 0 0 0 0 0 0 0
G 0 0 1 0 0 0 0 0 0 0 0 0
H 0 0 1 0 0 0 0 0 0 0 0 0
I 0 0 0 0 0 0 0 0 0 1 1 0
J 0 0 0 0 0 0 0 0 1 0 1 1
K 0 0 0 0 0 0 0 0 1 1 0 1
L 0 0 0 0 0 0 0 0 0 1 1 0
其中,每行表示一个用户,每列表示该用户与其他用户之间是否有关系,1表示有关系,0表示没有关系。例如,第一行表示用户A与其他用户之间的关系,可以看出A与B、D、E直接相连。
现在以用户A为起点进行广度搜索,请列出搜索过程中每个节点的顺序。
根据广度搜索的原理,我们从起点A开始,将其作为第一层结点,然后依次访问与该结点直接相连的所有未访问的节点,并将这些结点添加到第二层。然后再依次访问第二层结点相连的所有未访问节点,并将这些结点加入第三层。以此类推,直到找到终点或者所有可访问的节点都被访问过为止。
在本例中,A与B、D、E直接相连,因此将B、D、E作为第二层结点加入队列中。然后访问第二层结点B、D、E相连的未访问节点C、F、I,将它们作为第三层结点加入队列中。接下来访问第三层结点C、F、I相连的未访问节点G、H、J、K,将它们作为第四层结点加入队列中。最后,访问第四层结点J、K、L相连的未访问节点。
因此,广度搜索过程中每个节点的顺序是A、B、D、E、C、F、I、G、H、J、K、L。
void bfs(int i) { //宽度优先遍历,图用邻接矩阵表示
queue<int> q;
i=q.pop();
visited[i]=true;
q.push(i);
while(!q.empty()) {
v=q.front();
q.pop();
for(int j=1; j<=n; j++) { //对i的所有邻结矩阵进行遍历
if(!visited[j]) {
visited[j]=1;
q.push(j);
}
}
}
}
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
vector<int> g[MAXN];
bool vis[MAXN];
void bfs(int u) {
queue<int> q;
q.push(u);
vis[u] = true;
while (!q.empty()) {
int cur = q.front();
q.pop();
cout << cur << " ";
for (int i = 0; i < g[cur].size(); i++) {
int v = g[cur][i];
if (!vis[v]) {
q.push(v);
vis[v] = true;
}
}
}
}
int main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
bfs(1);
return 0;
}
输入:
5 5
1 3
2 6
3 4
5 6
4 8
输出:1 3 4 8
BFS与DFS的总结:
DFS:类似回溯,利用堆栈进行搜索
BFS:类似树的层次遍历,利用队列进行搜索
DFS:尽可能地走“顶点表”
BFS:尽可能地沿着顶点的“边”进行访问
DFS:容易记录访问过的路径
BFS:不易记录访问过的路径,需要开辟另外的存储空间进行保存路径
图的最短路径算法
分类:
多源最短路径算法:求任意两点之间的最短距离。(Floyd算法)
单源最短路径算法:求一个点到其他所有点的最短路径。(ijkstra算法,Spfa算法,Bellman-ford算法)
【练习一】八皇后问题
题目描述
一个如下的 6 × 6 6 \times 6 6×6 的跳棋棋盘,有六个棋子被放置在棋盘上,使得每行、每列有且只有一个,每条对角线(包括两条主对角线的所有平行线)上至多有一个棋子。
上面的布局可以用序列 2 4 6 1 3 5 2\ 4\ 6\ 1\ 3\ 5 2 4 6 1 3 5 来描述,第 i i i 个数字表示在第 i i i 行的相应位置有一个棋子,如下:
行号 1 2 3 4 5 6 1\ 2\ 3\ 4\ 5\ 6 1 2 3 4 5 6
列号 2 4 6 1 3 5 2\ 4\ 6\ 1\ 3\ 5 2 4 6 1 3 5
这只是棋子放置的一个解。请编一个程序找出所有棋子放置的解。
并把它们以上面的序列方法输出,解按字典顺序排列。
请输出前
3
3
3 个解。最后一行是解的总个数。
输入格式
一行一个正整数 n n n,表示棋盘是 n × n n \times n n×n 大小的。
输出格式
前三行为前三个解,每个解的两个数字之间用一个空格隔开。第四行只有一个数字,表示解的总数。
样例 #1
样例输入 #1
6
样例输出 #1
2 4 6 1 3 5
3 6 2 5 1 4
4 1 5 2 6 3
4
提示
【数据范围】
对于
100
%
100\%
100% 的数据,
6
≤
n
≤
13
6 \le n \le 13
6≤n≤13。
【题目解释】
在6 x 6的棋盘上摆放6个皇后,而且八个皇后中的任意两个是不能处于同一行、同一列、或同一斜线上。
在6 x 6的棋盘上面放置6个皇后,而且还要不在不同一行和不在同一列,不在同一斜线上,所以每行肯定是得放一个,但是位置就有好多的可能,只要满足上面的要求即可。 设棋盘是一个6 x 6矩阵,皇后i和皇后j的摆放位置分别为(i,Xi)和(j,Xj),要想这些皇后不在同一条斜线上,则需要这两个坐标点的斜率不等于 1 或 - 1。 也就是满足|Xj —Xi | ≠ |j – i| 这里采用迭代法解决八皇后问题,迭代就是循环代码中参与运算的变量同时是保存结果的变量,当前保存的结果作为下一次循环计算的初始值。
图1
白格1 行+列=1+3=4
白格2 行+列=2+2=4
白格3 行+列=3+1=4
图2
白格1 行−列=1−1=0
白格2 行−列=2−2=0
白格3 行−列=3−3=0
你发现规律了吗?
不难得出,如果两个格子在同一对角线上,则:
格子1 abs(行-列) = 格子2 abs(行-列)
或者
格子1 行+列=格子2 行+列
因为行−列很可能为负数,导致数组越界而CE。所以,必须把这个玩意整成正整数。
因为搜索的复杂度很不错,所以如果不停调用系统函数abs很可能超时。
所以,我们得想想其他办法。
假设:
a=b=3
a+3=3+3=6
b+3=3+3=6
得出结论:
两个相等的数,加上相同的一个数,两者仍然相等。
所以我们可以加上n来判断两个皇后的位置是否在同一对角线上。(因为1<=i,j<=n)
#include <iostream>
using namespace std;
int n;
int cnt;
int lie[20]; //判断用同一列是否存在数
int a[20];
int youxia[50]; //右斜差为定值 ,需要考虑负值的情况 !很有用的知识!
int zuoxia[20]; //左斜和为定值 !
void pr()
{
if (cnt <= 3)
{
for (int i = 1; i < n; i++)
{
cout << a[i] << " ";
}
cout << a[n] << endl;
}
}
void dfs(int i)
{
if (i > n)
{
cnt++;
pr();
}
else
{
for (int j = 1; j <= n; j++)
{
if (lie[j] == 0 && youxia[j - i + n] == 0 && zuoxia[j + i] == 0)
{
lie[j] = 1;
youxia[j - i + n] = 1;
zuoxia[j + i] = 1;
a[i] = j;
dfs(i + 1);
lie[j] = 0;
youxia[j - i + n] = 0;
zuoxia[j + i] = 0;
}
}
}
}
int main()
{
cin >> n;
dfs(1);
cout << cnt << endl;
}
Floyd算法(插点法)
Floyd算法,也称为插点法,是一种用于寻找有向图中多源点最短路径的算法。该算法的时间复杂度为O(n^3),其中n为图中节点数。
Floyd算法的基本思想是通过中间节点来更新两个节点之间的最短路径。具体来说,对于图中的任意两个节点i和j,如果存在一个中间节点k,使得从i到j经过k的路径比直接从i到j的路径更短,则更新i到j的最短路径为i到k的最短路径加上k到j的最短路径。
Floyd算法的优点是适用于任意图形,包括有负权边的图,同时可以同时求出任意两点之间的最短路径。缺点是时间复杂度高,当节点数量很大时,计算量会非常大。
假设我们有一个带权有向图,其邻接矩阵如下:
0 | 2 | 6 | 4
∞ | 0 | 3 | ∞
7 | ∞ | 0 | 1
5 | ∞ | 12 |0
其中,∞ 表示两个节点之间没有边相连,即权重为无穷大。
我们想要求出任意两点之间的最短路径长度,使用 Floyd 算法可以实现。
首先,我们定义一个二维数组 dp,表示从节点 i 到节点 j 的最短路径长度。初始状态下,dp[i][j] 的值为图中节点 i 到节点 j 的边权值,如果 i 和 j 之间没有边相连,则其值为无穷大。
然后,我们对 dp 数组进行更新,通过中间节点 k 来缩小路径长度。具体操作如下:
遍历所有节点 k,然后再遍历所有节点 i 和 j。如果从节点 i 到节点 j 经过节点 k 可以缩短路径长度,则更新 dp[i][j] 的值为 dp[i][k] + dp[k][j]。
最终,当遍历完所有节点 k 后,dp 数组中存储的就是任意两点之间的最短路径长度。
对于上面的邻接矩阵,经过一次 Floyd 算法的迭代,dp 数组会变成如下状态:
0 2 5 4
∞ 0 3 ∞
7 9 0 1
5 7 10 0
其中,dp[i][j] 表示从节点 i 到节点 j 的最短路径长度。可以看到,经过一次迭代后,我们可以找到所有不经过中间节点的最短路径长度。
接下来,我们再进行一次迭代,就可以找到所有经过一个中间节点的最短路径长度。最终,经过三次迭代后,dp 数组会变成如下状态:
0 2 5 4
7 0 3 8
6 8 0 1
5 7 10 0
可以看到,经过三次迭代后,我们已经找到了所有节点之间的最短路径长度。
#include<bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int maxn = 205;
int dis[maxn][maxn]; // 存储任意两点间的最短距离
void floyd(int n) {
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (dis[i][k] != INF && dis[k][j] != INF)
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
}
}
}
int main() {
int n, m;
cin >> n >> m; // 初始化
memset(dis, INF, sizeof(dis));
for (int i = 1; i <= n; i++) dis[i][i] = 0; // 读入每条边
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
dis[u][v] = min(dis[u][v], w);
dis[v][u] = min(dis[v][u], w); // 若为无向图,需加上这一行
}
floyd(n); // 求任意两点间的最短路
// 输出任意两点间的最短距离
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (dis[i][j] == INF) cout << "INF ";
else cout << dis[i][j] << " ";
}
cout << endl;
}
return 0;
}
Dijkstra算法 (迪杰斯特拉算法)
Dijkstra算法是一种用于求解单源最短路径的贪心算法。下面是Dijkstra算法的过程:
- 初始化:将源节点的距离设置为0,将其他节点的距离设置为正无穷大。
- 遍历:对于每个节点,计算从源节点到该节点的距离,并更新该节点的距离。
- 选择:在未处理的节点中,选择距离最短的节点作为下一个处理的节点。
- 更新:对于该节点的所有邻居节点,计算从源节点经过该节点到达邻居节点的距离是否比原来的距离短,如果是,则更新邻居节点的距离。
- 标记:将该节点标记为已处理。
- 重复:重复步骤3~5,直到所有节点都被处理。
- 输出:最终得到源节点到每个节点的最短路径。
Dijkstra算法可以用于解决多种问题,如路由选择、图像处理等。但是,它只适用于带有非负边权的图,如果存在负边权,需要使用其他算法,如Bellman-Ford算法。
Spfa算法
拓扑排序
拓扑排序是指将有向无环图(DAG)中的节点按照它们之间的依赖关系排序的过程。其中,如果有一条从节点 A 到节点 B 的有向边,表示节点 A 依赖于节点 B,则节点 B 应该先于节点 A 排序。
拓扑排序可以用来解决很多实际问题,比如编译器的依赖关系、任务调度等等。
常见的拓扑排序算法有 Kahn 算法和 DFS 算法。其中,Kahn 算法基于贪心思想,每次选择入度为 0 的节点进行排序;DFS 算法则通过深度优先搜索的方式进行排序。
需要注意的是,只有有向无环图才能进行拓扑排序,否则就会出现循环依赖的情况。
拓扑排序是一个有向无环图(DAG)。要满足:
(1)每个顶点出现且只出现一次。
(2)若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
构造拓扑序列的方法:
(1)从 DAG 图中选择一个 没有前驱(即入度为0)的顶点并输出。
(2)从图中删除该顶点和所有以它为起点的有向边。
(3)重复 1 和 2 直到当前的 DAG 图为空或当前图中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环。
举例:
这个地方我们使用邻接表来存储图的数据(不用邻接矩阵),邻接表是一种图的表示方法,它是由若干个链表组成的,其中每个链表表示一个节点的所有邻居节点。具体来说,邻接表中的每个节点都包含两个字段,分别是该节点的标识符和一个指向以该节点为起点的所有边的链表的头指针。对于无向图,每条边都会在两个节点的邻接表中都出现一次;而对于有向图,则只会在起点节点的邻接表中出现。
邻接表的优点在于,它只需要O(E)的空间,其中E为边的数量,而不像邻接矩阵需要O(V^2)的空间,其中V为节点的数量。因此,当图的边的数量较小,或者需要快速地查询某个节点的邻居节点时,邻接表是一种较好的选择。
使用邻接表时,我们可以通过遍历每个节点的邻接表来遍历整个图。同时,由于邻接表是链表,因此我们可以通过在链表中插入或删除节点,来实现图的动态修改。
邻接表和邻接矩阵都是图的表示方法。
邻接表是一种链式存储结构,每个节点都对应一个链表,链表中存储该节点所连向的节点。邻接表适用于稀疏图,因为只需要存储有连接的节点即可,不需要存储无连接的节点。邻接表的空间复杂度为O(V+E),其中V为节点数,E为边数。
邻接矩阵是一种二维数组,数组中的元素表示节点之间是否有边相连。邻接矩阵适用于稠密图,因为每个节点都与其他节点相连,所以需要存储所有节点之间的连接情况。邻接矩阵的空间复杂度为O(V^2),其中V为节点数。
相同点:
都可以用来表示图。
都可以进行遍历和搜索。
区别:
存储方式不同:邻接表是链式存储结构,邻接矩阵是二维数组。
空间复杂度不同:邻接表适用于稀疏图,空间复杂度为O(V+E);邻接矩阵适用于稠密图,空间复杂度为O(V^2)。
时间复杂度不同:邻接表适用于搜索起点的度数较小的情况,时间复杂度为O(V+E);邻接矩阵适用于搜索起点的度数较大的情况,时间复杂度为O(V^2)。
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int h[N], e[N], ne[N], idx;
int d[N]; // 入度
int q[N]; // 存储拓扑序
int n, m;
//代码实现了向一个邻接表中添加一条从节点a到节点b的边。
//具体来说,代码中的h数组是一个大小为N的数组,表示每个节点的邻接表的头结点位置。其中,N为节点总数。
//在添加从节点a到节点b的边时,我们将b加入节点a的邻接表中。具体实现为:首先将b加入到邻接表的数组e中,然后将邻接表的数组ne中的第h[a]个元素(即节点a
//的邻接表的头结点位置)赋值给当前的ne[idx],最后将当前的idx作为节点a的新的邻接表的头结点位置,更新h[a]为idx。
//最后,需要注意的是,idx是一个全局变量,用来记录当前邻接表中已存储的边的数量。每次添加一条边之后,idx的值都会自增1。
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
// n表示图中节点的总数,
// d[i]表示节点i的入度,
// h[i]表示节点i的出边链表的头指针,
// ne[i]表示节点i的出边链表上下一条边的编号,
// e[i]表示节点i的出边链表上编号为i的边指向的节点。
// 函数的返回值为bool类型,表示图是否存在拓扑序列。
bool topsort() {
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt) {
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if (-- d[j] == 0)
q[ ++ tt] = j;
}
}
return tt == n - 1;
}
int main() {
cin >> n >> m;
memset(h, -1, sizeof h);
while (m -- ) {
int a, b;
cin >> a >> b;
add(a, b);
d[b] ++ ;
}
if (topsort())
for (int i = 0; i < n; i ++ ) cout << q[i] << ' ';
else puts("-1");
return 0;
}
输入一个无环有向图(输出图的拓扑排序)或者一个有环有向图(输出-1)