树 基本操作 直通车

树的概念

树的定义

树是一种重要的数据结构,本质上是一种特殊的图。每一个结点可能有多个或者没有子结点,但是除了根节点外每一个节点都必须有一个父节点。因为这一个特性,点数为 \(n\) 的树会有 \(n-1\) 条边。

基本术语

  1. 一个结点的孩子个数叫做这个结点的度;
  2. 假如有一个结点 \(K\),那么结点 \(K\) 到根结点上的所有结点都叫做结点 \(K\) 的祖先;
  3. 结点的层次定义为根结点到当前结点的长度;
  4. 度为 \(0\) 的结点称之为叶子结点,度不为 \(0\) 的结点称之为分支结点;

树的存储

双亲表示法

记录每一个结点的父亲,假设第 \(i\) 个结点的父亲为 \(f_i\)。这种结构的好处是可以方便的从下往上进行遍历,并且占用空间小,但是由于只记录了父亲,因此很多操作都无法完成,只能在特定的环境下使用。比如并查集。

邻接矩阵

如果这棵树没有边权,那么我们就使用一个数组 \(f\) 来记录,如果 \(f_{(i,j)}\)\(1\),那么说明 \(i\)\(j\) 有边。这种做法十分简单,但是空间十分大。树是一个稀疏图,当 \(n\) 很大的时候其实边数只有 \(n-1\),因此大多数情况也不会这样写。

邻接表

我们直接存储一个结点可以到那些结点,那么就需要 \(n\) 个动态数组,可以使用 vector 或者链表。这种存储结构是最适合树的,应用场景最广,大部分题目都可以使用邻接表来进行存储。

vector<int> e[kMaxN];
e[u].push_back(v);  // u 加一条边到 v

复杂度对比

假设点的数量是 \(n\),树的高度为 \(k\)

项目 双亲表示法 邻接矩阵 邻接表
空间复杂度 \(\mathcal O(n)\) \(\mathcal O(n^2)\) \(\mathcal O(n)\)
查询 \(u\)\(j\) 是否有边 \(\mathcal O(n)\) \(\mathcal O(1)\) \(\mathcal O(n)\)
遍历整棵树 \(\mathcal O(n^2)\) \(\mathcal O(n^2)\) \(\mathcal O(n)\)
从下往上遍历 \(\mathcal O(k)\) \(\mathcal O(nk)\) 原版 \(\mathcal O(nk)\),魔改版 \(\mathcal O(k)\)

可以看出,邻接表在大部分情况下复杂度都是否优秀,所以这就是为什么邻接表会在树的存储中大受欢迎的原因。以下的程序都使用邻接表来实现的。

树的遍历

深度优先

遍历完当前结点就递归遍历所有的子结点,可以使用 dfs 实现。注意如果存了双向边的话,其实不需要开标记数组,我们可以在递归的使用使用一个变量 \(f\) 来记录当前结点的父亲,这样子就不会遍历到重复的点。

void dfs(int x, int f) {
  cout << x << ' ';
  for (int i : e[x]) {
    if (i != f) {
      dfs(i, x);
    }
  }
}

广度优先

每次遍历一整层,其实就是广度优先搜索,使用队列实现。

queue<pair<int, int>> q;

void Record(int lst, int f, int x) {
  if (x == f) {
    return;
  }
  q.push({x, lst});
}

void Record(int st) {
  for (q.push({st, 0}); q.size(); q.pop()) {
    cout << q.front().first << ' ';
    for (int i : e[q.front().first]) {
      Record(q.front().first, q.front().second, i);
    }
  }
}

简单树上问题

树的直径

定义

树的直径其实就是两点之间的最长距离。

思路

我们从根节点开始跑 dfs,可以求出根节点到任意结点的距离,然后我们挑选一个距离最远的点,从这个点开始再跑一遍 dfs,最后遍历所有的点求出最大值。这种方法适合没有负权边的树,还有一种方法是树上 dp,不过过于复杂暂时不讲。

代码

void dfs(int x, int f) {
  for (auto i : e[x]) {
    if (i.first != f) {
      d[i.first] = d[x] + i.second;
      dfs(i.first, x);
    }
  }
}

int main() {
  cin >> n;
  for (int i = 1, u, v, w; i < n; i++) {
    cin >> u >> v >> w;
    e[u].push_back({v, w});
    e[v].push_back({u, w});
  }
  dfs(1, 0);
  int id = max_element(d + 1, d + n + 1) - d;
  fill(d + 1, d + n + 1, 0);
  dfs(id, 0);
  cout << *max_element(d + 1, d + n + 1) << '\n';
  return 0;
}

树的重心

定义

若一个点 \(p\) 的所有儿子的 / 子树的大小 / 当中的最大值 / 小于等于 \(n\div 2\),那么 \(p\) 就是这棵树的重心。

思路

