打算学的:
树的基础——基本概念,遍历,重心,直径,LCA,dfn,括号序
线段树——基础线段树,带标记的线段树
树状数组——基础,树状数组求第K大,多维树状数组
树链剖分——重链剖分
笛卡尔树
树的启发式合并
DSU on tree
目录
树的基本概念
1.递归定义。
2.每个元素称为结点,至少有一个根节点(树根)。
3.结点的子树个数称为度,度为0的结点是叶结点,根不为0的是分支节点,除根以外的分支节点为内部节点,树的度为最大的那个度。
4.根节点的层次为0。树的深度为层次的最大值。
5.结点自上而下能到另一个结点,称为路径,不同子树结点不存在路径
一个没有固定根结点的树称为 无根树(unrooted tree)。无根树有几种等价的形式化定义:
-
有n个结点,n-1条边的连通无向图
-
无向无环的连通图
-
任意两个结点之间有且仅有一条简单路径的无向图
-
任何边均为桥的连通图
-
没有圈,且在任意不同两点间添加一条边之后所得图含唯一的一个圈的图
在无根树的基础上,指定一个结点称为 根,则形成一棵 有根树(rooted tree)。有根树在很多时候仍以无向图表示,只是规定了结点之间的上下级关系。
有关树的定义
适用于无根树和有根树
-
森林(forest):每个连通分量(连通块)都是树的图。按照定义,一棵树也是森林。
-
生成树(spanning tree):一个连通无向图的生成子图,同时要求是树。也即在图的边集中选择n-1条,将所有顶点连通。
-
无根树的叶结点(leaf node):度数不超过1的结点。
- 有根树的叶结点(leaf node):没有子结点的结点。
只适用于有根树
-
父亲(parent node):对于除根以外的每个结点,定义为从该结点到根路径上的第二个结点。 根结点没有父结点。
-
祖先(ancestor):一个结点到根结点的路径上,除了它本身外的结点。 根结点的祖先集合为空。
-
子结点(child node):如果u 是v 的父亲,那么v是u 的子结点。
子结点的顺序一般不加以区分,二叉树是一个例外。 -
结点的深度(depth):到根结点的路径上的边数。
-
树的高度(height):所有结点的深度的最大值。
-
兄弟(sibling):同一个父亲的多个子结点互为兄弟。
-
后代(descendant):子结点和子结点的后代。
- 子树(subtree):删掉与父亲相连的边后,该结点所在的子图。
特殊的树
-
链(chain/path graph):满足与任一结点相连的边不超过2条的树称为链。
-
菊花/星星(star):满足存在u使得所有除u 以外结点均与u相连的树称为菊花。
-
有根二叉树(rooted binary tree):每个结点最多只有两个儿子(子结点)的有根树称为二叉树。常常对两个子结点的顺序加以区分,分别称之为左子结点和右子结点。
大多数情况下,二叉树 一词均指有根二叉树。 -
完整二叉树(full/proper binary tree):每个结点的子结点数量均为 0 或者 2 的二叉树。换言之,每个结点或者是树叶,或者左右子树均非空。
- 完全二叉树(complete binary tree):只有最下面两层结点的度数可以小于 2,且最下面一层的结点都集中在该层最左边的连续位置上。\
- 完美二叉树(perfect binary tree):所有叶结点的深度均相同的二叉树称为完美二叉树。
树的存储
1.父亲表示法
struct node
{
int data,parent;
};
node tree[10];
2.孩子表示法
struct node;
node *tree;
struct node
{
char data;
tree child[10];
};
tree t;
3.父亲孩子表示法
struct node;
node *tree;
struct node
{
char data;
tree child[10];
tree father;
};
tree t;
4.孩子兄弟表示法
struct node;
node *tree;
struct node
{
char data;
tree firstchild,next;
};
tree t;
树的遍历
先序遍历:根左右
中序遍历:左根右
后序遍历:左右根
题目:求先序遍历
#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
void beford(string in,string after)
{
if (in.size()>0)
{
char ch=after[after.size()-1];
cout<<ch;//找根输出
int k=in.find(ch);
beford(in.substr(0,k),after.substr(0,k));
beford(in.substr(k+1),after.substr(k,in.size()-k-1));//递归左右子树;
}
}
int main()
{
string inord,aftord;
cin>>inord;
cin>>aftord;//读入
beford(inord,aftord);
cout<<endl;
return 0;
}
/*BADC
BDCA
ABCD
*/
题目:求后序遍历
#include<bits/stdc++.h>
using namespace std;
string s1,s2;
void bl(int l1,int r1,int l2,int r2)
{
int m=s2.find(s1[l1]);
if(m>l2) bl(l1+1,l1+m-l2,l2,m-1);
if(m<r2) bl(l1+m-l2+1,r1,m+1,r2);
cout<<s1[l1];
}
int main()
{
cin>>s1>>s2;
bl(0,s1.length()-1,0,s2.length()-1);
cout<<endl;
return 0;
}
/* abdec dbeac
debca
*/
附加:
已知中序遍历序列和另外一个序列可以求第三个序列。
- 前序的第一个是 root,后序的最后一个是 root。
- 先确定根节点,然后根据中序遍历,在根左边的为左子树,根右边的为右子树。
- 对于每一个子树可以看成一个全新的树,仍然遵循上面的规律。
我重新梳理一遍哈
存储树
typedef struct BiTree
{
char data;
node *lchild,*rchild;
}BiTNode,*tree;
创建树
根据前序遍历创建,如果为#则是空
void CreateBiTree(BiTree &T)
{
char ch;
cin >> ch;
if(ch=='#') T=NULL; //递归结束,建空树
else{
T=new BiTNode;
T->data=ch; //生成根结点
CreateBiTree(T->lchild); //递归创建左子树
CreateBiTree(T->rchild); //递归创建右子树
} //else
}
知道前序和中序建树
//pre、mid分别存储前序和中序序列,ipre、imid分别是前序和中序开始遍历的位置,n为树的结点个数
BiTree *Create1(char *pre,char *mid,int ipre,int imid,int n)
{
if(n==0) return NULL;//如果长度为零说明根之前之后没有子树了
BiNode *p=new BiNode;
p->data=pre[ipre];//根节点指向这一段前序的第一个
int i=0;
while(pre[ipre]!=mid[imid+i]) i++;//去找在中序排列中的那个根
p->lchild=Create1(pre,mid,ipre+1,imid,i);//根的前面必然是左子树,设置左孩子是这个树,此时前序往后移动一位,而中序开始位置不变
p->rchild=Create1(pre,mid,ipre+i+1,imid+i+1,n-i-1);//是右子树,长度是减左子树减1,前序去掉左子树,再往后一位是这个右子树的根节点,中序到长度这一段,是这个右子树的结点们
return p;如果这个点的根和左右子树都找到了,可以返回
}tree =Create1(pre,mid,0,0,n);
找了个更简洁的
TREE *creat1(int l, int r)
{
if (l > r) return NULL;
int mid;
TREE *root = new TREE;
root->data = qian[now];
for (int i = l; i <= r; i++)
{
if (qian[now] == zhong[i])
{
mid = i;
break;
}
}
now++;
root->l = creat1(l, mid - 1);
root->r = creat1(mid + 1, r);
return root;
}
TREE *root = creat(0, n - 1);
中序后序建树
BiTree* Create2(int lin, int rin, int lpost, int rpost) {
if (lin>rin||lpost>rpost) return NULL;//也就是说没有长度
BiNode* root = new(BiNode);
root->data =post[rpost];
root->left = root->right = NULL;
int index = -1;
for (int i = lin; i <= rin; i++) {
if (in[i] == post[rpost]) {
index = i;
break;
}
}
int r = rin - index; //中序序列中右子树节点数
root->left = Create2(lin, index-1, lpost, rpost-r-1);//左子树
root->right = Create2(index+1, rin, rpost-r, rpost - 1);//右子树
return root;
}tree=Create2(0,n-1,0,n-1);
分别是
lin这个树在中序遍历中的最左边
rin这个树在中序遍历的最右边
lpost后序的开始
rpost后序的末尾
struct TREE
{
int data;
TREE *left,*right;
};
TREE* create(int l1,int r1,int l2,int r2)
{
if(l1>r1||l2>r2) return NULL;
TREE* root=new(TREE);
root->data=a[r2];
int index=-1;
f(i,l1,r1)
{
if(a[r2]==b[i])
{
index=i;break;
}
}
root->left=create(l1,index-1,l2,r2-r1+index-1);
root->right=create(index+1,r1,r2-r1+index,r2-1);
return root;
}f(i,1,n) cin>>a[i];
f(i,1,n) cin>>b[i];
TREE* tree;
tree=create(1,n,1,n);
中序层序比较简单感觉
BiTree* root = NULL;
BiTree* create3(int index, node * &root)
{
if(root == NULL)
{
root = new BiTNode
root->left = root->right = NULL;
root->data = level[index];//让此时的根结点为层序遍历的第一个
return root;
}//不管是第一个根节点,还是左右子树,只要此时这个点没有连接小树,就可以给他赋值如果此时这个结点已经有东西了,说明此时这个树已经有父亲了,此时的点是他的儿子,就应该找这是左儿子还是右儿子
if(pos[root->data] > pos[level[index]]) create3(index, root->left);//这句话的意思是如果此时的根结点比当前的结点在中序中的位置还后面,就是在左子树中建这个树
else create3(index, root->right);//反之,如果这个结点在中序中排在根结点的后面,说明它对于根节点来说是右子树,由右子树去创建
return root;
}所以这个代码是在重复执行创建第一个根节点,对于下一个层序遍历的节点,如果当前点没人就是他的位置,如果有人就往下寻找可以存放的位置,变量root会一直更新,替它找到合适的建树位置。
level i代表 层序遍历的第三个数字是leveli
pos i代表i这个数字在中序遍历中排在第posi个
写的是for i->n 调用create(i,root)
先中后遍历方式
void pre(tree t)
{
if(t==NULL) return ;
printf("%c->",t->data);
pre(t->lchild);
pre(t->rchild);
}vector<int> ans;
void ceng(node *root) {
queue<node *> q;
q.push(root);
while (q.size()) {
auto now = q.front();
q.pop();
if (now == NULL) continue;
ans.push_back(now->data);
q.push(now->l);
q.push(now->r);
}
}
void in(tree t)
{
if(t==NULL) return ;
pre(t->lchild);
printf("%c->",t->data);
pre(t->rchild);
}
void post(tree t)
{
if(t==NULL) return ;
pre(t->lchild);
pre(t->rchild);
printf("%c->",t->data);
}
层序遍历
void Inorder(TREE* T)
{
queue<TREE*>q;
q.push(T);
while(!q.empty()){
int n=q.size();
while(n--){
TREE* root=q.front();
if(!r)cout<<root->data;
else cout<<" "<<root->data;
q.pop();
if(root->left)q.push(root->left);
if(root->right)q.push(root->right);
}
r++;
}
}
树的重心
定义:对于树上的每一个点,计算其所有子树中最大的子树节点数,这个值最小的点就是这棵树的重心。
性质:
1.以树的重心为根时,所有子树的大小都不超过整棵树大小的一半。
2.树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么到它们的距离和一样。
3.把两棵树通过一条边相连得到一棵新的树,那么新的树的重心在连接原来两棵树的重心的路径上。
4.在一棵树上添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
求法:在 DFS 中计算每个子树的大小,记录“向下”的子树的最大大小,利用总点数 - 当前子树(这里的子树指有根树的子树)的大小得到“向上”的子树的大小,然后就可以依据定义找到重心了。
用到第一条性质
定义几个数组:f[u]f[u]表示以u为根的总距离,size[u]size[u]表示以u为根的子树的大小(结点数,此题每个点要乘以权值,下文结点数均指此)。
显然,ans=min(f[i],1<=i<=n)ans=min(f[i],1<=i<=n)
首先我们任意以一个点为根dfs一遍,求出以该点为根的总距离。方便起见,我们就以1为根。
接下来就是转移,对于每个u能达到的点v,有:
f[v]=f[u]+size[1]-size[v]-size[v]f[v]=f[u]+size[1]−size[v]−size[v]
怎么来的呢?试想,当根从u变为v的时候,v的子树的所有节点原本的距离要到uu,现在只要到vv了,每个结点的距离都减少1,那么总距离就减少size[v]size[v],同时,以v为根的子树以外的所有节点,原本只要到uu就行了,现在要到vv,每个节点的路程都增加了1,总路程就增加了size[1]-size[v]size[1]−size[v],其中size[1]size[1]就是我们预处理出来的整棵树的大小,减去size[v]size[v]就是除以v为根的子树以外的结点数。
最后取最小值,得解。
#include <bits/stdc++.h>
#define rep(i,m,n) for(register int i=m;i<=n;++i)
using namespace std;
struct Edge
{
int next,to;
}e[10010 << 1];
int head[10010],num,w[10010],n,size[10010],a,b;
long long ans=0xffffff,f[10010];
inline void Add(int from,int to)
{
e[++num].to=to;
e[num].next=head[from];
head[from]=num;
}
void dfs(int u,int fa,int dep)
{ //预处理f[1]和size
size[u] = w[u];
for(int i = head[u];i;i=e[i].next)
{
if(e[i].to!=fa)
dfs(e[i].to,u,dep+1),size[u]+=size[e[i].to];
}
f[1]+=w[u]*dep;
}
void dp(int u,int fa)
{ //转移
for(int i=head[u];i;i=e[i].next)
if(e[i].to!=fa)
f[e[i].to]=f[u]+size[1]-size[e[i].to]*2,dp(e[i].to,u);
ans=min(ans,f[u]); //取最小值
}
int main()
{
ans*=ans;
scanf("%d",&n);
rep(i,1,n)
{
scanf("%d",&w[i]);
scanf("%d",&a);
scanf("%d",&b);
if(a) Add(i,a),Add(a,i);
if(b) Add(i,b),Add(b,i);
}
dfs(1,0,0);
dp(1,0);
printf("%lld\n",ans);
return 0;
}
树的直径
树的直径:树上任意两节点之间最长的简单路径
一棵树可以有多条直径,他们的长度相等。
可以用两次 DFS 或者树形 DP 的方法在O(N)时间求出树的直径。
树的直径的性质:
(1).对树上的任意一点而言,树上与它距离最远的点一定为树的直径的两个端点的其中之一;
(2).直径两端点一定是两个叶子节点;
(3).对于两棵树,如果第一棵树直径两端点为( u , v ),第二棵树直径两端点为( x , y ),用一条边将两棵树连接,那么新树的直径的两端点一定是u,v,x,y 中的两个点;
(4).对于一棵树,如果在一个点的上接一个叶子节点,那么最多会改变直径的一个端点;
(5).若一棵树存在多条直径,那么这些直径交于一点且交点是这些直径的中点;
题目:给定一棵N个节点的树,求其直径的长度。
1)搜索(dfs/bfs):
步骤:
首先,以任意一点为搜索的起点,进行第一次搜索。此次搜索得到的距起点最远的点即为直径端点之一。然后以该点为起点进行第二次搜索,所得到的距其距离最远的点即为另一个端点。
复杂度: O ( n )
方便记录路径
const int N = 10000 + 10;
int n, c, d[N];
vector<int> E[N];
void dfs(int u, int fa) {
for (int v : E[u]) {
if (v == fa) continue;
d[v] = d[u] + 1;
if (d[v] > d[c]) c = v;
dfs(v, u);
}
}
int main() {
scanf("%d", &n);
for (int i = 1; i < n; i++) {
int u, v;
scanf("%d %d", &u, &v);
E[u].push_back(v), E[v].push_back(u);
}
dfs(1, 0);
d[c] = 0, dfs(c, 0);
printf("%d\n", d[c]);
return 0;
}
(2)树形dp:
我们记录当1为树的根时,每个节点作为子树的根向下,所能延伸的最远距离D1 ,和次远距离 D2,那么直径就是所有d1+d2的最大值。
树形 DP 可以在存在负权边的情况下求解出树的直径。
const int N = 10000 + 10;
int n, d = 0;
int d1[N], d2[N];
vector<int> E[N];
void dfs(int u, int fa) {
d1[u] = d2[u] = 0;
for (int v : E[u]) {
if (v == fa) continue;
dfs(v, u);
int t = d1[v] + 1;
if (t > d1[u])
d2[u] = d1[u], d1[u] = t;
else if (t > d2[u])
d2[u] = t;
}
d = max(d, d1[u] + d2[u]);
}
int main() {
scanf("%d", &n);
for (int i = 1; i < n; i++) {
int u, v;
scanf("%d %d", &u, &v);
E[u].push_back(v), E[v].push_back(u);
}
dfs(1, 0);
printf("%d\n", d);
return 0;
}
这样做就是,先看能否更新最大值,若能,它的次大值就是原先的最大值,再更新它的最大值;若不能,就看能不能更新次大值,若能,就更新,不能就不管它
LCA
最小生成树
例如:连通图最小费用连接(n-1,无回路)
子图: 从原图中选中一些由节点和边组成的图,称之为原图的子图。
生成子图:选中一些由边和所有节点组成的图,称之为原图的生成子图。
生成树:如果生成的子图恰好是一棵树, 则称之为生成树。
最小生成树:权值之和最小的生成树,称之为最小生成树。
求解算法有两种: Prim 算法和Krnuskal算法。
//3.算法实现
void Prim(int n)
{
s[1] = true;//初始时,在集合U中只有一个元素,即节点1。也就是第一个集合U是1
for (int i = 1; i <= n; i++)
{//①初始化
if (i != 1)
{
lowcost[i] = c[1][i];//lowcost初始化是1到i的距离
closest[i] = 1;//设置离i最近的点是1
s[i] = false;//此时i不在集合中
}
else
lowcost[i] = 0;
}
for (int i = 1; i < n; i++)
{//在集合V-U中寻找距离集合U最近的节点t
int temp = INF;
int t = 1;
for (int j = 1; j <= n; j++)
{
if ((!s[j]) && (lowcost[j] < temp)) //如果j点不在集合里,且存在点j到当前点的直达点
{
t = j;//让t=j
temp = lowcost[j];//更新temp
}
}
if (t == 1) break;//找不到t,跳出循环,因为1一开始就在u,t不可能被更新成1
s[t] = true;//否则,说明找到了链接两个集合的最短线,将t加入集合U中
for (int j = 1; j <= n; j++)
{//④更新lowcost和closest
if ((!s[j]) && (c[t][j] < lowcost[j]))//如果j不在集合里,而且存在一条线从新点到j距离比原本标记的l还近
{
lowcost[j] = c[t][j];//我们更到j点的最近距离
closest[j] = t;//并且用c把这个最近距离点保存下来
}
}
}
总结一下普利姆算法:
开一个二维数组c从两点之间的距离。
设置已入树集合s
把1放入集合s里头
开一个一维数组存s到i最短距离li,初始化存的是1的相邻点
开一个一维数组存i距离最近s中的点ri,初始化存的都是1
循环找一遍点不在集合且距离最近的点
把这个点加到集合里,然后再判断一下c中和他相连的边中有没有更近的,有的话更新l和r
完事之后s里头的点就是树了,权值之和就是l数组之和,树的链接法就是r里头的头
举例,r1,2,3,4,5,6,7等于空,1,7,7,4,5,2
所以1-2-7(3,4-5-6)
克鲁斯卡尔算法
struct edge{
int u,v,w;
}e[N*N];//e是边的集合,分别有起点终点权值三个变量
bool cmp (edge x,edge y)//排序方法是把权值小的放前面
{
return x.w<y.w;
}
void init(int n)//这是通过初始化设定每个人的父亲是自己
{
for(int i=1;i<=n;i++)
{
fa[i]=i;
}
}
int merge (int a,int b)//将a,b相连合并到一个集合里,并把所有把b当爹的父亲都换成a的父亲
{
int p=fa[a];
int q=fa[b];
if(p==q) return 0;//如果本来就是同一个爹,这说明出现回路了,返回0
for(int i=1;i<=n;i++)
{
if(fa[i]==q) fa[i]=p;
}
return 1;
}
int kruskal(int n)
{
int ans=0;//初始化
sort(e,e+m,cmp)//对e进行排序,这个排序的目的是让权值小的在前面哦,这样一定是最优的
//这里是贪心的思想,先排好序,然后从短的边边开始遍历,每一个点都一定要进集合的,所以这个方式会是最优解哦
{
for(int i=0;i<m;i++)//m是边的数量
{
if(merge(e[i].u,e[i].v))//试试看这一条边能不能连起来啦,如果连起来返回1
{
ans+=e[i].w;//我们加上这个边的权值
n--;//此时还有一些没有连上的点
if(n==1) return ans;//当最终只剩1的时候,说明每个点都在树中啦,fa都等于第一条可以连的边边哦
}
}
}
return 0;
}
总结一下把:
(这是一个无向联通带权图)
用一个结构体来存边
初始化每个人的父亲是自己
把每一个边按照权值从小到大排序
依次遍历每一条边,看看这个边可不可以放到集合中,(父亲一样就是回路,父亲不一样就放进去,并把父亲变成一样,把权值加上去)
一旦添加了N-1条边,就说明全部的点都在集合中了,退出遍历。
做一下例题:
并查集+克鲁斯卡尔
#include<bits/stdc++.h>
using namespace std;
const int N=20005;
int fa[10005],father[10005],n,m,s,t;
struct edge{
int u,v,w;
}e[N];//e是边的集合,分别有起点终点权值三个变量
bool cmp (edge x,edge y)//排序方法是把权值小的放前面
{
return x.w<y.w;
}
void init(int n)//这是通过初始化设定每个人的父亲是自己
{
for(int i=1;i<=n;i++)
{
fa[i]=i;
father[i]=i;
}
}
int find(int x)
{
return x==father[x]?x:father[x]=find(father[x]);
}
void union2(int x,int y)
{
int rx=find(x);
int ry=find(y);
if(rx!=ry) father[rx]=ry;
}
int merge (int a,int b)//将a,b相连合并到一个集合里,并把所有把b当爹的父亲都换成a的父亲
{
int p=fa[a];
int q=fa[b];
if(p==q) return 0;//如果本来就是同一个爹,这说明出现回路了,返回0
if(find(a)!=find(s)||find(s)!=find(b)) return 0;
for(int i=1;i<=n;i++)
{
if(fa[i]==q) fa[i]=p;
}
return 1;
}
int kruskal()
{
sort(e,e+m,cmp);//对e进行排序,这个排序的目的是让权值小的在前面哦,这样一定是最优的
//这里是贪心的思想,先排好序,然后从短的边边开始遍历,每一个点都一定要进集合的,所以这个方式会是最优解哦
{
for(int i=0;i<m;i++)//m是边的数量
{
if(merge(e[i].u,e[i].v))//试试看这一条边能不能连起来啦,如果连起来返回1
{
if(fa[s]==fa[t]) return e[i].w;//当最终只剩1的时候,说明每个点都在树中啦,fa都等于第一条可以连的边边哦
}
}
}
return 0;
}
int main()
{
cin>>n>>m>>s>>t;
init(n);
for(int i=0;i<m;i++)//注意边界
{
cin>>e[i].u>>e[i].v>>e[i].w;
union2(e[i].u,e[i].v);
}
cout<<kruskal()<<endl;
}