简单树问题
多叉树
定义
-
*有n个结点,n-1条边的连通无向图
-
无向无环的连通图
-
任意两个结点之间有且仅有一条简单路径的无向图
-
任何边均为桥的连通图
桥
连接两个连通块的边
也就是说
删除桥后,图上的连通块个数+1
- 没有环,且在任意不同两点间添加一条边之后所得图含唯一的一个环的图
简单来说
-
连通图
-
无向图
-
边比点少一条
-
无环
存储方式
-
只储存父结点编号
fa[i]对于任意树,因为树上每个结点仅有一个父结点
仅储存最低信息
只能从叶子结点向上,不能向下
仅使用一个数组
-
图论存储方式(邻接表,链式前向星)
多用于多叉树
可以从根结点DFS整棵树
-
二叉树存储方式
左儿子是
2i,右儿子是2i+1
树的遍历
为了访问树的所有结点
二叉树:前中后序遍历
多叉树:
进行DFS 扫一遍
利用了树的递归性质:
在遍历子树前,将要下放的信息,由当前结点下放到子树
在便利子树后,将子树的信息合并到当前结点
BFS应用较少,通常需要同一深度的结点一起处理时使用
Q:是不是和深度就要使用BFS???
A:NONONO
A:虽然说DFS不是严格按照深度进行遍历
但是我们可以在不同的深度进行不同的处理
BFS一是要在“同一深度的结点一起处理时使用”
A:况且DFS比BFS要好写哇
例题
一句好神奇又不得不承认的一句话:
万物皆可哈希
哈希:比较两个值是否相等
X Y
/ \ / \
Lx Rx Ly Ry
这是两棵树
那么就会有
Hash1(x)=(Lx*b^2+Rx*b+x)%p
Hash1(y)=(Ly*b^2+Ry*b+y)%p
其中,b为大于1000的质数,p随机取
如果当这两棵子树完全相等的时候
即:Lx=Ly,Rx=Ry,X=Y
那么Hash(x)=Hash(y)
我们只要判断Hash(x)和Hash(y)是否相等,就可以知道两棵子树是否相等
这个过程是递归的
所以说,举个栗子,Lx=Hash(Lx)
因为是对称的,所以我们要再弄一个函数,来存储对称的值
Hash1(x)=(Rx*b^2+Lx*b+x)%p
Hash1(y)=(Ry*b^2+Ly*b+y)%p
然后比较Hash1(X)与Hash2(Y)是否相等
然后这题比较玄学的就是
你打个40分的暴力,交上去竟然是正解
对于DFS,主要消耗复杂度的地方在于搜索失败返回的地方
暴力:
从根结点开始,依次判断每个点是否为对称二叉树的根
如果两个结点不一样,就返回
如果当前子树是对称二叉树,则忽略其他子树
假如,我们在搜索一棵树,并且刚刚枚举了k个点出现了不一样的点
就说明左右子树至少有k个结点
那么问题就变成了两个不小于k的,且和为n的子问题
设左右字数比较小的那一个有p个结点
那么我们相当于将一个规模为p(k≤p<n/2)和规模为n-p的问题合并为一个规模为n的问题
整棵树的复杂度表示为f(n)
那么f(n)=f(p)+f(n-p)+k≤f(p)+f(n-p)+p
第二个是启发式合并的复杂度
启发式合并在合并一个大小为n和一个大小为m的两棵子树的时候(n<m)
复杂度是O(n)
也就是较小的那一个
此题同理
所以此题的复杂度最坏不会大过启发式合并
也就是小于O(nlogn)
此题范围常数较小
所以暴力一样可以过
感觉可以拿去当题解
最小生成树
树
有n个点,n-1条边的连通图
(树的性质有许多,这里先说一个)
两棵树通过一条边相连所得的图还是树
生成树指一个图的边集的一个为树的子集
最小生成树即指所有树的子集中权值(权值和)最小的
常用贪心策略
Kruskal
(基于边的贪心)
-
先将所有边安从小到大的顺序排序(一般使用快排),并认为每一个点都是孤立的
-
按从小到大的顺序枚举每一条边
如果这条边连接着两个点不在一个连通块,那么就把这条边加入最小生成树,并将两个连通块合并 如果这条边连接的两个点在一个连通块,就跳过。直到选取了n-1条边为止 -
使用并查集维护数的连通性,复杂度 \(O(mlogm+mk)\) \((k\) 为并查集常数 \()\)
证明
(反证法)
假如用Kruskal生成的最小生成树是错误的
图中有一个更小的
那么图中就会有一条边比最小生成树中的一条边要小
但是这条边是不存在的
因为我们的边是从小到大枚举的
所以,如果没有枚举到这条边,就说明他不存在
就算出现了也只能是和它相等
因为在枚举的时候相等的两条边先后顺序不大一样
可能会有另外一条边与其相等
但是,这并不影响最终的权值和
所以,我们还可以得出另外一个结论:
在有多条边的权值相等的时候,最小生成树不唯一
Prim
(基于点的贪心)
-
将点分为两类 ,已选择和未选择的,初始化所有点都是未选择的(没有被选入最小生成树)
-
随机挑选一个点作为初始点
-
从所有连接两种类型的点的边,选取一条权值最小的,将对应边加入最小生成,将对应点点加入已选择的
-
复杂度取决于如何选择最小边(采用什么结构来维护)
暴力选取复杂度为\(O(n^2+m)\)
二叉堆维护为\(O((n+m)logn)\)
斐波那契堆维护为\(O(m+logn)\)
很像Dij
没理解可以不用理解(doge
Prim \(VS\) Kruskal
两个算法有着近似的理论复杂度,但是Prim常数更大
使用二叉堆优化的Prim跑不过Kruskal
虽然使用斐波那契堆的Prim复杂度比Kruskal杀了一个logm,但斐波那契堆没有STL实现手写 几乎不可行,且Kruskal中logm进位排序中使用,常数较小反而还快
在稠密图 \((m≈n^2)\) 中暴力的Prim有着最好的时间复杂度,但是同样不一定跑得过Kruskal
并且Kruskal比Prim更易理解,代码更简单
所以在OI中,常用的是Kruskal
所以Prim没理解没关系
树形DP
DP通常用子问题推到更大的问题
树本身字数就是树的子结构,具有一样的性质
树形DP通常采用子树推到当前结点的方向,层层向上推导
求一个树,每个子树内所有结点的最大值,其实就是一种简单的树形DP
树的LCA
最近公共祖先 简称LCA(Lowest Common Ancestor)
两个结点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个(离他们最近的那个)
性质
任意两个结点和他们的LCA相连形成一条路径,这条路径是该两点在树上的唯一路径(树上任意两点仅有一条路径)
同时LCA为该路径中深度最浅的点
怎么找两个点的LCA
朴素求法
-
平高度
如果两个结点的高度不相等,就要与比较浅的那个高度平齐
因为LCA为该路径中深度最浅的点
所以LCA只能比浅的点更浅,或者就是比较浅的那个点,但一定不能使深的那个点
-
向上跳
同时减少他们的高度
-
同一点
找他们的公共祖先,直到找到为止
树的直径
树中最长的一条边(不唯一,可以有很多条)
-
从当前根找一个深度最大的点x,以x为根找到离x最远的点,这两点的连线即为数的一条之境
-
DP求出每个点对影响下的链最长有多长,每个点取子树最大值和次大值,表示该节点为直径LCA时的最大长度
树的重心
使节点数最多的子树的结点最小的根为树的重心(最多两个)
树的重心可以用DFS模拟定义求解
等价条件:
以树的重心为根时,所有子树的大小都不超过整棵树大小的一半
等价于
如果一个结点为根时,所有子树的大小都不超过整棵树的大小的一般,该结点为重心
注意:自己父结点也是一棵子树
(性质)重心为根时,所有点到重心的和最小
void dfs(int x)
{
siz[x]=1;//大小
for(int i=first[x];i;i=nxt[i])
{
int v=to[i];
if(v==fa[x]) continue;
fa[v]=x;
dfs(v);//孩子结点
siz[x]+=siz[v];
w[x]=max(w[x],siz[v]);//计算最大的子树
}
w[x]=max(w[x],n-siz[x]);//不要忘记他的父亲也是一颗子树
if(w[x]<=n/2)//树的重心不会大于数的一半
ans[ans[0]!=0]=x;//因为树的重心最多有两个,所以如果ans[0]上已经存了一个重心了,那么就存在ans[1]里
}

浙公网安备 33010602011771号