多叉树
一、概念
1. 数据结构
一般情况下,我们会将数据结构分为逻辑结构和物理结构,其中逻辑结构是我们的逻辑下存储的结构,而物理结构是计算机的逻辑下存储的结构。数据结构的分类可以参见下表:
2. 树的基础知识
树是一种非线性的数据结构,每个数据都是一对多的关系,由结点和边组成。树是有 n n n( n ≥ 0 n \ge 0 n≥0)个结点组成的具有分支和层次特性的数据集合。
树当中的数据就是 结点。
结点的深度:设根结点的深度为
1
1
1(根据题目),其他根结点深度为其父节点深度加
1
1
1。
结点的高度:设叶结点的高度为
1
1
1(根据题目),其他结点高度为其他子结点高度加
1
1
1。
层次:从根开始,根为第
1
1
1 层,根的子结点为第
2
2
2 层,以此类推。
路径:对于树中两点不同的结点,如果从一个结点出发,自上而下沿着树中连着结点的线段能到达另一个结点,称为它们之间存在着一条路径。
一棵树中,结点最大的度就是 树的度。
一个结点拥有子树的个数称为 结点的度。
具有相同父结点的结点被称为 兄弟结点。
某个结点的直接后继结点被称为该结点的 子结点。
某个结点的直接前驱结点被称为该结点的 父结点。
没有直接前驱,只有直接后继的结点为 根结点。
一棵树中,度为
0
0
0 的结点称为 叶结点。
当
n
>
1
n>1
n>1 的时候,除根结点以外其余结点可以分为
m
m
m 个互不相交的有限集合,其中每个集合本身又是一棵树,这些集合称为这棵树的 子树。
当有
m
m
m(
m
≥
1
m\ge1
m≥1)个树的集合时,称呼其为 森林。
当
n
=
0
n=0
n=0 的时候,树为 空树。
3. 树的相关概念
性质1: 树中的结点数等于所有结点的度之和加
1
1
1,即:
n
=
1
+
n
1
×
1
+
n
2
×
2
+
⋯
n=1+n_1\times1+n_2\times2+\cdots
n=1+n1×1+n2×2+⋯
性质2: 树中的结点树等于所有结点个数总和,即:
n
=
n
0
+
n
1
+
n
2
+
⋯
n=n_0+n_1+n_2+\cdots
n=n0+n1+n2+⋯
性质3: n n n 个结点的树中有 n − 1 n-1 n−1 条边。
4. 树的遍历
- 先序(根)遍历
先遍历根结点,然后遍历左子树和右子树 - 中序(根)遍历
先遍历左子树,再遍历根节点和右子树 - 后序(根)遍历
先遍历左子树,在遍历右子树和根结点 - 层序(根)遍历
一层一层地遍历每一层的所有结点
5. 多叉树
顾名思义,就是很多个分叉的树。后续也会称作 m m m 叉树。
性质1: 在多叉树中,第 k k k 层最多有 m k − 1 m^{k-1} mk−1 个结点( k ≥ 1 k\ge1 k≥1)。
性质2: 多叉树的高度为 h h h,则整个 m m m 叉树最多有 m h − 1 m − 1 \frac{m^h-1}{m-1} m−1mh−1 个结点( h ≥ 1 h\ge1 h≥1)。
性质3: 根据性质2, m h − 1 m − 1 ≥ n \frac{m^h-1}{m-1} \ge n m−1mh−1≥n,那么 m h ≥ n ( m − 1 ) + 1 m^h \ge n(m-1)+1 mh≥n(m−1)+1,因此 h ≥ log m ( n ( m − 1 ) + 1 ) h \ge \log_m(n(m-1)+1) h≥logm(n(m−1)+1)。
二、绘制树
1. 知先中得后
有一棵树:
先序遍历序列为 a b d g c e f h
中序遍历序列为 d g b a e c h f
则它的后序遍历序列为_______________。
首先通过先序找到根 a,然后通过中序找到左子树(d g b)和右子树(e c h f)。
对于左子树(d g b),先序是(b d g),中序是(d g b)。通过先序找到根 b,然后通过中序找到左子树(d g)并且它没有右子树。
然后再看(d g),先序和中序都是(d g)。通过先序找到根 d,然后通过中序找到 d 的右孩子 g。
对于右子树(e c h f),先序确定根是 c,中序确定 c 的左孩子是 e,右子树是(h f),推出 h 是 f 的左孩子。
因此,后序遍历是 g d b e h f c a。
2. 知中后得先
中序遍历序列为 d g b a e c h f
后序遍历序列为 g d b e h f c a
先序遍历序列为_______________。
a ⇒ (dgb, echf)
(dbg): b ⇒ (db, NULL)
(db): d ⇒ (g, NULL)
(echf): c ⇒ (e, hf)
(hf): f ⇒ (h, NULL)
因此,先序遍历是 a b d g c e f h。
3. 知先后猜结点数
【NOIP 2010 普及组 T17】 一颗二叉树的前序遍历是 ABCDEFG,后序遍历是 CBFEGDA,否则结点的左子树的结点个数可能是( A )。
A. 2
B. 3
C. 4
D. 5
我们可以直接用试的方法:
4. 知中层得先
中序遍历 DBEAC
层序遍历 ABCDE
根据层序遍历得定义,那么层序得根结点,即 A 就是这棵树得根结点。根据中序,左子树是(DBE),右子树是(C)。
对于左子树,根据层序得到根结点 B,再根据中序推导出左孩子是 D,右孩子是 E。那么这棵树如下:
三、树的操作
1. 建树
首先,每个结点都可以当作是一个数组中的元素,它有三个信息:自己本身、父结点、左孩子、右孩子。所以我们可以考虑用 struct[]
来实现。
2. 转换
- 找根结点(先序、层序的头 / 后序的尾)
- 存储根结点
- 在中序找到左子树和右子树
- 在左子树重复步骤 1~4,直到碰到叶结点
- 在右子树重复步骤 1~4,直到碰到叶结点
根据上面的步骤,我们直到可以用递归的方法来实现。
四、例题
1. 知中层得先
#include <iostream>
#include <string>
using namespace std;
int num[130]; // 下标: ASCII 元素: 下标
string InOd, LvOd;
void dfs(string InOd)
{
int len = InOd.length(); // 序列长度
if (len == 0) return; // 序列为空,表无子树
// 找根结点
int k; // 中序遍历中根结点的下标
char root; // 根结点
int minn = 1e8; // 最小值
for (int i = 0; i < len; i++)
if (num[InOd[i]] < minn)
{
k = i; // 更新根结点在的下标
root = InOd[i]; // 记录根结点
minn = num[InOd[i]]; // 记录最小值(找根结点核心)
}
// 输出先序遍历
cout << root; // 根结点
dfs(InOd.substr(0, k)); // 左子树
dfs(InOd.substr(k+1)); // 右子树
}
int main()
{
cin >> InOd >> LvOd;
for (int i = 0; i < LvOd.length(); i++)
num[LvOd[i]] = i;
dfs(InOd);
return 0;
}
2. 道路修改
题目描述
在 W 星球上有 n n n 个国家。为了各自国家的经济发展,他们决定在各个国家之间建设双向道路使得国家之间连通。但是每个国家的国王都很吝啬,他们只愿意修建恰好 n − 1 n−1 n−1 条双向道路。
每条道路的修建都要付出一定的费用,这个费用等于道路长度乘以道路两端 的国家个数之差的绝对值。
由于国家的数量十分庞大,道路的建造方案有很多种,同时每种方案的修建费用难以用人工计算,国王们决定找人设计一个软件,对于给定的建造方案,计算出所需要的费用。请你帮助国王们设计一个这样的软件。
输入格式
输入的第一行包含一个整数 n n n,表示 W W W 星球上的国家的数量,国家从 1 1 1 到 n n n 编号。
接下来 n – 1 n–1 n–1 行描述道路建设情况,其中第 i i i 行包含三个整数 a i , b i a_i,b_i ai,bi 和 c i c_i ci ,表示第 i i i 条双向道路修建在 a i a_i ai 与 b i b_ i bi 两个国家之间,长度为 c i c_i ci。
输出格式
输出一个整数,表示修建所有道路所需要的总费用。
输入样例#1
6 1 2 1 1 3 1 1 4 2 6 3 1 5 2 1
输出样例#1
20
数据规模与约定
对于 100 % 100\% 100% 的数据, 1 ≤ a i , b i ≤ n 1\le a_i,b_i\le n 1≤ai,bi≤n, 0 ≤ c ≤ 1 0 6 0\le c\le 10^6 0≤c≤106, 2 ≤ n ≤ 1 0 5 2\le n\le 10^5 2≤n≤105。
我们可以将题目换一种说法:求从第 1 1 1 个结点为根结点的总结点数。
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
int n;
int a, b, c;
int len[100005]; // 每个点到父结点的路径长度
int cnt[100005]; // 每个点为根的结点数
long long ans;
struct Node
{
vector<int> adj; // 邻结点数组
vector<int> w; // 和邻结点的长度差
} tree[100005];
// 计算以 id 为根子树的结点数 cnt[id]
void dfs(int id, int father)
{
cnt[id] = 1; // 自己本身
int len1 = tree[id].adj.size(); // 邻结点数组的长度
for (int i = 0; i < len1; i++) // 遍历 tree[id] 的所有邻结点
{
int son = tree[id].adj[i];
if (son == father) continue; // 碰到叶结点
len[son] = tree[id].w[i]; // 更新长度
dfs(son, id);
cnt[id] += cnt[son]; // 增加结点数
}
}
int main()
{
cin >> n;
for (int i = 1; i < n; i++)
{
cin >> a >> b >> c;
tree[a].adj.push_back(b), tree[a].w.push_back(c);
tree[b].adj.push_back(a), tree[b].w.push_back(c);
}
dfs(1, 0);
for (int i = 2; i <= n; i++)
ans += (long long)len[i]*abs(n-cnt[i]*2);
cout << ans;
return 0;
}
3. 最大子树和
题目描述
小明对数学饱有兴趣,并且是个勤奋好学的学生,总是在课后留在教室向老师请教一些问题。一天他早晨骑车去上课,路上见到一个老伯正在修剪花花草草,顿时想到了一个有关修剪花卉的问题。于是当日课后,小明就向老师提出了这个问题:
一株奇怪的花卉,上面共连有 N N N 朵花,共有 $N−14 条枝干将花儿连在一起,并且未修剪时每朵花都不是孤立的。每朵花都有一个“美丽指数”,该数越大说明这朵花越漂亮,也有“美丽指数”为负数的,说明这朵花看着都让人恶心。所谓“修剪”,意为:去掉其中的一条枝条,这样一株花就成了两株,扔掉其中一株。经过一系列“修剪“之后,还剩下最后一株花(也可能是一朵)。老师的任务就是:通过一系列“修剪”(也可以什么“修剪”都不进行),使剩下的那株(那朵)花卉上所有花朵的“美丽指数”之和最大。
老师想了一会儿,给出了正解。小明见问题被轻易攻破,相当不爽,于是又拿来问你。
输入格式
第一行一个整数 N N N( 1 ≤ N ≤ 16000 1≤N≤16000 1≤N≤16000)。表示原始的那株花卉上共 N N N 朵花。
第二行有 N N N 个整数,第III个整数表示第III朵花的美丽指数。
接下来 N − 1 N−1 N−1 行每行两个整数 a , b a,b a,b,表示存在一条连接第 a a a 朵花和第 b b b 朵花的枝条。
输出格式
一个数,表示一系列“修剪”之后所能得到的"美丽指数"之和的最大值。保证绝对值不超过 2147483647 2147483647 2147483647。
输入样例1
7 -1 -1 -1 1 1 1 0 1 4 2 5 3 6 4 7 5 7 6 7
输出样例1
3
数据说明
对于 100 % 100\% 100% 的数据,有 N ≤ 16000 N≤16000 N≤16000。
#include <iostream>
#include <vector>
using namespace std;
int n;
int ans = 0xc0c0c0c0;
struct Node
{
vector<int> adj;
int w;
int max_w; // 以当前结点为根的子树的最大美丽指数
} tree[100005];
void dfs(int id, int father)
{
tree[id].max_w = tree[id].w;
for (int son : tree[id].adj)
{
if (son == father) continue;
dfs(son, id);
if (tree[son].max_w > 0) tree[id].max_w += tree[son].max_w;
}
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> tree[i].w;
for (int i = 1, u, v; i < n; i++)
{
cin >> u >> v;
tree[u].adj.push_back(v);
tree[v].adj.push_back(u);
}
dfs(1, 0);
for (int i = 1; i <= n; i++) ans = max(ans, tree[i].max_w);
cout << ans;
return 0;
}