树的概念
树的逻辑结构是树形结构,和我们之前的线性结构又不太一样了,是一种一对多的关系
树的结点分为根节点,叶子结点(没有分支的结点) 以及分支结点
从上往下看,每个结点都有0个或多个后继
从下往上看,每个结点除了根结点以外都有一个前驱结点
所以每个结点都有一条边去连接前驱结点,而根结点没有边连接前驱结点,所以结点数=边数+1(除了根结点每个结点都有一条边连接前驱结点,再加上根结点就是结点的个数)
如图,从下往上看,除了根结点,每个结点都有一个前驱结点,也就是对应一条边。所以结点数=边数(9)+1 = 10
关于树的一些相关术语
父结点:直接前驱,根结点没有父结点
孩子结点:直接后继,叶子结点没有直接后继
结点的度:该结点孩子的个数,比如上图A结点的度就是3
树的度:所有结点的度的最大值
树的高度:一共有多少层,图中有四层
两个结点之间的路径:两个结点之间的最短路径
路径长度:两点的路径中,边的个数
有序树:结点的子树顺序按从左到右有序,不能随意更改
无序树:结点的子树顺序是无序的,随便变,没事儿
我们竞赛一般用的都是无序树
有根树:根是固定的
无根树:根是不固定的
(无根树会导致父子关系不明确,我们要把所有清空都存储起来,比如假如a和b之间存在一条边,那我们既要把a存在b的孩子里,也要把b存在a的孩子里)
我们竞赛中用的都是无根树
树的存储
关于树的存储方式有很多,我们本篇文章只学孩子表示法,未来我们的并查集里会学到双亲表示法,竞赛里,我们只需要掌握这两种表示方法就ok了
孩子表示法:对于每个结点,把他所有的孩子全部存起来
因为我们竞赛都是无根树,我们需要把所有情况都考虑到
用vector数组实现孩子表示法
我们竞赛里,一般都是这样给信息
我们首先要创建一个大小充足 的vector的数组,把每个结点的孩子都各自存储在各自的vector里面
如图,我们九个结点,我们就创建一个大小为10的vector数组,下标为0的vector数组我们不用
cpp
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
vector <int> v1[N];
int main()
{
int n;
cin >> n;
}
比如我们看第一个输入的是3和1,那么3和1之间有一条边,我们竞赛都是无根树,所以我们把3当成1的孩子存一次,把1当成3的孩子存一次
1的孩子结点:2,3,4
2的孩子结点:1,5,6
3的孩子结点:1
4的孩子结点:1,7,8,9
5的孩子结点:2
6的孩子结点:2
7,8,9的孩子结点:4
存储时候的代码
cpp
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
vector <int> v1[N];
int main()
{
int n;
cin >> n;
for (int i = 1; i < n; i++)
{
int x, y; cin >> x >> y;
v1[x].push_back(y);
v1[y].push_back(x);
}
}
用链式前向星实现孩子表示法
链式前向星就是用链表来存储所有的孩子
我们需要创建一个数组来存储每个结点的一个哨兵位,
然后创建一个哨兵位数组2倍的数组来存储完成e[2*N]和ne[2*N]的数组,还要有id表示存储位置(这里是两倍,因为我们有的结点可能不止一个边,那么就有可能存储两次)
我们实现的时候就是用头插来实现的
比如这个图,我们3和1有一条边,那我们就要把3的哨兵位连接一个1,把1结点的哨兵位连接一个3,代码也就是:id++,e[id] = 1,ne[id]=h[3] h[3] = id,这是把1连接到3的哨兵位,3连接1同理,我们暂且不实现,3和4有一条边,我们把4连接到3的哨兵位的后面,同样是采取头插,id++,e[id]=4,ne[id]=h[3],h[3]=id,这样我们的3结点哨兵位就变成了 头->4->1了,也就是3的两个孩子
实现完整代码如下
cpp
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
int h[N], e[2 * N], ne[2 * N], id;
void add(int x, int y)
{
id++;
e[id] = x;
ne[id] = h[y];
h[y] = id;
}
int main()
{
int n;
cin >> n;
for (int i = 1; i < n; i++)
{
int x, y; cin >> x >> y;
add(x, y); add(y, x);
}
}
树的遍历(DFS和BFS)
DFS,即深度优先遍历,英文叫做Depth First Search
是一种遍历树或者图的一种算法,所谓深度优先遍历,就是每次都尝试往更深的结点走
也就是一条路走到黑
从根结点开始,遍历根结点的孩子,然后再以这个孩子为根遍历它的孩子,所以我们可以写成一种递归的形式
vector存储的树进行dfs
cpp
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e6 + 10;
vector <int> edges[N];
bool st[N];
void dfs(int u)
{
cout << u << " ";
st[u] = true;
for (auto v : edges[u])
{
if (!st[v])
dfs(v);
}
}
int main()
{
int n;
cin >> n;
//建树
for (int i = 1; i < n; i++)
{
int a, b; cin >> a >> b;
edges[a].push_back(b);
edges[b].push_back(a);
}
//深度优先搜索
dfs(1);
return 0;
}
过程就是不断的遍历根结点的孩子,不断的遍历根结点的孩子,我们来画一下递归过程图
递归的展开图就是一棵树,递归就是对树进行深度搜索
为了更好的理解递归,我们再画一下斐波那契数列的递归展开图
可以看到,斐波那契算法的递归图画出来也是一棵树
链式前向星存储的树进行dfs
cpp
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int h[N], e[2*N], ne[2*N], id;
bool st[N];
void add(int a, int b)
{
id++;
e[id] = b;
ne[id] = h[a];
h[a] = id;
}
void dfs(int u)
{
cout << u << " ";
st[u] = true;
for (int v = h[u]; v; v = ne[v])
{
if (!st[e[v]])
{
dfs(e[v]);
}
}
}
int main()
{
int n;
cin >> n;
for (int i = 1; i < n; i++)
{
int a, b; cin >> a >> b;
add(a, b); add(b, a);
}
dfs(1);
return 0;
}
vector存储的树进行bfs
bfs的话,我们的思路就是用队列,我们先把根结点入队列,然后出队列的时候要把该结点的孩子入队列,直到队列为空,搜索完毕
比如这个,我们先把1入队列,然后把输出1并把1出队列,把3,5,2入队列,然后把输出3并把3出队列,把3的孩子7,10带进去,
出5,把4带进去,出2把11带进去,这时候我们的队列里就是7,10,4,11了 我们输出了1,3,5,2 接下来也是这种操作,直到队列为空的时候我们遍历完毕
下面实现我们的代码
cpp
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
const int N = 1e5 + 10;
queue <int> q;
vector <int> edges[N];
bool st[N];
void bfs()
{
q.push(1);
while (q.size())
{
auto t = q.front();
cout << t << " ";
st[t] = true;
q.pop();
for (auto e : edges[t])
{
if (!st[e])
{
q.push(e);
st[e] = true;
}
}
}
}
int main()
{
int n;
cin >> n;
for (int i = 1; i < n; i++)
{
int a, b; cin >> a >> b;
edges[a].push_back(b);
edges[b].push_back(a);
}
bfs();
}
符合我们的树,结束
链式前向星存储的树进行bfs
cpp
#include <iostream>
#include <queue>
using namespace std;
const int N = 1e5 + 10;
int h[N], ne[2 * N], e[2 * N], id;
bool st[N];
void add(int a,int b)
{
id++;
e[id] = b;
ne[id] = h[a];
h[a] = id;
}
queue <int> q;
void bfs()
{
q.push(1);
while (q.size())
{
auto t = q.front(); q.pop();
cout << t << " ";
st[t] = true;
for (int i = h[t]; i; i = ne[i])
{
if (!st[e[i]])
{
q.push(e[i]);
st[e[i]] = true;
}
}
}
}
int main()
{
int n;
cin >> n;
for (int i = 1; i < n; i++)
{
int a, b; cin >> a >> b;
add(a, b); add(b, a);
}
bfs();
return 0;
}