[week13] D - TT的苹果树(选做)—— 树型DP

    技术2023-07-18  77

    文章目录

    题意InputOutput输入样例输出样例提示 分析总结代码

    题意

    在大家的三连助攻下,TT 一举获得了超级多的猫咪,因此决定开一间猫咖,将快乐与大家一同分享。并且在开业的那一天,为了纪念这个日子,TT 在猫咖门口种了一棵苹果树。

    一年后,苹果熟了,到了该摘苹果的日子了。

    已知树上共有 N 个节点,每个节点对应一个快乐值为 w[i] 的苹果,为了可持续发展,TT 要求摘了某个苹果后,不能摘它父节点处的苹果。

    TT 想要令快乐值总和尽可能地大,你们能帮帮他吗?

    Input

    结点按 1~N 编号。

    第一行为 N (1 ≤ N ≤ 6000) ,代表结点个数。

    接下来 N 行分别代表每个结点上苹果的快乐值 w[i](-128 ≤ w[i] ≤ 127)。

    接下来 N-1 行,每行两个数 L K,代表 K 是 L 的一个父节点。

    输入有多组,以 0 0 结束。

    Output

    每组数据输出一个整数,代表所选苹果快乐值总和的最大值。

    输入样例

    7 1 1 1 1 1 1 1 1 3 7 4 2 3 4 5 6 4 3 5 0 0

    输出样例

    5

    提示


    分析

    这是一道典型的利用树型DP解决的问题。


    树型DP

    1. 什么是树型DP?

    顾名思义,就是在树型结构上进行的动态规划。由于树结构的特殊性,树型DP一般都是递归进行。

    树型DP中,一般来说父节点所获的的解与其子节点有关。因此在计算父节点的时候,需要知道其子节点的所有解。显然这需要通过递归来实现。

    2. 无根树转有根树

    假如题目只给出了一棵树的所有边,则我们是不知道这棵树的根节点的。

    那么该如何将这棵没有根的树变成一个有根的树呢?

    首先确定一个点作为根节点 然后从该点开始对这棵树作dfs 将每次dfs传入的点作为一个父节点,与该点直接相连的所有点都将这个点标记为他们的父节点。 将与当前点相连的点作为新的父节点递归调用新的dfs。 若本次dfs搜索重新回到了当前父节点的父节点,则结束本次递归结束开始回溯。

    在搜索过程中用一个数组来记录每个节点的父节点。

    3. 经典题型

    移动距离总和

    这一类题主要关注的是边连接的两个节点,这两个节点在作为父节点时对应的两棵子树。

    🌰

    对该题的粗略分析如下:

    移动距离总和这个概念可以细分为在每一条边上移动次数的计算,这确实很巧妙。显然整棵树上的移动距离最大总和就是所有边与在该边最大移动次数的乘积的总和。

    而每条边的最大移动次数就和这条边上连接的两点对应的子树有关。两棵子树中含有节点较少的一边全部移动到另一棵子树上进行交换,显然是最大的移动次数。因此,关键在于求解每条边上连接的两点对应的两棵子树中含有的节点个数。

    节点个数可以通过递归求解。另一棵子树的节点总数显然就是整棵树的节点总数与已求出子树节点个数的的差。

    没有上司的舞会

    非常经典的常见的树型DP问题:

    这个问题中的树型结构也比较清晰。显然上司就是父节点,其直属员工就是其在树型结构上的子节点。

    在这个问题中,父节点的去留直接决定了子节点的去留,因此就形成了树型结构上父子节点之间子解的相互关联。

    (1)定状态:

    (2)状态转移方程

    这个还是很好理解的:

    当上司不去时,其所有员工都可以去,也可以不去。因此,此时上司节点不去对应的最优解应该等于其所有直属员工的最大值之和。每个员工的最大值是其去和不去两种状态中的一种,而不是一定是不去或去。

    当上司要去时,显然所有员工都不能去。因此该状态对应的解就为上司的值加其直属的所有员工不去对应的值。

    树上背包


    题目分析

    读完题目应该就知道是典型的“没有上司的舞会”。按照该类型题目的思路进行求解即可。

    利用邻接连表存储树结构 用一个数组存储所有节点对应的值 用一个布尔数组标记出该树的根节点,即讲所有有父节点的节点进行标记,剩下的就是根节点(没有父节点的节点) 从根节点开始对整棵树进行DP 输出根节点两种状态中的最大值

    在递归求解的过程中,每次传入参数为父节点。遍历该节点的所有子节点,依次叠加求出当前父节点两种状态对应的最优解。

    需要注意的是,在叠加计算之前,先对每个子节点进行递归。


    问题

    最大的问题是如何以0 0结束输入【??】

    最开始是用一个循环来输入所有的父子关系,当有输入时继续读入,若读入0 0则进行标记。

    但是天真的我没有想到的是,在有多组数据时,虽然看上去没有同一行继续输入两个数据,但是其后仍然是有数据在输入流中的。

    因此可以用输入n是否等于0来判断。若n输入0,就说明此时是输入的0 0中的第一个0,可以判断为结束。


    总结

    没有上司的舞会算是后面的DP中最好理解的了555

    代码

    // // main.cpp // lab4 // // #include <iostream> #include <vector> #include <string.h> #include <algorithm> using namespace std; vector<int> tree[6001]; //存储树结构 int weight[6001]; //存储每个节点的值 int f[6001][2]; //存储每个节点的dp结果 int n = 0,w = 0,a = 0,b = 0,start = 0; bool father[6001]; //标记树的根节点 void dp(int idx) //以当前idx节点为父节点 { f[idx][0] = 0; //不选择当前节点,初始化为0 f[idx][1] = weight[idx]; //选择当前节点,初始化为该节点的值 for( int i = 0 ; i < tree[idx].size() ; i++ ) //遍历该节点的孩子节点 { int bot = tree[idx][i]; dp(bot); //对该孩子进行dp //不选择该父节点时,该节点的dp值为所有孩子节点的最大dp值的和 f[idx][0] = f[idx][0] + max(f[bot][1],f[bot][0]); //选择该父节点,则所有孩子都只能不选择 f[idx][1] = f[idx][1] + f[bot][0]; } } int main() { ios::sync_with_stdio(false); while(1) { cin>>n; if( n == 0 ) //当输入0 0时停止 { cin>>a; break; } memset(weight, 0, sizeof(weight)); //初始化 memset(father, 0, sizeof(father)); for( int i = 1 ; i <= n ; i++ ) //输入所有值 { cin>>weight[i]; tree[i].clear(); } for( int i = 0 ; i < n - 1 ; i++ ) //建树 { cin>>a>>b; tree[b].push_back(a); father[a] = true; } for( int i = 1 ; i <= n ; i++ ) //找到根节点 { if( !father[i] ) { start = i; break; } } dp(start); //从根节点开始dp cout<<max(f[start][1], f[start][0])<<endl; //输出根节点两种情况中的最大值 } return 0; }
    Processed: 0.017, SQL: 9