树形结构全纪录(当然并不全)

这一部分的内容可以说是最杂的
只能给出一些典型题和简单的知识讲解

我们从简单的开始吧

树的重心

点分治的必要操作,难度:☆

经典例题:树的重心

void findroot(int now,int fa) {
    f[now]=0;
    size[now]=1;
    for (int i=st[now];i;i=way[i].nxt)
        if (way[i].y!=fa&&!vis[way[i].y]) {
            findroot(way[i].y,now);
            size[now]+=size[way[i].y];
            f[now]=max(f[now],size[way[i].y]);
        }
    f[now]=max(f[now],sz-size[now]);
    if (f[now]<f[root]) root=now;
}

树的直径

难度:☆~☆☆
经典例题:树的直径

喜闻乐见,树的直径有两种求法
方法一:两遍dfs

int pre[N],ans,nowx;

void dfs(int now,int fa,int dis) { 
    if (dis>ans) {
        ans=dis;                //记录最长链 
        nowx=now;       
    }
    for (int i=st[now];i;i=way[i].nxt)
        if (way[i].y!=fa) {
            pre[way[i].y]=i;    //记录路径
            dfs(way[i].y,now,dis+way[i].v); 
        }
}

void solve() {
    ans=0,nowx=0;
    memset(pre,-1,sizeof(pre));
    dfs(1,0,0);
    memset(pre,-1,sizeof(pre));
    dfs(nowx,0,0);
} 

