树上问题小结

知识点总结。

判断树上的链
···每个点的度数不超过二(出度与入度之和)

判断树
···n个点,只有n-1条边
···每两个点之间的边具有唯一性

多叉树转换成二叉树
···第一个孩子作为左孩子,第一个孩子的兄弟作为它的右孩子

最小生成树
生成:prim or kruskal具体代码在这里
一些小性质:
(1)切割性质:假定所有边权均不相同。设S为既非空集也非全集的V的子集,边e是满足一个端点在S内,另一个端点不再S内的所有边中权值权值最小的一个,则图G的所有生成树均包含e。
(2)回路性质。假定所有边权均不相同。设C为图G的任意回路,边e是C上权值最大的边,则图G的所有生成树均不包含e。

最小瓶颈生成树
给出加权无向图,求一个最小生成树,使得最大边权值尽量小。
每颗最小生成树一定是最小瓶颈生成树,每颗最小瓶颈生成树却不一定是最小生成树

最小瓶颈路
给定加权无向图的两个结点u和v,求出从u到v的一条路径,使得路径上的最长边尽量短。
我们直接求出这个图的最小生成树,则起点和终点在树上的唯一路径就是我们要找的路径,这条路经上的最长边就是问题的答案。

每对结点间的最小瓶颈路
给出加权无向图,求每两个结点u和v之间的最小瓶颈路的最大边长f(u,v)
我们先求出来最小生成树,同时计算f(u,v),每访问一个结点u时,考虑所有已经访问过的老结点x,更新f(x,u)=max(f(x,v),w(u,v)),其中v是u的父亲结点。(使用dfs实现上述过程)

树的重心
树上一点,满足删除该点时,树内剩下的子树最大节点数最小。

性质
1、树的重心每棵子树的大小一定小于等于n/2

2、每颗子树的大小都小于等于n/2的点一定是这棵树的重心(就是上一个的逆定理)

3、树中所有点到某个点的距离和中,到重心的距离和最小(如果有两个重心,他们的距离一样)
证明:我们考虑使用调整法,设当前最优决策为u点,v为u的任意相邻节点。记size(x)为当u为整棵树的根时,以x为根的子树的节点的大小。
u为全局最优决策当且仅当n−size(v)≥size(v),否则最优策略一定在不满足该条件的v的子树中。
我们化简这个式子,即size(v)≤n/2
由定理2得,该点为树的重心。

4、两棵树通过一条边相连成为一颗新的树,新树重心一定在原来两棵树得重心的路径上。(注意中心不止一个的情况)

怎么找重心?
方法1:处理出每个节点的𝑠𝑖𝑧𝑒,依次枚举点,模拟删除该点后各子树大小,更新最优解。

方法2:采用“调整法”的思想,从一个点出发,调整过去。

两种方法都是Ο(𝑛)的。

树的直径
树(可带权)上最长的简单路径。
1、一棵树的直径可能有若干条,但是有一点显然——他们一定两两相交,不然我们就一定可以找出一条更长的。

2、所有直径的交集一定非空。因为如果三条直径两两相交。如果他们没有共同交集,那么就会形成环。然后我们可以一直推广到所有直径的情况。

3、以树上任意一个点作为起点的最长路径,重点一定是直径上的一个端点。
4、对于两条相交直径,他们不相交的部分一定对称。

5、两棵树用一条边合并,新树的直径两端一定是原本两棵树直径四个端点中的两个。
证明:(1)直径不经过新边 这个时候显然是原本两条直径中的一条。否则就不满足直径的定义了。(2)直径经过新边。 新边两端分属两棵树,那么这条直径的新边的两端部分肯定是从这两个点出发在各自树中的最长路径。根据性质3,端点还是四个端点的其中之二。

