树形背包模板题(AcWing286 选课)

题目描述

学校实行学分制。每门的必修课都有固定的学分,同时还必须获得相应的选修课程学分。

学校开设了 N门的选修课程,每个学生可选课程的数量 M是给定的。学生选修了这 M门课并考核通过就能获得相应的学分。

在选修课程中,有些课程可以直接选修,有些课程需要一定的基础知识,必须在选了其他的一些课程的基础上才能选修。

例如《Windows程序设计》必须在选修了《Windows操作基础》之后才能选修。我们称《Windows操作基础》是《Windows程序设计》的先修课。每门课的直接先修课最多只有一门。两门课可能存在相同的先修课。

你的任务是为自己确定一个选课方案,使得你能得到的学分最多,并且必须满足先修条件。假定课程之间不存在时间上的冲突。

输入格式
输入文件的第一行包括两个整数 N、M(中间用一个空格隔开)其中 1≤N≤300,1≤M≤N。

接下来 N行每行代表一门课,课号依次为 1,2,…,N。

每行有两个数(用一个空格隔开),第一个数为这门课先修课的课号(若不存在先修课则该项为 0),第二个数为这门课的学分。学分是不超过 10的正整数。

输出格式
输出一个整数,表示学分总数。

输入样例:
7 4
2 2
0 1
0 4
2 1
7 1
7 6
2 2
输出样例:
13

分析

树形背包问题是背包问题和树形DP问题的结合,可以归类于树形DP问题。

回忆下背包问题,在n件物品中选出体积不超过V的物品,使得这些物品价值之和最大。再回忆下典型的树形DP问题AcWing 285 没有上司的舞会,就是在树中选择若干个节点,这些节点之间有某种依赖关系,使得最后选择节点的权值最大。

对于背包问题而言,树形背包问题相当于增加了不同物品之间的依赖关系,实际也就是AcWing 10 有依赖的背包问题;对于树形DP问题而言,树形背包相当于增加了一个选择节点数量不超过m的限制条件。

背包问题是线性DP的一种,是用线性的顺序去进行状态的转移,而树形DP则是在遍历树的过程中进行状态的转移,所以树形背包无非是在遍历树的过程中进行背包问题的状态转移。对于本题而言,假设节点u有a、b、c三个孩子,要想选择a、b、c就必须先选择u。状态表示为: f [ i ] [ j ] f[i][j] f[i][j]表示以 i i i为根的子树中选择 j j j个节点能获得的最大学分。对于 i i i的某个孩子 t t t而言,以 t t t为根的子树中可选择的节点数是0到 j − 1 j-1 j1,状态转移方程为: f [ i ] [ j ] = m a x ( f [ i ] [ j ] , f [ i ] [ j − k ] + f [ j ] [ k ] ) f[i][j]=max(f[i][j],f[i][j-k]+f[j][k]) f[i][j]=max(f[i][j],f[i][jk]+f[j][k])

这个状态转移方程可能没那么容易理解,因为这是使用滚动数组优化后的方程。原来的状态表示应该是三维的,即 f [ i ] [ t ] [ j ] f[i][t][j] f[i][t][j]表示在 i i i的前 t t t棵子树中选择 j j j个节点能获得的最大学分。此时的状态转移方程是 f [ i ] [ t ] [ j ] = m a x ( f [ i ] [ t ] [ j ] , f [ i ] [ t − 1 ] [ j − k ] + f [ s o n ] [ m ] [ k ] ) f[i][t][j]=max(f[i][t][j],f[i][t-1][j-k] + f[son][m][k]) f[i][t][j]=max(f[i][t][j],f[i][t1][jk]+f[son][m][k]),也就是说在 i i i的第 t t t棵子树中选择 k k k个节点,前 t − 1 t-1 t1棵子树中选择 j − t j-t jt个节点。由于每种状态的转移仅用到了上一层状态左边的状态,所以我们可以去掉第二维,倒着遍历 j j j,这样就省去了一维,得到了最终的状态转移方程: f [ i ] [ j ] = m a x ( f [ i ] [ j ] , f [ i ] [ j − k ] + f [ j ] [ k ] ) f[i][j]=max(f[i][j],f[i][j-k]+f[j][k]) f[i][j]=max(f[i][j],f[i][jk]+f[j][k])

