动态规划 树形dp系列:没有上司的舞会----中专生刷算法

树形dp就是在树形结构上的动态规划

难点

  1. 和线性动态规划相比,树形DP往往是要利用递归+记忆化搜索。
  2. 细节多,较为复杂的树形DP,从子树,从父亲,从兄弟……一些小的要处理的地方,脑子不清晰的时候做起来颇为恶心
  3. 状态表示和转移方程,也是真正难的地方。做到后面,树形DP的老套路都也就那么多,难的还是怎么能想出转移方程,各种DP做到最后都是这样!
  4. 难点总结于endl的博客

题目:没有上司的舞会

输入输出格式以及数据范围

输入样例:
7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5
输出样例:
5

提前需要知道邻接表的存储(树形是特殊的邻接表)

实际上是拿n个一维单链表,指向该头能到的所有节点

//e[idx]表示第idx条边的终点,也是子节点的值
// ne[idx]表示和第idx条边同起点的下条边的idx是多少
//h[a]表示节点a的第一条边父节点,链表头
//idx表示边的下标,边的顺序不重要,最后能联通即可
int h[N], e[M], ne[M], idx;
//a是b的父节点
 void add(int a,int b){
     e[idx] = b;// 记录 加入的边 的终点节点
     // h[a] 表示 节点 a 为起点的第一条边的下标
     //ne[idx] = h[a] 表示把 h[a] 这条边接在了 idx 这条边的后面
     //其实也就是把 a 节点的整条链表 接在了 idx 这条边 后面;目的就是为了下一步 
     //把 idx 这条边 当成 a 节点的单链表的 第一条边,完成把最新的一条边插入到 链表头的操作;
     ne[idx] = h[a];
     h[a] = idx; // a节点开头的第一条边为当前边
     idx++;//,idx移动到下一条边,方便下次调用本函数时,用新的边连接
}

我个人认为idx的主要作用还是把三个数组互相绑定,实际上没啥用

  1. e[]:这个数组用于存储边的目标节点。在图的上下文中,如果你有一条边从节点a到节点b,那么b就被存储在e[]数组中。
  2. ne[]:这个数组通常用于存储下一条边的索引。它的作用是帮助你遍历从同一个节点出发的所有边,即链表中的“next”指针。
  3. h[]:这个数组用于存储每个节点的边的头部索引。在添加一条边时,你会更新h[a]来指向这条新边的索引。
  4. idx:这是一个计数器,用于记录当前已经添加到数组中的边的数量。每次添加一条边时,idx都会增加,确保边和它们的索引能够正确对应。

用样例举一个例子

1->3
2->3
6->4
7->4
4->5
3->5

h数组全部初始化值为-1

此图:acwing冰中月

我们随便从一个节点开始,例如3

h[3]存储着3节点对应的idx,我们使用这个idx,配合e[],e[idx]对应的就是b,父节点的值,也就是3

这里的父节点是相较于idx线段,例如1->3,这里3就是父值,en[idx],存储的是另外一条边的值,也就是3->2这条边,

h[a]+e[idx]可以深搜节点的子节点,通过h[a]+en[idx]+e[i]可以宽搜切换线路

深搜例如:int i=h[a],则e[i]=h[a]子节点值,int b=e[i],int j=h[b],则e[j]=h[b]子节点值....

宽搜例如:i==h[a],ne[h[a]]也就是ne[i]=下一条边,int j=ne[i],e[j]等于h[a]下一条边的子节点的值

由于初始h数组全部初始化值为-1,所以当h[n]==-1时,越界,超出图,结束深搜或者宽搜

验证

不会有漏查的情况,首先深搜从根节点开始,不会漏查

假设宽搜的抽象理解是从左往右逐个搜索

那最右子节点一定是最开始被添加的,因为h[a]屡次被idx更新,最后我们用h[a]时,对应的一定是最大的idx值

先添加的就是最右节点,在一个新节点中,ne[idx]在第一次等于h[a]值时,h[a]在被更新前,对应的值一定是-1

h[a]在更新前对应值-1,则ne[idx]对应值为-1, 代表我们搜到了最右处。

也就是在宽度搜索时的最后个节点,所以搜索到-1时,一定是宽度结束

画图理解宽搜为什么不越界

1->3
2->3
6->4
7->4
4->5
3->5
//样例中是先子节点,后父节点,我们调换一下
3->1
3->2
4->6
4->7
5->4
5->3

理解了邻接表,那这题就好做了

上闫氏dp分析法

代码实现

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 6010;
int f[N][2];//dp数组,不理解看上图
int hy[N];//记录高兴度
int h[N],e[N],ne[N],idx;//链表模拟数,邻接表
bool boolf[N];//判断是否有父节点,好从根节点开始计算
//将a,b插入数中,a是父节点
void add(int a,int b){
    e[idx]=b;
    ne[idx]=h[a];
    h[a]=idx;
    idx++;
}
void dfs(int u){
    f[u][1]=hy[u];//添加上高兴度
    //下面的代码是深搜和宽搜子节点
    //循环遍历的是本行,对应宽搜,~1表示1!=-1,位运算
    for(int i=h[u];~i;i=ne[i]){
        int j=e[i];//j是子节点的值
        //函数递归对应深搜
        dfs(j);
        //不选,等于子节点选或者不选的最大方案集合的最大高兴值
        //因为是横向宽搜,确定每一个子节点选或者不选,所以要用+=来保留值,毕竟不止一个子节点
        f[u][0]+=max(f[j][0],f[j][1]);
        //选,那子节点肯定是不选
        f[u][1]+=f[j][0];
    }
}
int main(){
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)cin>>hy[i];
    //初始化原始值为-1
    memset(h,-1,sizeof h);
    for(int i=1;i<=n-1;i++){
        int a,b;
        cin>>b>>a;
        //插入邻接表中
        boolf[b]=true;//标注b有父节点
        add(a,b);
    }
    // for(int i = 1;i < n;i ++){
    //     int a,b; //对应题目中的L,K,表示b是a的上司
    //     scanf("%d%d",&a,&b); //输入~
    //     boolf[a] = true; //说明a他有上司
    //     add(b,a); //把a加入到b的后面
    // }

    //开始找根节点
    int root=1;
    while(boolf[root])root++;
    dfs(root);//从根节点开始找最大方案属性
    //选根节点和不选根节点两者里的最大方案,就是结果的最大方案
    printf("%d",max(f[root][1],f[root][0]));
    return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值