方法二:dp
f [ i ] 表示最长链
g [ i ] 表示次长链
每次用儿子的 f [ s o n ] + w ( f a , i ) 更新 f [ i ] , g [ i ]
这样一个儿子最多只会对一个值产生影响(不是 f [ i ] ,就是 g [ i ]
因此我们可以保证 f [ i ] , g [ i ] 代表的链在不同子树中
最后答案: f [ r o o t ] + g [ r o o t ]

int f[N],g[N];

void dfs(int now,int fa) {
    f[now]=0,g[now]=0;
    for (int i=st[now];i;i=way[i].nxt)
        if (way[i].y!=fa) {
            dfs(way[i].y,now);
            int len=f[way[i].y]+way[i].v;
            if (len>f[now]) {
                g[now]=f[now];
                f[now]=len;
            }
            else g[now]=len;
        }
}

dsu on tree

难度:☆☆☆

dsu on tree讲解(yveh学长压箱底的宝贝)
经典例题:dsu on tree(子树中数目最多的颜色的权值和)

很惭愧,这种算法学的不是很好

主体思路就是轻重链剖分
但是没有那么繁琐了,不用维护太多东西
首先找到每个结点的重儿子
之后再进行一遍 d f s

d f s 流程:

  • d f s 轻儿子

  • d f s 重儿子

  • 把儿子的信息记录到父亲

  • s o n = 0

  • 记录答案

  • 一键消除轻儿子影响

认真看了一下,发现很多题可以用树上莫队瞎搞。。。


树分块

难度:☆☆~☆☆☆

经典例题:树分块(王室联邦)

怎么说呢,只搞过这一道树分块,所以没什么资格聊这个问题
不过感觉就是考虑子树


树形dp

难度:☆☆☆~☆☆☆☆
下面给出的只是一部分

枚举是种好方法
经典例题:树形结构(路径的权值为权值的最大公约数)

有时候,我们在状态中不需要记录当前结点的状态,而需要记录父节点的状态
一般,根结点需要特判一下
转移的时候看准了状态的定义
经典例题:树形dp+贪心
经典例题:树形dp(服务器)
经典例题:树形dp+双元限制

经典例题:树形dp(三色二叉树)
经典例题:树形dp(时态同步)
经典例题:树形dp(树上染色,求黑白点对的距离)

有时候我们考虑每一条边的贡献,这样反而会简单一点
树形dp经常和背包结合,基本套路(注意当前背包容量从大到小枚举):

//当前结点是now
for (循环now的所有儿子)
    for (i=M;i>=0;i--)        //M是背包容量,我们要转移f[now][i]
        for (j=0;j<=M;j++)    //j是当前儿子中要占据多少容量
            f[now][i]=max(f[now][i],f[now][i-j]+f[son][j]);

树上的路径我们经常拆成 ( u > l c a ) + ( l c a > v ) 两条
期望什么的,式子最重要
有些题两遍 d f s 就可以解决(第一遍 d f s 处理子树信息,第二遍 d f s 维护答案)
谨记期望的计算公式:

E =

E ( ) =

一般情况下,我们在转移期望的时候需要之前的期望,所以只有当前一步的花费是一个常量

经典例题:树形dp+期望概率(一)
经典例题:树形dp+期望概率(二)
经典例题:树形dp+期望概率(三)


Kruskal重构树

难度:☆☆☆☆

例题太经典了,绝对展现了Kruskal重构树的优势

Kruskal重构树
经典例题:Kruskal重构树+主席树


虚树

难度:☆☆☆☆☆

虚树简单讲解

经典例题:树形dp+虚树
经典例题:树形dp+虚树

有这样一类问题:给出一棵 n 个结点的树,每次指定 m 个结点,给予ta们一些性质,求出某答案,保证 m n 同阶
显然我们需要基于 m 的算法

虚树实际上是对树的一种简化
一般题目会给出一些关键点
你会发现,那些不是关键点的结点无关紧要
而且我们可以把树上的路径压缩(比如说我们只关心权值和,最大值,最小值)
那么我们就可以考虑建立虚树,只有关键点以及任意两个关键点之间的LCA在虚树上

之后再在这棵虚树上 d p 啊, d f s 啊,该干嘛就干嘛

虚树的构建比较重要
主要就是用一个单调栈维护结点(保证栈中的结点深度单调不降)
每产生一个新结点(不管是LCA还是关键点),我们都要塞入栈中
连边的话,深度小的向深度大的结点连边即可

const int N=100010;
int n,m;
struct node{
    int y,v,nxt;
}way[N];
int st[N],tot=0,dfn[N],clo;
int pre[N][20],deep[N],dis[N][20];
int a[N],top,S[N];

void dfs(int now,int fa,int dep) {
    deep[now]=dep;
    pre[now][0]=fa;
    dfn[now]=++clo;
    for (int i=1;i<20;i++) {
        if ((1<<i)>dep) break;
        pre[now][i]=pre[pre[now][i-1]][i-1];
        dis[now][i]=dis[now][i-1]+dis[pre[now][i-1]][i-1];
    }
    for (int i=st[now];i;i=way[i].nxt)
        if (way[i].y!=fa) {
            dis[way[i].y][0]=way[i].v;
            dfs(way[i].y,now,dep+1);
        }
} 

int LCA(int x,int y) {
    if (deep[x]<deep[y]) swap(x,y);
    int d=deep[x]-deep[y];
    if (d)
        for (int i=0;i<20&&d;i++,d>>=1)
            if (d&1)
                x=pre[x][i];
    if (x==y) return x;
    for (int i=19;i>=0;i--) 
        if (pre[x][i]!=pre[y][i]) {
            x=pre[x][i];
            y=pre[y][i];
        }
    return pre[x][0];
}

void prepare() {
    clo=0;
    dfs(1,0,1);
}

int getlen(int x,int y) {
    int sum=0;
    if (deep[x]<deep[y]) swap(x,y);
    int d=deep[x]-deep[y];
    if (d)
        for (int i=0;i<20&&d;i++,d>>=1)
            if (d&1) {
                x=pre[x][i];
                sum+=dis[x][i];
            }
    if (x==y) return sum;
    for (int i=19;i>=0;i--) 
        if (pre[x][i]!=pre[y][i]) {
            sum+=dis[x][i];
            x=pre[x][i];
            sum+=dis[y][i];
            y=pre[y][i];
        }
    sum+=dis[x][0];
    sum+=dis[y][0];
    return sum;
}

void build(int u,int w) {
    if (u==w) return;
    tot++;
    way[tot].y=w;way[tot].nxt=st[u];st[u]=tot;
    way[tot].v=getlen(u,w);
}

int cmp(const int &x,const int &y) {
    return dfn[x]<dfn[y];                   //按照dfs序排序 
}

void solve() {
    int m;
    for (int i=1;i<=m;i++)
        scanf("%d",&a[i]);                  //读入关键点
    sort(a+1,a+1+m,cmp); 

//  int cnt=0;                              我觉得这一部分很不靠谱 
//  a[++cnt]=a[1];
//  for (int i=2;i<=m;i++)
//      if (lca(a[i],a[cnt])!=a[cnt]) a[++cnt]=a[i]; 
//  m=cnt;

    tot=0;                                   //边 
    memset(st,0,sizeof(st));
    top=0; 
    S[++top]=1;                              //默认根结点 
    for (int i=1;i<=m;i++) {
        int now=a[i];
        int p=LCA(S[top],now);
        while (1) {
            if (deep[p]>=deep[S[top-1]]) {
                build(p,S[top--]);
                if (S[top]!=p) S[++top]=p;
                break;
            }
            build(S[top-1],S[top]);
            top--;
        }
        if (now!=S[top]) S[++top]=now;
    }
    while (top-1) build(S[top-1],S[top]),top--;
}
发布了941 篇原创文章 · 获赞 192 · 访问量 32万+

猜你喜欢

转载自blog.csdn.net/wu_tongtong/article/details/79805366