我们直接模拟这个过程。若当前点为 \(i\),那么 \(i\) 的整个子树的大小就是 \(sz_i\),儿子的子树的最大大小就是 \(mx_i\)。我们遍历邻边,递归处理之后将大小加到当前子树的大小上,然后取最大值就行了。注意一棵树的重心可能有多个,而题目中要求排序。

代码

void dfs(int x, int f) {
  sz[x] = 1, mx[x] = 0;
  for (int i : e[x]) {
    if (i != f) {
      dfs(i, x);
      sz[x] += sz[i];
      mx[x] = max(mx[x], sz[i]);
    }
  }
  mx[x] = max(mx[x], n - sz[x]);
  if (mx[x] <= n / 2) {
    ans.push_back(x);
  }
}

int main() {
  cin >> n;
  for (int i = 1, u, v; i < n; i++) {
    cin >> u >> v;
    e[u].push_back(v);
    e[v].push_back(u);
  }
  dfs(1, 0);
  sort(ans.begin(), ans.end());
  for_each(ans.begin(), ans.end(), [](int x) {
    cout << x << ' ';
  });
  return 0;
}

表达式树

题目

给定一个后缀表达式,求中缀表达式。

思路

我们可以使用一个树来存储。比如 1 2 * 2 + 4 5 + * 可以看成如下的图片:

而我们只需要进行中序遍历就行了。至于如何建立这棵树——每一次的操作都对应了两个操作数,因此使用一个简简单单的栈结构就行了。

代码

#include <iostream>
#include <vector>

using namespace std;

struct Node {
  int l, r, c;

  Node(int l, int r, int c) : l(l), r(r), c(c) { }
};

int n, x;
char c;
vector<Node> t;
vector<int> v;

void dfs(int x) {
  if (t[x].l == -1) {
    cout << t[x].c;
  } else {
    cout << '(';
    dfs(t[x].l);
    cout << ' ' << char(t[x].c) << ' ';
    dfs(t[x].r);
    cout << ')';
  }
}

int main() {
  cin >> n;
  for (int i = 1; i <= n; i++) {
    cin >> c;
    if (isdigit(c)) {
      cin.putback(c) >> x;
      t.push_back(Node(-1, -1, x));
      v.push_back(t.size() - 1);
    } else {
      int r = v.back();
      v.pop_back();
      int l = v.back();
      v.pop_back();
      t.emplace_back(l, r, c);
      v.push_back(t.size() - 1);
    }
  }
  dfs(t.size() - 1);
  return 0;
}

二叉树

二叉树的概念

二叉树的定义

顾名思义,二叉树就是任意一个结点最多只有 \(2\) 个儿子结点,其实也非常简单,只是多了一些特殊性质。

特殊的二叉树

斜树

所有结点都只有左儿子,就叫做左斜树;所有结点都只有右儿子,叫做右斜树。这俩玩意都统称为斜树。

满二叉树

有一颗高度为 \(k\) 的二叉树,若结点数正好为 \(2^k-1\) 个,那么这棵树就叫做满二叉树。也就是除了叶子结点的结点,每一个结点都正好有两个儿子。那么对于一个结点 \(p\),结点 \(p\) 的左儿子就可以看作 \(2\times p\),右儿子就可以看作 \(2\times p+1\),父亲就可以看作 \(\lfloor \dfrac{p}{2}\rfloor\)

完全二叉树

假如有一颗树,每一个结点的编号都与满二叉树上的同样位置的结点对应,那么这棵树就叫做完全二叉树。

二叉树的性质

  1. 若结点数量为 \(n\),那么边的数量为 \(n-1\)
  2. \(k\) 层的结点树最多有 \(2^{k-1}\) 个结点,高度为 \(k\) 的二叉树最多有 \(2^k-1\) 个结点,也就是 \(\sum\limits_{i=0}^{k-1}2^k\)
  3. 假如一颗完全二叉树的结点数量为 \(n\),那么这颗完全二叉树的高度为 \(\lfloor\log_2 n\rfloor +1\)

二叉树的遍历

遍历方法

前序遍历

每次先遍历自己,再遍历左儿子,最后遍历右儿子。

void dfs(int x) {
  cout << x << ' ';
  if (hasLeft(x)) {
    dfs(left(x));
  }
  if (hasRight(x)) {
    dfs(right(x));
  }
}

中序遍历

先遍历左儿子,再遍历当前结点,最后遍历右儿子。

void dfs(int x) {
  if (hasLeft(x)) {
    dfs(left(x));
  }
  cout << x << ' ';
  if (hasRight(x)) {
    dfs(right(x));
  }
}

后序遍历

先遍历左右儿子,最后遍历当前结点。

void dfs(int x) {
  if (hasLeft(x)) {
    dfs(left(x));
  }
  if (hasRight(x)) {
    dfs(right(x));
  }
  cout << x << ' ';
}

层序遍历

遍历顺序转换

前序+中序 转 后序

posted @ 2023-12-02 17:35  haokee  阅读(43)  评论(0)    收藏  举报