1、树的基础
1、树的定义
树是一种特殊的图,它必须为连通图,且每个结点最多只能有一个父结点。下图展示了一棵树
1 (2和3的父结点)
/ \
2 3(1的子结点)
树还分为有根树和无根树,有根树就是有根结点的树,而无根树就是没有根结点的树。
2、关于树的专业名词
1、父结点:(直接看图)。
2、子结点:(直接看图)。
1、根结点:指的是一棵树唯一一个没有父结点的结点。
2、叶结点:指的是一棵树上没有子结点的结点。
4、兄弟结点:指的是在同一深度的结点。
5、深度:指的是与根结点的距离,但有时是与根结点的距离加1。
6、高度:树的最大深度。
7、度:指的是一个结点的父结点与子结点数量之和。
2、树的操作
1、树的存储
树可以用vector数组来存储,vec[i]表示结点i的所有子结点,这被称为父亲表示法。还有一种方法,就是用一个数组存储每一个结点的父结点,这被称为孩子表示法。而两种方法一起用称为父亲孩子表示法。还有一种特殊的方法,称为兄弟表示法,存储每一个结点的兄弟结点,但是这种方法并不常用。最后还有两种图的存储方法,分别是邻接矩阵和邻接表,树主要使用邻接矩阵进行存储,邻接表我们暂时不讨论。
2、树的基本操作
1、遍历(dfs)树
树的遍历其实没有什么特殊的,就是依次遍历一个结点所有的孩子结点,没有就回溯。然后不用记录走过的点,因为到达一个结点的方法只有一种。
示例代码1(父亲表示法):
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 1;
int n, root;
vector<int> vec[maxn];
void dfs(int x) {
cout << x << ' ';
for (v : vec[x]) dfs(v); // 这里用了-based for循环,也可以用普通的for循环
}
int main() {
int i;
cin >> n;
int t;
for (i = 1; i <= n; ++i) {
cin >> t;
vec[t].push_back(i);
/* 如果是无根树需要加上以下语句
vec[i].push_back(t); */
}
/* 如果是无根树需要加上以下语句
cin >> root; */
dfs(1 /*如果是无根树需要改成root*/);
}
示例代码2(邻接矩阵):
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 1;
int n, root;
int tree[maxn][maxn];
void dfs(int x, int f) {
cout << x << ' ';
for (i = 1; i <= n; ++i) {
if (i != f) dfs(i, x);
}
}
int main() {
int i;
cin >> n;
int x, y;
for (i = 1; i < n; ++i) {
cin >> x >> y;
tree[x][y] = 1;
/* 如果是无根树,那么还需要加下面的语句
tree[y][x] = 1; */
}
/* 如果是无根树就加上以下语句
cin >> root; */
dfs(1 /* 如果是无根树改为root */, 0 /* 邻接矩阵需要传入父结点参数才能遍历 */);
return 0;
}
2、求树的高度
事先声明,我们这里默认根结点的深度为1。
树的高度本质上就是树中深度最大的结点的深度。而求深度需要先遍历树,深度可以用数组来记录。对于一个结点i来说,它的深度就等于它的父结点的深度加1,但是还有一个特殊的结点,那就是根结点,它没有父结点,那求它的深度就需要给它假想一个不存在的父结点,而这个假想的父结点的深度为0,这样就可以解决这个问题了。
示例代码1(父亲孩子表示法):
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 1;
int n, ma;
int dep[maxn];
vector<int> vec;
void dfs(int x, int f) {
dep[x] = dep[f] + 1;
for (int v : vec[x]) dfs(v, x);
}
int main() {
int i;
cin >> n;
int t;
for (i = 1; i <= n; ++i) {
cin >> t;
vec[t].push_back(i);
// 如果是无根树需要反向建边,代码略
}
// 如果是无根树需要输入根结点,而且下面的参数要改
dfs(1, 0); // 这里默认根结点的深度为1,而且需要传入父结点参数
// 打擂台求最大深度
for (i = 1; i <= n; ++i) ma = max(ma, dep[i]);
cout << ma;
return 0;
}
示例代码2(邻接矩阵):
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 1;
int n, ma;
int dep[maxn];
bool p[maxn][maxn];
void dfs(int x, int f) {
dep[x] = dep[f] + 1;
for (i = 1; i <= n; ++i) {
if (i != f) dfs(i, x);
}
}
int main() {
int i;
cin >> n;
int x, y;
for (i = 1; i < n; ++i) {
cin >> x >> y;
p[x][y] = 1;
// 如果是无根树需要反向建边,代码略
}
// 如果是无根树需要输入根结点,而且下面的参数要改
dfs(1, 0); // 这里默认根结点的深度为1,而且需要传入父结点参数
// 打擂台求最大深度
for (i = 1; i <= n; ++i) ma = max(ma, dep[i]);
cout << ma;
return 0;
}
3、求子树大小
求子树大小需要给函数添加返回值,返回的就是子树大小。一个结点的子树大小就等于它所有的子结点的子树大小之和加1,这个1就是自己。而叶结点没有子结点,所以子树大小就为初始值1。而它返回到上一层继续上面的操作,直到求出根结点的子树大小。子树大小可以用一个数组来记录。
示例代码1(父亲表示法):
#include<bits/stdc++.h>
using namespace std;
const int manx = 1e3 + 1;
int n;
int siz[maxn];
vector<int> vec[maxn];
int dfs(int x) {
// 子树大小最小为1
siz[x] = 1;
for (v : vec[x]) siz[x] += dfs(v);
return siz[x];
}
int main() {
int i;
cin >> n;
int t;
for (i = 1; i <= n; ++i) {
cin >> t;
vec[t].push_back(i); // 以后省略提示
}
cout << dfs(1);
return 0;
}
示例代码2(邻接矩阵):
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 1;
int n;
int siz[maxn];
bool p[maxn][maxn];
int dfs(int x, int f) {
int i;
siz[x] = 1; // 子树大小最小为1
for (i = 1; i <= n; ++i) {
if (i != f) siz[x] += dfs(i, x);
}
return siz[x];
}
int main() {
int i;
cin >> n;
int x, y;
for (i = 1; i < n; ++i) {
cin >> x >> y;
p[x][y] = 1;
}
cout << dfs(1, 0);
return 0;
}
4、求父结点
这个非常简单,就用一个数组记录一个结点的父结点就行了。
示例代码1(父亲孩子表示法):
#include<bits/stdc++.h>
using namespace std;
int n;
vector<int> vec[100001];
int fa[100001];
void dfs(int x, int p) {
fa[x] = p;
for (auto v : vec[x]) {
if (v != p) dfs(v, x);
}
}
int main() {
int i;
cin >> n;
int x, y;
for (i = 1; i < n; ++i) {
cin >> x >> y;
vec[x].push_back(y);
vec[y].push_back(x);
}
dfs(1, 0);
for (i = 1; i <= n; ++i) cout << fa[i] << ' ';
return 0;
}
示例代码2(邻接矩阵):
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 1;
int n;
int fa[maxn];
bool a[maxn][maxn];
void dfs(int x, int f) {
int i;
fa[x] = f;
for (i = 1; i <= n; ++i) {
if (a[x][i] && i != f) dfs(i, x);
}
}
int main() {
int i;
cin >> n;
int x, y;
for (i = 1; i < n; ++i) {
cin >> x >> y;
a[x][y] = 1;
}
dfs(1, 0);
for (i = 1; i <= n; ++i) cout << fa[i] << ' ';
return 0;
}
3、树的进阶操作
1、最近公共祖先(LCA)问题
最近公共祖先可以通过爬树操作实现,爬树就是让表示位置的变量编程它所表示的结点的父结点的位置。首先要判断第二个结点的深度是不是大于第一个结点的深度,如果大于需要交换两个结点。然后使第一个结点不停进行爬树操作直到第一个结点的深度等于第二个结点的深度。然后再让两个结点一起爬树。直到两个结点爬到一个相同的结点,然后这个结点就是这两个结点的最近公共祖先。我这么说可能有一点抽象,看代码就知道了。
(这里我就不写父亲孩子表示法的示例了,相信大家应该已经懂这两种方法怎么转换了)
示例代码:
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 1;
int n, a, b;
int dep[maxn], fa[maxn];
bool p[maxn][maxn];
void dfs(int x, int f) {
int i;
fa[x] = f;
dep[x] = dep[f] + 1;
for (i = 1; i <= n; ++i) {
if (p[x][i] && i != f) dfs(i, x);
}
}
int main() {
int i;
cin >> n;
int x, y;
for (i = 1; i < n; ++i) {
cin >> x >> y;
p[x][y] = 1;
p[y][x] = 1; // 必须进行反向建边才能爬树
}
cin >> a >> b;
dfs(1, 0); // 先要求出深度和每一个结点的父结点,才能进行爬树操作
if (dep[b] > dep[a]) swap(a, b);
// A结点爬树
while (dep[a] > dep[b]) a = fa[a];
// AB结点一起爬树
while (a != b) a = fa[a], b = fa[b];
cout << a;
return 0;
}
2、树的直径
我们想要求树的直径首先要知道它的定义,树的直径就是树中最远的两个点的距离。而暴力求直径的复杂度是O(n²),有时候会超时。所以我们需要一个复杂度比O(n²)小的方法。如果想要快速求直径,就需要知道一个性质,任意一点a对应的最远点一定是树的直径的端点之一。然后再求这个端点的最远点与这个端点之间的距离,就是树的直径。而这个方法从理论上来说是O(n)复杂度的,可行。
// 为了好展示复杂度的优势,这里的数据非常大,用邻接矩阵存不了,所以只能用vector
#include<bits/stdc++.h>
using namespace std;
int n, ma, ma_p;
vector<int> vec[100001];
int dep[100001];
void dfs(int x, int p, int d) {
if (ma < d) {
ma = d;
ma_p = x;
}
for (auto v : vec[x]) {
if (v != p) dfs(v, x, d + 1);
}
}
int main() {
int i;
cin >> n;
int x, y;
for (i = 1; i < n; ++i) {
cin >> x >> y;
vec[x].push_back(y);
vec[y].push_back(x);
}
dfs(1, 0, 0);
ma = 0;
dfs(ma_p, 0, 0);
cout << ma;
return 0;
}
3、树的重心
树的重心就是一个特殊的结点,这个结点与其它结点的距离之和最大。这个地方有一个技巧,那就是树的重心的子树大小一定小于半棵树的大小。所以我们可以算出每一个结点的子树大小,然后就可以计算了。但是注意,剩下的不是重心的子树的结点也要被算作另一个子树,而两者取最大值计算。最后再求这个离这个结点最远的结点就行了。
#include<bits/stdc++.h>
using namespace std;
int n, ans = -1;
vector<int> vec[100001];
int siz[100001];
long long sum;
void dfs1(int x, int p) {
// cout << x << ' ';
siz[x] = 1;
int ma = 0;
for (auto v : vec[x]) {
if (v != p) {
dfs1(v, x);
siz[x] += siz[v];
ma = max(ma, siz[v]);
}
}
ma = max(ma, n - siz[x]);
if (ma <= n / 2) {
if (ans == -1 || ans > x) ans = x;
}
}
void dfs2(int x, int p, int d) {
// cout << x << ' ';
sum += d;
for (auto v : vec[x]) {
if (v != p) dfs2(v, x, d + 1);
}
}
int main() {
int i;
cin >> n;
int x, y;
for (i = 1; i < n; ++i) {
cin >> x >> y;
vec[x].push_back(y);
vec[y].push_back(x);
}
dfs1(1, 0);
dfs2(ans, 0, 0);
cout << sum;
return 0;
}
3、总结
树是一种特殊的数据结构,我们可以利用它解决很多问题。我们以后还会学习更多关于树的内容。