树形dp整理及入门

树形dp常用作解三种题:

1.最大独立子集

最大独立子集的定义是,对于一个树形结构,所有的孩子和他们的父亲存在排斥,也就是如果选取了某个节点,那么会导致不能选取这个节点的所有孩子节点,同样,它的父亲也不能被选择。

一般询问是要求给出当前这颗树的最大独立子集的大小(被选择的节点个数)。

思路:对于任意一个节点,他都有两种选择:

Father.Yes表示当前Father节点选择的时候,能拿到的最大独立子集大小。

Father.No表示当前Father节点不选的时候,能拿到的最大独立子集大小

A . 选择:选择当前节点Father,那么他的孩子必定不能被选择,此时对于Father和Father的孩子们来说,能构成的最大独立子集是

Father.Yes=1//选择自身
for(i=0;i<Father.sons.size();++i)
    Father.Yes+=Father.sons[i].No;

B . 不选择:不选择当前节点Father,那么对于他的每一个孩子来说都可以选择要或者不要,此时对于Father和Father的孩子们来说,此时能构成的最大独立子集是

for(i=0;i<Father.sons.size();++i)  
    Father.No+=max(Father.sons[i].No,Father.sons[i].Yes);

当然可以在这基础上加以变化,比如给点设置权值,要求求得的这个最大独立子集是满足有最大权值和的。

2.树的重心

树的重心x定义为,当将节点x以及由x衍生出来的边全部去掉后,树会形成多个联通子集,当树所形成的各个联通子集的节点个数的最大值最小,即拥有最大节点数的联通子集的节点数要求最小(因为重心选择的不同会导致拥有最大节点数的联通子集的节点数是不同的),那么X就是树的重心。

那么显而易见,我们需要对每一个节点维护两个值。一个是包含他在内的所有节点的个数【用来返回给他们的‘父亲’】,一个是这个点的最大子树所含有的节点个数。

虽然说是一棵树,但其实本身应该作为无根树考虑,也就是,父亲并不是严格意义上的父亲,如下图:

        在考虑C的时候,我们应该把C当成整棵树的根,因此C有三棵子树,但是在真正实现的时候,这种方法的复杂度是O(n^2)的,并不好。实际上,根据树的性质我们可以先求出C在向下遍历时的孩子个数sum,往上走的分支只要用n-sum就得到了。我们要求的节点就是拥有最大子树的节点。

我们讨论一下当去掉某一个节点会发生什么:

        以C为例,去掉之后,他的两个子树会独立成两个连通块,同时上面的A-B也会独立,因此总共产生3个连通块。在这个过程中我们大致可以发现,某个节点被去掉后,他有多少个孩子(直接孩子),就会产生多少个子联通集合。而剩余的部分,也会成为一个子联通集合。

        当然有些边界并不是这样,如果被去掉的节点是叶子,那么此时就不存在孩子独立成联通子集合了。另外如果去掉的节点是整棵树的根(如上图结构中的A),那么也就不存在剩余部分构成的子树了。

        我们先来看求法:

                1.对于子树的节点总数,可以在DFS过程中维护

                2.对于剩余部分的节点数,等价于求 总节点数 - 当前节点(1个) - 当前节点的所有子树大小

        通过这个求法我们可以发现,对于叶子来说,无非就是子树大小全为0,对于根节点来说,无非就是剩余部分为0~

3.树的直径

树的直径定义为一棵树上两个节点间的路径长度的最大值。

一般思路:一棵树上n个节点,两两配对可以组成n*(n-1)/2,所以我们可以对n个节点都做一次dfs,并求得最长的路径,这样这些配对必定会在dfs的过程中求出,这样复杂度就是O(n*n),n次遍历,每次都要遍历除了自己以外的n-1个节点。

进阶思路:我们以任意一个节点作为根节点,得到他的最高子树的叶子节点,那么这个节点必定是其中的一个节点,在以这个点为起点,dfs得到和他最大距离的点,这条路就是最长路径,复杂度为O(2*n),两次dfs。

树形dp:我们以任意一个点为根,向下进行遍历,而经过这个点的最大路径一定是在他的不同的子树中,因此我们可以记录这个点的各个子树的高度的最大值和次大值。累加即为经过这个节点时的最长路经长度。

疑问:题目并没有限制一个节点只能往下找,同样可以往上找,那为什么不往上找呢。解答:往上找出来的路径必定会经过他的父亲,问题就由经过孩子的最大路径变成了经过父亲的最大路径,问题的本质是没有变的,因为这样找出来的路径,父亲和孩子是相同的,是同时经过父亲和孩子的,也就是同样的问题,那么这个问题归根到底就是经过根的最大路径,所以问题重复了,因此我们不必往上找。

树形dp的实现:

对于有根树,可以考虑建树递归,在回溯的时候更新结果。

对于无根树,用vector实现邻接表存储。