具体dfs的代码如下

void dfs(int u) {
    f[u][1] = w[u];
    for(int i = h[u];~i;i=ne[i]) {
        int j = e[i];
        dfs(j);
        for (int t = m;t >=2; t--) {
            for (int k = 0;k < t;k++) {
                f[u][t] = max(f[u][t], f[u][t - k] + f[j][k]);
            }
        }
    }
}

状态边界是 f [ u ] [ 1 ] = w [ u ] f[u][1]=w[u] f[u][1]=w[u],表示如果在以 u u u为根的子树中只选择1个节点,那么一定是选择 u u u。我们假设 d f s ( u ) dfs(u) dfs(u)遍历完就可以求出所有以 u u u为根的子树的状态 f [ u ] [ t ] f[u][t] f[u][t],那么我们在尝试以 u u u的孩子 j j j的状态去更新 u u u的状态前,需要先求出 j j j的状态,也就是做次 d f s ( j ) dfs(j) dfs(j),有了子树的状态,才能进行下一步的状态转移。由于使用了滚动数组,我们需要倒着枚举节点的数量 t t t,最大是 m m m,最小是2,因为1是状态的边界,然后下一重循环枚举下子树 j j j中要选择的节点数 k k k,范围在0到 t − 1 t-1 t1之间。

复杂度分析:正常树的遍历复杂度是 O ( n ) O(n) O(n),也就是说,最外层的循环累加起来是 O ( n ) O(n) O(n),加上里面两层循环总的复杂度就是 O ( n m 2 ) O(nm^2) O(nm2)

优化

在很多问题中, O ( n m 2 ) O(nm^2) O(nm2)的复杂度能够处理的问题规模有限,复杂度还是高了,优化的办法也很简单。在前面的 d f s dfs dfs中,我们枚举 u u u每个子树的状态时选择节点的个数是从0枚举到 t − 1 t-1 t1的,但是 u u u的某个子树可以选择的节点数也应该不超过它的规模,所以如果我们在 d f s dfs dfs的过程中顺便统计下树的规模,对枚举的节点数做下限制,时间复杂度也会相应的降低。

int dfs(int u) {
    f[u][1] = w[u];
    int s = 1;
    for(int i = h[u];~i;i=ne[i]) {
        int j = e[i];
        int cnt = dfs(j);
        s += cnt;
        int tot = min(m, s + 1);
        for (int t = tot;t >=2; t--) {
            cnt = min(cnt, t - 1);
            for (int k = 0;k <= cnt;k++) {
                f[u][t] = max(f[u][t], f[u][t - k] + f[j][k]);
            }
        }
    }
    return s;
}

观察上面的代码,我们在枚举 u u u的某棵子树时,能够选择的节点数不会超过已经遍历的节点总和;同理,某棵子树上能够选择的节点数量 k k k,也不会超过该子树的规模,修改了内部两层循环的上界后复杂度降低到了 O ( n m ) O(nm) O(nm)。(复杂度证明比较复杂,这里就偷点懒不再证明了)

代码

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 305;
int h[N],e[N],ne[N],w[N],idx;
int n,m;
int f[N][N];
void add(int a,int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

int dfs(int u) {
    f[u][1] = w[u];
    int s = 1;
    for(int i = h[u];~i;i=ne[i]) {
        int j = e[i];
        int cnt = dfs(j);
        s += cnt;
        int tot = min(m, s + 1);
        for (int t = tot;t >=2; t--) {
            cnt = min(cnt, t - 1);
            for (int k = 0;k <= cnt;k++) {
                f[u][t] = max(f[u][t], f[u][t - k] + f[j][k]);
            }
        }
    }
    return s;
}
int main() {
    int p;
    cin>>n>>m;
    memset(h,-1,sizeof h);
    for (int i = 1;i <= n;i++) {
        cin>>p>>w[i];
        add(p, i);
    }
    m++;
    dfs(0);
    cout<<f[0][m]<<endl;
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值