树学习笔记

引用自 树基础

定义:

二叉树 Binary Tree:

递归定义:
二叉树要么为空,要么由根结点root ,左子树left subtree ,右子树right subtree 组成,而左子树和右子树分别是一棵二叉树。

void build(int root){
build(root<<1); //left subtree
build(root<<1|1); //right subtree
...
}

树学习笔记_第1张图片
完整二叉树(full/proper binary tree):每个结点的子结点数量均为 0 或者 2 的二叉树。换言之,每个结点或者是树叶,或者左右子树均非空。

完全二叉树(complete binary tree):只有最下面两层结点的度数可以小于 2,且最下面一层的结点都集中在该层最左边的连续位置上。

完美二叉树(perfect binary tree):所有叶结点的深度均相同的二叉树称为完美二叉树。

树和二叉树相似,区别在于每个结点不一定只有两棵子树。
一些名词解释
叶结点:没有子树的结点
子结点:一个结点含有的子树的根结点称为该结点的子结点
父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点
深度:定义一棵树的根结点深度为1,其他节点的深度是其父结点深度加1。一棵树中所有结点的深度的最大值称为这棵树的深度。
森林:由m(m >= 0)棵互不相交的树的集合称为森林;

应用

二叉树的编号

对于一个结点k,其左子结点、右子结点的编号分别是2k和
2k + 1 (从1开始编号)

二叉树的层次遍历

BFS

二叉树的递归遍历

先序遍历
中序遍历
后序遍历

二叉搜索树

Binary Search Tree , BST,也叫排序二叉树
是一个数据结构,它的每个结点都保存着一个可以比较大小的的东西,并且对于任意结点u,u的左子树中的所有结点(如果存在)都比根节点小,u的右子树的所有结点都比根结点大。
支持3种基本操作:插入,删除,查找。

根据定义,查找过程可以从根开始递归进行。假定要查找的元素为x,如果x比根小,递归在左子树中查找x;如果比根大,递归在右子树中查找x,如果根和x相等,直接返回结果即可。最坏时间复杂度O(h) , h 为树的高度。

插入过程类似,假定要插入的元素为x,如果x比根小,递归在左子树中插入x;如果比根大,递归在右子树中插入x;如果根和x相等,插入失败,因为不能有相同元素。这个过程和查找最大的区别是,如果在递归时发现对应子树并不存在,查找过程的做法是返回“元素
不存在”,而插入过程的做法是在该子树的位置创建新结点。

删除比较复杂,暂不讨论。

对于同一结点集合,因为插入顺序不同,所以构造出的BST不唯一。
极端情况下,甚至会成为一条链。那么操作的时间复杂度就退化成线性。所以实用的BST必须是平衡的。如何让BST平衡呢?答案是旋转,让BST在保持合法的前提下改变形态。
主流编程语言大多都提供了直接可用的BST,比如STL中的map 和set

本着“不要重复造轮子”的原则,如果二者(或者
multiset / multimap )已经可以满足要求,建议不要自己实现平衡BST

其他应用

  • 线段树 区间信息的维护和查询
  • 树状数组 同上
  • 字典树 典型应用是用于统计,排序和保存大量的字符串
  • 并查集 查询元素所在集合,合并两个元素所在集合

定义

有n个顶点的树具有以下3个特点:连通,无环,恰好包含n − 1条边。

树的存储与表示方法

用fa[x] 表示结点x的父结点,因为除根结点外,每个结点都有且只有一个父结点(前驱)。
树可以看作一种包含n个结点,n-1条边的无向图,自然也可以用一般的存图方法来表示

树的遍历

深度优先遍历DFS

void dfs(int u,int p){ // u 为当前结点,p为当前结点的父结点
	 for(auto v:g[u]){ //枚举子结点 邻接表存图(vector g[size])
		 if(v==p) continue; //避免访问父结点
		dfs(v,u);
	 }
}

广度优先遍历BFS

void bfs(int root){
	 q.push(root); //根结点入队
	 while(q.size()){ //q.size() 返回队列大小(O(1))
			int u = q.front(); //取队首元素
			 q.pop(); //队首出队
			vis[u] = true; //访问标记
			 for(auto v:g[u]){ // 枚举子结点
					if(vis[v]) continue; //避免访问父结点
					q.push(v); // 子结点入队
			 }
		}
}

树的直径

给定一棵树,树中的每条边都有一个权值,树中两点之间的距离定义为连接两点的路径上的边权之和。树中最远的两个结点之间的距离被称为树的直径,连接这两点的路径被称为树的最长链。后者通常也可称为直径,即直径既是一个数值概念,也可代指一条路径。

树的直径可以作为很多树上题目的突破口。

树的直径求法

  • 树形DP
  • 两次BFS求树的直径
    1.从任意一个结点出发,通过BFS对树进行一次遍历,求出与出发点距离最远的结点,记为p。
    2.从结点p出发,通过BFS再进行一次遍历,求出与p距离最远的结点,记为q。
    3.从p到q的路径就是树的一条直径。这是因为p一定是直径的一端,否则总能找到一条更长的链,与直径的定义矛盾。

在第2步的遍历过程中,可以记录下来每个点第一次被访问时的 前驱结点。最后从q递归回到p,即可得到直径的具体方案。

例题:POJ 2631 Roads in the North

