引用自 树基础
递归定义:
二叉树要么为空,要么由根结点root ,左子树left subtree ,右子树right subtree 组成,而左子树和右子树分别是一棵二叉树。
void build(int root){
build(root<<1); //left subtree
build(root<<1|1); //right subtree
...
}
完整二叉树(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); // 子结点入队
}
}
}
给定一棵树,树中的每条边都有一个权值,树中两点之间的距离定义为连接两点的路径上的边权之和。树中最远的两个结点之间的距离被称为树的直径,连接这两点的路径被称为树的最长链。后者通常也可称为直径,即直径既是一个数值概念,也可代指一条路径。
树的直径可以作为很多树上题目的突破口。
在第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
结点的祖先:该结点到根结点的所有结点
最近公共祖先:给定一棵有根树,若结点z既是结点x的祖先,又是结点y的祖先,则称z是x,y的公共祖先。在x,y的所有公共祖先中,深度最大的一个称为
x,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) 分为以下几步:
具体来说,就是依次尝试从x向上走k = 2log2n , …, 2 1, 20 步, 检查到达的结点是否比y深。在每次检查中,若是,则令x =f[x][k] 。
具体来说,就是依次尝试把x,y同时向上走 k = 2log2n , …, 2 1, 20 步,在每次尝试中,若f[x][k]!=f[y] [k] (即仍未相会),则令x = f[x][k],y = f[y][k] 。
参考代码
#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)]);
}
}
}