树是特殊的图,无向图是将有向图两条有向边的有向图
存储有向图有:邻接矩阵 和 邻接表的方法
树的存储
我们采用邻接表的方法
// 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
int h[N], e[N], ne[N], idx;
// 添加一条边a->b
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
// 初始化
idx = 0;
memset(h, -1, sizeof h);
树的深度优先遍历
时间复杂度O(n + m), n 表示点数, m 表示边数
DFS:
模板:
int dfs(int u)
{
st[u] = true; // st[u] 表示点u已经被遍历过
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j]) dfs(j);
}
}
/*
树的存储,与树的深度优先遍历
无向图将有向图边方向相反
*/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010, M = N * 2;// 临接表的一个链可以连接全部结点,有相反两个方向所以翻倍
int h[N], e[M], ne[M], idx = 0;// h[k] 是第k个节点,e[k]是他相连的边,ne[k]是指向它相连边的指针,idx是当前用到哪个空闲空间
bool st[N];// 这个节点是否走过
int ans = N; //因为要找最小值,初始为最大
int n;
void add (int a, int b)// 插入到开头
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
// 求以k结点为根的节点的树的结点个数
int dfs (int k)
{
st[k] = true;
int sum = 1, size = 0;// sum包含此节点的整个数的结点数量,size是这个节点连接的子树最大节点数
for (int i = h[k]; ~i; i = ne[i])// 遍历与h[k] 相连的所有子树
{
int j = e[i];//求得与h[k]相连的所有子树的结点s
if (st[j]) continue;
int s = dfs(j);
sum += s;// sum用来求k节点上面的所有节点数
size = max (s, size);
}
size = max (n - sum, size);
ans = min (size, ans);
return sum;
}
int main()
{
memset(h, -1, sizeof h);
cin >> n;
for (int i = 0; i < n; i ++)
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b), add(b, a);
}
dfs (1);// 无向图中各个结点是向联通的任意从一个节点都可以走完整个图
printf("%d", ans);
return 0;
}
BFS:
模板:
queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);
while (q.size())
{
int t = q.front();
q.pop();
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true; // 表示点j已经被遍历过
q.push(j);
}
}
}
/*
数的存储和宽度优先搜索
模板:队列,while循环, 队列初始化
*/
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010;
int h[N], e[N], ne[N], idx = 0;
int d[N];// 在深度优先遍历中用来st[N] 来存节点是否走过但是在宽度优先遍历中由于要存上一个点的距离所以用d[N]来存
// 可以将d[N]初始化为空-1用于判断是否走过
int n, m;
void add (int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
int bfs (int k)
{
memset(d, -1, sizeof d);
queue<int> q;
q.push(k);
d[k] = 0;// 注意这里初始化填k不然对不上号走不出循环
while (q.size ())
{
int t = q.front();
q.pop();// 用过及时弹出
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (d[j] == -1)
{
d[j] = d[t] + 1;//由于j表示与t相连的边,i表示t在数组中的位置两者有所区别
q.push(j);
}
}
}
return d[n];
}
int main ()
{
memset(h, -1, sizeof h);
cin >> n >> m;
for (int i = 0; i < m; i ++)
{
int a, b;
scanf("%d%d", &a, &b);
add (a, b);// 有向边只增添一条
}
printf("%d", bfs(1));
return 0;
}
BFS的应用:
拓扑排序:
拓扑序列:拓扑序列是指对有向图来说
(1)每个顶点出现且只出现一次。
(2)若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
是一个无环图,有向无环图也称为拓扑图
/*
拓扑排序使用的是将入度为零的结点先添加到队列,然后一次访问这些结点的下一个节点,遍历这些结点的时候就弹出
这样下一个节点就是入度为零的结点了,将它的入度减1,然后入队
拓扑序列就只需要一次将这些入度为零的结点添加到答案序列即可
*/
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <vector>
using namespace std;
const int N = 100010;
int h[N], e[N], ne[N], idx = 0;// 图的存储模板
//需要用入度判断
int in[N]; //由于是通过入度访问结点所以就不用判读是否访问的结点,因为访问的已经删除了
vector<int> ans;
int n, m;// 节点个数n, 边的个数m
void add (int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
void topsort ()
{
queue<int> q;
for (int i = 1; i <= n; i ++)// 要遍历每一个结点,而不是一个邻接表
if (!in[i]) q.push(i); // 将所有入度为零的结点入队
while (q.size())
{
int t = q.front();
ans.push_back(t);
q.pop();
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
in[j] --;// 前一个节点被弹出,入度减1
if (!in[j]) q.push(j);
}
}
}
int main ()
{
memset(h, -1, sizeof h);
cin >> n >> m;
for (int i = 0; i < m; i ++)
{
int a, b;
scanf ("%d%d", &a, &b);
add (a, b);
in[b] ++; // 啊,忘记插入的时候入度初始化了
}
topsort();
if (ans.size() < n) puts("-1");
else {
for (auto item : ans)
printf("%d ", item);
}
return 0;
}