题解

#include 
#include 
#include 
#include 
#include 
using namespace std;
const int N=20005;
bool vis[N];
int dis[N],head[N],cnt;
struct Edge
{
    int v,l;
    int next;
}edge[N];
int node,ans;

void bfs(int n)
{
    memset(vis,false,sizeof(vis));
    queue p;
    p.push(n);
    vis[n]=true;
    while(p.size())
    {
        int u=p.front();
        p.pop();
        for(int i=head[u];i!=-1;i=edge[i].next)
        {
            int v=edge[i].v;
            if(!vis[v])
            {
                dis[v]=dis[u]+edge[i].l;
                vis[v]=true;
                p.push(v);
                if(ans

最近公共祖先(Lowest Common Ancestors, LCA)

定义

结点的祖先:该结点到根结点的所有结点
最近公共祖先:给定一棵有根树,若结点z既是结点x的祖先,又是结点y的祖先,则称z是x,y的公共祖先。在x,y的所有公共祖先中,深度最大的一个称为
x,y的最近公共祖先,记作LCA(x,y) 。

求最近公共祖先的方法

向上标记法
  1. 从x向上走到根结点,并标记所有经过的结点。
  2. 从y向上走到根结点,第一次遇到已经标记的结点时,就找到了LCA(x,y) 。

可以看出,这是一种朴素算法,对于每个询问,向上标记法的时间复杂度最坏为O(n)

树上倍增法(重点)

对LCA、树上倍增、树链剖分(重链剖分&长链剖分)和LCT(Link-Cut Tree)的学习
树上倍增的写法和应用

树上倍增法是一个很重要的算法。除了求LCA之外,它在很多问题中都有广泛应用。

设f[x][k] 表示x的2 k辈祖先,即从x向根结点走2 k 步到达的结点。
特别的,若该结点不存在,则令f[x][k]=0 。
f[x][0] 就是x的父结点。除此之外,∀k∈ [1, log2{N} ], f[x][k] =f[f[x][k-1]][k-1] 。

所以,我们可以对树进行广度优先遍历,按照层次排序,在结点入队之前,计算它在f数组中相应的值。
以上部分是预处理,时间复杂度为O(nlogn)

基于f数组计算LCA(x,y) 分为以下几步:

  1. 设d[x] 表示x的深度。不妨设d[x]>=d[y] (否则可以交换x,y)
  2. 用二进制拆分思想,把x向上调整到与y同一高度。

具体来说,就是依次尝试从x向上走k = 2log2n , …, 2 1, 20 步, 检查到达的结点是否比y深。在每次检查中,若是,则令x =f[x][k] 。

  1. 若此时x=y,就说明已经找到了LCA,LCA就等于y。
  2. 用二进制拆分思想,把x,y同时向上调整,并保持同一深度且二
    者不相会。

具体来说,就是依次尝试把x,y同时向上走 k = 2log2n , …, 2 1, 20 步,在每次尝试中,若f[x][k]!=f[y] [k] (即仍未相会),则令x = f[x][k],y = f[y][k] 。

  1. 此时x,y必定只差一步就相会了,它们的父结点f[x][0] 就是LCA。

参考代码

#include
using namespace std;
const int SIZE = 50010;
int f[SIZE][20], d[SIZE], dist[SIZE];
int ver[2 * SIZE], Next[2 * SIZE], edge[2 * SIZE], head[SIZE];
int T, n, m, tot, t;
queue q;
void add(int x, int y, int z)
{
    ver[++tot] = y;
    edge[tot] = z;
    Next[tot] = head[x];
    head[x] = tot;
}
// 预处理
void bfs()
{
    q.push(1);
    d[1] = 1;
    while (q.size())
    {
        int x = q.front();
        q.pop();
        for (int i = head[x]; i; i = Next[i])
        {
            int y = ver[i];
            if (d[y]) continue;
            d[y] = d[x] + 1;
            dist[y] = dist[x] + edge[i];
            f[y][0] = x;
            for (int j = 1; j <= t; j++)
                f[y][j] = f[f[y][j - 1]][j - 1];
            q.push(y);
        }
    }
}
// 回答一个询问
int lca(int x, int y)
{
    if (d[x] > d[y]) swap(x, y);
    for (int i = t; i >= 0; i--)
        if (d[f[y][i]] >= d[x]) y = f[y][i];
    if (x == y) return x;
    for (int i = t; i >= 0; i--)
        if (f[x][i] != f[y][i]) x = f[x][i], y = f[y][i];
    return f[x][0];
}
int main()
{
    cin >> T;
    while (T--)
    {
        cin >> n >> m;
        t = (int)(log(n) / log(2)) + 1;
// 清空
        for (int i = 1; i <= n; i++) head[i] = d[i] = 0;
        tot = 0;
// 读入一棵树
        for (int i = 1; i < n; i++)
        {
            int x, y, z;
            scanf("%d%d%d", &x, &y, &z);
            add(x, y, z), add(y, x, z);
        }
        bfs();
// 回答问题
        for (int i = 1; i <= m; i++)
        {
            int x, y;
            scanf("%d%d", &x, &y);
            printf("%d\n", dist[x]+dist[y]-2*dist[lca(x, y)]);
        }
    }
}

Tarjan离线算法 (挖坑)

你可能感兴趣的:(树)