怎么找直径
由于性质3,我们可以通过两次bfs或者dfs来确定直径——任选一点,通过搜索找到从该点出发的最长路,由性质得到,终点为直径的一个端点。从该端点出发,再通过搜索找到最长路,由定理得,此终点一定是直径的另外一个端点。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<cmath>
#define MAXN 300010
using namespace std;
int n,m,k,t;
int head[MAXN<<1],done[MAXN],dis[MAXN];
struct Edge{int nxt,to,dis;}edge[MAXN<<1];
inline void add(int from,int to,int dis)
    {edge[++t].nxt=head[from],edge[t].to=to,edge[t].dis=dis,head[from]=t;}
inline void solve(int x)
{
    done[x]=1;
    for(int i=head[x];i;i=edge[i].nxt)
    {
        int v=edge[i].to;
        if(!done[v])
        {
            dis[v]=dis[x]+edge[i].dis;
            solve(v);
        }
    }
}
int main()
{
    #ifndef ONLINE_JUDGE
    freopen("ce.in","r",stdin);
    #endif
    scanf("%d%d",&n,&k);
    for(int i=1;i<n;i++)
    {
        int x,y,w;
        scanf("%d%d%d",&x,&y,&w);
        add(x,y,w),add(y,x,w);
    }
    solve(41);
    int maxx=-0x3f3f3f3f,pos;
    for(int i=1;i<=n;i++) 
        if(dis[i]>maxx)
            maxx=dis[i],pos=i;
    memset(dis,0,sizeof(dis));
    memset(done,0,sizeof(done));
    solve(pos);
    maxx=-0x3f3f3f3f,pos;
    for(int i=1;i<=n;i++)
        if(dis[i]>maxx)
            maxx=dis[i],pos=i;
    printf("%d\n",maxx);
    return 0;
}

但是要注意!这种方法对于有负权的树,求直径是错的!!
不过还有一种肯定是正确的算法——树形DP

inline void dp(int x)
{
    done[x]=1;
    for(int i=head[x];i;i=edge[i].nxt)
    {
        int v=edge[i].to;
        if(done[v]) continue;
        dp(v);
        ans=max(ans,d[x]+d[v]+edge[i].dis);
        d[x]=max(d[x],d[v]+edge[i].dis);
    }
}

遍历序列

欧拉序
从根节点开始dfs遍历树——在点x时,走到一个未遍历过的儿子,或者儿子已经全部遍历过从x返回到父亲时,以此法得到的遍历序列是欧拉序。

DFS序
从根节点开始dfs遍历树,一个节点第一次被遍历到时加入到序列中,以此法得到的遍历序列是dfs序。

括号序
从根节点开始dfs遍历树,一个节点第一次被遍历或者遍历完儿子要退出时将其加入到序列中,以此法得到的遍历序列是括号序。

应用
1、将树中子树表示为遍历序列的一段区间。(括号序)

2、判断一个点是否在以另外一个点为根的子树里。
(设一个点x在dfs序列中位置为dfn(x),那么如果点y在以x为根的子树离=>dfn(x)≤dfn(y)<dfn(x)+size(x)

3、和2等价的,还可以推出判断点x是否在点y到根节点的路径上(只要判断不在子树里就可以了)

4、再考虑这样的一类询问,求点x到根路径上所有点的权值和,且存在修改点权操作。
显然,每个点的权值,对以该点为根的子树的所有点都有贡献,所以每次修改操作(初始化时给点赋值也看成修改)就可以将以该点为根的子树所有点答案加上修改的值。
处理时我们可以开一颗全局线段树,每个点在其中的下标就是该点的dfs序,值为该点的权值。
求答案时就是单点查询该点在线段树中的权值,修改时就是对整棵子树进行区间修改。
但是还有更简单的方法!我们考虑括号序列。
我们开一个大小为2n的数组,点x在位置arrx插入权值,从位置leax插入权值的相反数,做一次前缀和后,设sumi为前i项的和,显然点x到根节点的路径权值就是sumarrx
维护这个可以修改的前缀和数组可以用树状数组qwq

posted @ 2021-08-15 20:41  Mint-hexagram  阅读(67)  评论(0编辑  收藏  举报