Loading

树形dp总结:从入门到放弃

一、如何拥有一棵树 how to build a tree

存树的方法有很多种,我个人常用的有:

链式前向星存图法

因为树也是一种图,所以说对于存图法是适用的。缺点就是不好利用树的性质。

因为比较普遍,教程也很多,就不再赘述了。

左儿子右兄弟法

 1 struct node //结构体存树
 2 {
 3     int lson,rson;//因为转变为二叉树,所以只有两个儿子
 4 }a[N];//这个好像叫tree更标准
 5 
 6 void findplace(int x,int y)//x是根,y是要插入的儿子
 7 {
 8     int pos=a[x].lson;//这是在x已经有第一个儿子的情况
 9     while(a[pos].rson!=-1)//循环找直到右儿子(兄弟位)空出
10         pos=a[pos].rson;
11     a[pos].rson=y;//成功存入
12 }
13 
14 int main()
15 {
16     for(int i=0;i<n;i++)//初始化
17     {
18         a[i].lson=a[i].rson=-1;
19     }
20     for(int i=1;i<=n;i++)
21     {
22         cin>>x>>y>>w;
23         a[y].val=w;
24         if(a[x].lson==-1)//x还没有儿子就可以充任
25             y=a[x].lson;
26         else
27             findplace(x,y);//否则就适用上面的函数了
28     }
29 }

优点是可以非常方便地遍历整个树,缺点是有点慢。但是在操作系统内部,文件夹之间似乎也是这么存放的呢。

遍历的方法:

void printree(int pos)
{
    cout<<pos<<endl;
    int r=a[pos].rson,l=a[pos].lson;
    while(r!=-1)//先打印兄弟
    {
        printree(r);
        r=a[r].rson;
    }
    if(l!=-1)//再考虑儿子
        printree(l);
}

二、如何状态转移 how to dp

树上的dp一般状态转移都是和子树相关的,而且第一维一般而言都是当前节点的标号。利用记忆化搜索其实能更好利用树,刷表填表我都不太适应……

普通dp比较常用的考虑方法是f[i][0/1]表示点i选/不选的情况下,其子树的最优情况

如果是树形背包,那么f[i][j]表示在以点i为根节点的子树中,选择j个点的最优情况

当然这个要视具体情况分析而定。

需要注意的是树形背包的转移方程通常是各个子树的份额分配问题,然后取max或者相加或者玄学操作。当这棵树是二叉树时可以简单地使用单重循环,其他需要多重循环。这时候循环的边界就变得重要了。通常可以考虑记录size(子树节点个数)、直接儿子个数什么的或者其他的题目条件来优化边界。

一般而言要考虑的问题:叶子节点(也就是边界)、选择和不选择时对应的子节点情况(有什么要求)、根节点怎么处理等。如果没有根节点,可以考虑构建一个虚根;如果题目中是边权,可以考虑下沉至点权来做。注意根节点是不是占用名额!这个是比较重要的。

三、如何水经验 how to get exp

luogu上P201x的题目很多都是树形dp的。个人的成长路线是:

1.没有上司的舞会(经典dp)

2.二叉苹果树(树形背包入门)

3.选课(树上依赖背包入门)

4.战略游戏(普通dp)

5.“访问”美术馆(有意思)

5.有线电视网(更有意思)

 

本蒟蒻比较菜,所以底线就到这里啦。

posted @ 2019-10-30 15:29  喻文州  阅读(364)  评论(0编辑  收藏  举报