存储结构:

A. 无权值

vector<int>Picture[n+1];

遍历方式为:

void dp(int now,int pre){
    int len=Picture[now].size();
    for(int i=0;i<len;i++) {
        if(Picture[now][i]==pre)continue;//不能回父亲 否则递归不能结束  会吃尽内存导致程序崩溃抑或是爆栈等
        dp(Picture[now][i],now);
    }
} 

下面给出三种类型题目的代码:【注:下面的遍历方式都是默认树节点从1开始的,如从0开始将递归入口search(1,0)修改为search(0,-1)即可】

1.最大独立子集

#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
vector<int>Node[1000005];
int Maxsize,n;//最大独立子集大小 

struct returnType{
    int come,notcome;
};

returnType search(int now,int pre){
    returnType fa,son; 
    int len=Node[now].size(),soncome,sonnotcome;
    fa.come=1;//总结点数先算上自身1 
    fa.notcome=0;//最大子树先设置为0 
    for(int i=0;i<len;i++){
        if(Node[now][i]==pre){continue;}//不回父亲
        son=search(Node[now][i],now);
        fa.come+=son.notcome;
        fa.notcome+=max(son.come,son.notcome);
    }
    return fa;
}

int main(){
    int u,v;
    scanf("%d",&n);
    for(int i=1;i<n;i++){
        scanf("%d%d",&u,&v);
        Node[u].push_back(v);
        Node[v].push_back(u);
    }
    returnType father=search(1,0);
    printf("%d",max(father.come,father.notcome));
    return 0;
}

2.树的重心

#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
int num[1000005],size[1000005],n;//num :节点数   size :最大子树的节点数
vector<int>Node[1000005];
int target,Maxsize;//重心与最大子树尺寸 

void search(int now,int pre){
    int len=Node[now].size(),result,rest;
    num[now]=1;//总结点数先算上自身1 
    size[now]=0;//最大子树先设置为0 
    for(int i=0;i<len;i++){
        if(Node[now][i]==pre){continue;}//不回父亲
        search(Node[now][i],now);
           size[now]=max(size[now],num[Node[now][i]]);
        num[now]+=num[Node[now][i]]; 
    }
    rest=n-num[now];
    size[now]=max(size[now],rest);
       if(Maxsize<size[now]){
           Maxsize=size[now];
           target=now;
    }
}

int main(){
    int u,v;
    scanf("%d",&n);
    for(int i=1;i<n;i++){
        scanf("%d%d",&u,&v);
        Node[u].push_back(v);
        Node[v].push_back(u);
    }
    search(1,0);
    printf("%d",target);
    return 0;
}

哇,有没有感觉num和size数组是在太耗内存了。没关系,咱们有奇技淫巧。

#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
vector<int>Node[1000005];
int n,target,Maxsize;//重心与最大子树尺寸 

struct returnType{
    int size;
    int num;
};

returnType search(int now,int pre){
    returnType fa,son; 
    int len=Node[now].size(),result,rest;
    fa.num=1;//总结点数先算上自身1 
    fa.size=0;//最大子树先设置为0 
    for(int i=0;i<len;i++){
        if(Node[now][i]==pre){continue;}//不回父亲
        son=search(Node[now][i],now);
           fa.size=max(fa.size,son.num);
        fa.num=son.num; 
    }
    rest=n-fa.num;
    fa.size=max(fa.size,rest);
       if(Maxsize<fa.size){
           Maxsize=fa.size;
           target=now;
    }
    return fa;
}

int main(){
    int u,v;
    scanf("%d",&n);
    for(int i=1;i<n;i++){
        scanf("%d%d",&u,&v);
        Node[u].push_back(v);
        Node[v].push_back(u);
    }
    search(1,0);
    printf("%d",target);
    return 0;
}

其实对一个节点来说,只是需要它的孩子们的信息,所以并没有必要开个数组存下来所有的数据。

3.树的直径

#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
int first[1000005],second[1000005];//树形dp求解最长路
vector<int>Node[1000005];
int longest;
void search(int now,int pre){
    int len=Node[now].size(),result;
    for(int i=0;i<len;i++){
        if(Node[now][i]==pre)continue;//不回父亲
        search(Node[now][i],now);
        if(first[Node[now][i]]+1>first[now]){
            second[now]=first[now];
            first[now]=first[Node[now][i]]+1;
        }
        else if(first[Node[now][i]]+1>second[now]){
            second[now]=first[Node[now][i]]+1;
        }
    }
    longest=max(first[now]+second[now],longest);
}

int main(){
    int n,u,v;
    scanf("%d",&n);
    for(int i=1;i<n;i++){
        scanf("%d%d",&u,&v);
        Node[u].push_back(v);
        Node[v].push_back(u);
    }
    search(1,0);
    printf("%d",longest+1);
    return 0;
}

  • 23
    点赞
  • 90
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值