树形dp

目录
  • 树形dp
    • 1.算法分析
    • 2. 典型例题
      • 2.1 统计树上信息:树的直径、树的中心、树的重心
        • 2.1.1 母题
        • 2.1.2 树的中心(二次扫描与换根法)
        • 2.1.3 树的重心
        • 2.1.4 树的直径
      • 2.2 树形背包问题

树形dp

1.算法分析

    在树上统计一些信息,可以使用树形dp来处理,一般的写法有递归和递推,递归写法最为常见。
常见的问题有:

  1. 求树的直径、树的重心、树的中心
  2. 树形背包dp
  3. 二次扫描与换根法

2. 典型例题

2.1 统计树上信息:树的直径、树的中心、树的重心

2.1.1 母题

acwing285 没有上司的舞会

Ural大学有N名职员,编号为1~N。
他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。
每个职员有一个快乐指数,用整数 Hi 给出,其中 1≤i≤N。
现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。
在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。

输入格式
第一行一个整数N。
接下来N行,第 i 行表示 i 号职员的快乐指数Hi。
接下来N-1行,每行输入一对整数L, K,表示K是L的直接上司。

输出格式
输出最大的快乐指数。

数据范围
1≤N≤6000,
−128≤Hi≤127

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

输出样例:
5

/*
状态表示:f[i][0]节点i不选的最大快乐值,f[i][1]节点i选择的最大快乐值
状态划分:子树的选择情况
状态转移:f[i][0] = 累加{max(f[j][0], f[j][1])}
         f[i][1] = 累加{f[j][0]}, f[i][1] += w[u]
入口:    f[all] = 0
出口:    max(f[1][0], f[1][1])
*/

#include 

using namespace std;

int const N = 6e3 + 10, M = N * 2;
int e[M], ne[M], w[N], h[N], idx;
int n, f[N][2];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void dp(int u, int fa)
{
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == fa) continue;
        dp(j, u);
        f[u][0] += max(f[j][0], f[j][1]);
        f[u][1] += f[j][0];
    }
    f[u][1] += w[u];
    return;
}

int main()
{
    memset(h, -1, sizeof h);
    cin >> n;
    for (int i = 1; i <= n; ++i) scanf("%d", &w[i]);
    for (int i = 1; i <= n - 1; ++i)
    {
        int a, b;
        scanf("%d %d", &a, &b);
        add(b, a);
        add(a, b);
    }
    
    dp(1, -1);
    cout << max(f[1][0], f[1][1]) << endl;
    return 0;
}

acwing1077皇宫看守

太平王世子事件后,陆小凤成了皇上特聘的御前一品侍卫。
皇宫各个宫殿的分布,呈一棵树的形状,宫殿可视为树中结点,两个宫殿之间如果存在道路直接相连,则该道路视为树中的一条边。
已知,在一个宫殿镇守的守卫不仅能够观察到本宫殿的状况,还能观察到与该宫殿直接存在道路相连的其他宫殿的状况。
大内保卫森严,三步一岗,五步一哨,每个宫殿都要有人全天候看守,在不同的宫殿安排看守所需的费用不同。
可是陆小凤手上的经费不足,无论如何也没法在每个宫殿都安置留守侍卫。
帮助陆小凤布置侍卫,在看守全部宫殿的前提下,使得花费的经费最少。

输入格式
输入中数据描述一棵树,描述如下:
第一行 n,表示树中结点的数目。
第二行至第 n+1 行,每行描述每个宫殿结点信息,依次为:该宫殿结点标号 i,在该宫殿安置侍卫所需的经费 k,该结点的子结点数 m,接下来 m 个数,分别是这个结点的 m 个子结点的标号 r1,r2,…,rm。
对于一个 n 个结点的树,结点标号在 1 到 n 之间,且标号不重复。

输出格式
输出一个整数,表示最少的经费。

数据范围
1≤n≤1500

输入样例:
6
1 30 3 2 3 4
2 16 2 5 6
3 5 0
4 4 0
5 11 0
6 5 0

输出样例:
25
样例解释:
在2、3、4结点安排护卫,可以观察到全部宫殿,所需经费最少,为 16 + 5 + 4 = 25。

/*
f[i][0]//第i个结点的父结点被选
f[i][1]//第i个结点有一个子节点被选
f[i][2]//第i个节点本身被选

转移可以设计为
f[u][0] += min(f[v][1],f[v][2])
f[u][1]的转移较为复杂,只需要一个子节点被选即可,所以选择一个最好的子节点来选,具体看代码
f[u][2] += min(f[v][1],f[v][2],f[v][0]);
*/
#include 
#include 
#include 

using namespace std;

const int N = 1510;

int n;
int h[N], w[N], e[N], ne[N], idx;
int f[N][3];
bool st[N];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

void dfs(int u)
{
    f[u][2] = w[u];

    int sum = 0;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        dfs(j);
        f[u][0] += min(f[j][1], f[j][2]);
        f[u][2] += min(min(f[j][0], f[j][1]), f[j][2]);
        sum += min(f[j][1], f[j][2]);
    }

    f[u][1] = 1e9;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        f[u][1] = min(f[u][1], sum - min(f[j][1], f[j][2]) + f[j][2]);
    }
}

int main()
{
    cin >> n;

    memset(h, -1, sizeof h);
    for (int i = 1; i <= n; i ++ )
    {
        int id, cost, cnt;
        cin >> id >> cost >> cnt;
        w[id] = cost;
        while (cnt -- )
        {
            int ver;
            cin >> ver;
            add(id, ver);
            st[ver] = true;
        }
    }

    int root = 1;
    while (st[root]) root ++ ;

    dfs(root);

    cout << min(f[root][1], f[root][2]) << endl;

    return 0;
}

2.1.2 树的中心(二次扫描与换根法)

acwing1073. 树的中心

给定一棵树,树中包含 n 个结点(编号1~n)和 n−1 条无向边,每条边都有一个权值。

请你在树中找到一个点,使得该点到树中其他结点的最远距离最近。

输入格式
第一行包含整数 n。

接下来 n−1 行,每行包含三个整数 ai,bi,ci,表示点 ai 和 bi 之间存在一条权值为 ci 的边。

输出格式
输出一个整数,表示所求点到树中其他结点的最远距离。

数据范围
1≤n≤10000,
1≤ai,bi≤n,
1≤ci≤105

输入样例:
5
2 1 1
3 2 1
4 3 1
5 1 1

输出样例:
2

/*
本题要求树的中心,需要找到到其他点最远距离最小的点,那么就需要得到每个点向上最大距离和向下最大距离
两个距离的和最小的那个值
可以分别向上dfs和向下dfs求的向上和向下的最大距离
值得注意的是,向下dfs时是利用子节点距离来更新父节点的距离,而向上dfs时是利用父节点来更新子节点的距离
*/
#include
 
using namespace std;

int n;
int const N = 2e4 + 10, INF = 1e9;
int e[N], ne[N], w[N], idx, h[N];
int d1[N], d2[N], up[N], p1[N], p2[N];  // d1[u]=j表示u的向下最大距离为j,d2[u]=j表示d的次大距离等于j
// up[u] = j表示u的向上最大距离等于j,p1[u] = j表示u的最大距离的第一个子节点为j,p2[u] = j表示u的次大距离的第一个子节点为j

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
 
// 向下dfs求的向下的最大距离
int dfs_d(int u, int fa)
{
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (j == fa) continue;
        int d = dfs_d(j, u) + w[i];
        if (d >= d1[u])
        {
            d2[u] = d1[u], p2[u] = p1[u];
            d1[u] = d, p1[u] = j;
        }
        else if (d > d2[u])
        {
            d2[u] = d;
            p2[u] = j;
        }
    }

    return d1[u];
}

// 向上dfs求的向上的最大距离
void dfs_u(int u, int fa)
{
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (j == fa) continue;
        if (p1[u] == j) up[j] = max(up[u], d2[u]) + w[i];  // 向上的距离由up[u]和u的最(次)大距离来更新
        else up[j] = max(up[u], d1[u]) + w[i];
        dfs_u(j, u);
    }
}

int main()
{
    // freopen("in.txt", "r", stdin);
    // freopen("out.txt", "w", stdout);

    memset(h, -1, sizeof h);
    cin >> n;
    for (int i = 0; i < n - 1; ++i)
    {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        add(a, b, c);
        add(b, a, c);
    }

    // 求的向上和向下的最大距离
    dfs_d(1, -1);
    dfs_u(1, -1);

    // 更新答案
    int res = INF;
    for (int i = 1; i <= n; ++i) res = min(res, max(up[i], d1[i]));
    cout << res << endl;
    return 0;
}

acwing287. 积蓄程度

有一个树形的水系,由 N-1 条河道和 N 个交叉点组成。
我们可以把交叉点看作树中的节点,编号为 1~N,河道则看作树中的无向边。
每条河道都有一个容量,连接 x 与 y 的河道的容量记为 c(x,y)。
河道中单位时间流过的水量不能超过河道的容量。
有一个节点是整个水系的发源地,可以源源不断地流出水,我们称之为源点。
除了源点之外,树中所有度数为 1 的节点都是入海口,可以吸收无限多的水,我们称之为汇点。
也就是说,水系中的水从源点出发,沿着每条河道,最终流向各个汇点。
在整个水系稳定时,每条河道中的水都以单位时间固定的水量流向固定的方向。
除源点和汇点之外,其余各点不贮存水,也就是流入该点的河道水量之和等于从该点流出的河道水量之和。
整个水系的流量就定义为源点单位时间发出的水量。
在流量不超过河道容量的前提下,求哪个点作为源点时,整个水系的流量最大,输出这个最大值。

输入格式
输入第一行包含整数T,表示共有T组测试数据。
每组测试数据,第一行包含整数N。
接下来N-1行,每行包含三个整数x,y,z,表示x,y之间存在河道,且河道容量为z。
节点编号从1开始。

输出格式
每组数据输出一个结果,每个结果占一行。
数据保证结果不超过231−1。

数据范围
N≤2∗105

输入样例:
1
5
1 2 11
1 4 13
3 4 5
4 5 10

输出样例:
26

/*
本题的解法和求数的中心类似
可以维护一个f[x]表示以x为根的子树的可以向下流出的最大值
那么f[x] = 累加【min(c1, f[yi])】(d[yi] != 1) 
    f[x] += w[i]

那么 x点能够流出的最大流量=x点向下流出的流量+x点向上流出的流量
如何求x向上流出的流量呢?只需要向上做一次dfs_u,这个dfs和向下dfs的区别在于,向下dfs_d是利用儿子的状态更新父亲的状态,
而向上dfs_u是利用父亲的状态更新儿子的状态
那么现在我们只需要把每个f[x]加上向上的流量即可
更新如下:如果子节点y是叶节点,那么f[j] += min(f[u] - w[i], w[i])
如果子节点y不是叶节点,且父节点是叶节点:f[j] += w[i]
如果子节点y不是叶节点,且父节点不是叶节点:f[j] += min(f[u] - min(f[j], w[i]), w[i])

处理的思路基本和求 树中心一致
*/
#include 

using namespace std;

int const N = 2e5 + 10, M = N * 2;
int e[M], ne[M], w[M], idx, h[N];
int n, t;
int d[N], f[N];

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

void dfs_d(int u, int fa)
{
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == fa) continue;
        
        dfs_d(j, u);
        if (d[j] == 1) f[u] += w[i];
        else f[u] += min(w[i], f[j]);
    }
    return ;
}

void dfs_u(int u, int fa)
{
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == fa) continue;
        
        if (d[j] == 1) f[j] += min(f[u] - w[i], w[i]);
        else 
        {
            if (d[u] != 1) f[j] += min(f[u] - min(f[j], w[i]), w[i]);
            else f[j] += w[i];
        }
        
        dfs_u(j, u);
    }
    return;
}

int main()
{  
    cin >> t;
    while (t--)
    {
        memset(f, 0, sizeof f);
        memset(d, 0, sizeof d);
        memset(h, -1, sizeof h);
        idx = 0;
        cin >> n;
        for (int i = 1; i <= n - 1; ++i)
        {
            int a, b, c;
            scanf("%d %d %d", &a, &b, &c);
            add(a, b, c), add(b, a, c);
            d[a]++, d[b]++;
        }
        
        dfs_d(1, -1);
        dfs_u(1, -1);
        
        int res = -1;
        for (int i = 1; i <= n; ++i) res = max(f[i], res);
        cout << res << endl;
    }
    return 0;
}

2.1.3 树的重心

acwing846树的重心
给定一颗树,树中包含n个结点(编号1~n)和n-1条无向边。
请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。

输入格式
第一行包含整数n,表示树的结点数。
接下来n-1行,每行包含两个整数a和b,表示点a和点b之间存在一条边。

输出格式
输出一个整数m,表示重心的所有的子树中最大的子树的结点数目。

数据范围
1≤n≤105

输入样例
9
1 2
1 7
1 4
2 8
2 5
4 3
3 9
4 6

输出样例:
4

/*
找子树最大值的最小值,直接树形dp一次即可
*/
#include 

using namespace  std;

int const N = 1e5 + 10, M = N * 2, INF = 0x3f3f3f3f;
int e[M], ne[M], h[N], idx;
int n, f[N], res = INF;

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a] , h[a] = idx++;
}

int dfs(int u, int fa)
{
    int sum = 1, ans = -1;  // ans记录子树的最大值, sum记录子树点数和和u点
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == fa) continue;
        
        int son = dfs(j, u);  
        sum += son;  // 更新sum
        ans = max(ans, son);  // 不断更新当前子树的最大值
    }
    ans = max(ans, n - sum);  // 向上的部分
    res = min(res, ans);  // 更新全局最小值
    return sum;
}

int main()
{
    memset(h, -1, sizeof h);
    cin >> n;
    for (int i = 1; i <= n - 1; ++i)
    {
        int a, b;
        scanf("%d %d", &a, &b);
        add(a, b), add(b, a);
    }
    dfs(1, -1);
    cout << res << endl;
    return 0;
}

2.1.4 树的直径

acwing1072树的最长路径
给定一棵树,树中包含 n 个结点(编号1~n)和 n−1 条无向边,每条边都有一个权值。

现在请你找到树中的一条最长路径。

换句话说,要找到一条路径,使得使得路径两端的点的距离最远。

注意:路径中可以只包含一个点。

输入格式
第一行包含整数 n。

接下来 n−1 行,每行包含三个整数 ai,bi,ci,表示点 ai 和 bi 之间存在一条权值为 ci 的边。

输出格式
输出一个整数,表示树的最长路径的长度。

数据范围
1≤n≤10000,
1≤ai,bi≤n,
−105≤ci≤105

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

输出样例:
22

/*
思路:树的直径就是一棵树的最大深度+次大深度,所以每次只需要更新最大深度和次大深度即可
*/

#include 

using namespace std;

int const N = 1e4 + 10, M = N * 2;
int e[M], ne[M], h[N], w[M], idx;
int n, ans;  // ans记录树的直径

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

// 在求子树深度的基础上求树的直径
int dp(int u, int fa)
{
    int dist = 0, d1 = 0, d2 = 0;  // dist:子树u的深度,d1子树u的最深深度,d2子树u的次深深度
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == fa) continue;
        int d = w[i] + dp(j, u);  // 当前这个分支算出的子树u的深度
        dist = max(dist, d);  // 更新dist,得到当前最大的子树u深度
        if (d >= d1)   // 更新d1和d2
        {
            d2 = d1;
            d1 = d;
        }
        else if (d > d2) d2 = d;
    }
    
    ans = max(ans, d1 + d2);  // 更新树的直径
    return dist;
}

int main()
{
    memset(h, -1, sizeof h);
    cin >> n;
    for (int i = 1; i <= n - 1; ++i)
    {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }
    dp(1, -1);
    cout << ans << endl;
    return 0;
}

acwing1075. 数字转换
如果一个数 x 的约数之和 y(不包括他本身)比他本身小,那么 x 可以变成 y,y 也可以变成 x。
例如,4 可以变为 3,1 可以变为 7。
限定所有数字变换在不超过 n 的正整数范围内进行,求不断进行数字变换且不出现重复数字的最多变换步数。

输入格式
输入一个正整数 n。

输出格式
输出不断进行数字变换且不出现重复数字的最多变换步数。

数据范围
1≤n≤50000

输入样例:
7

输出样例:
3
样例解释
一种方案为:4→3→1→7。

/*
要求最长的变化方案,我们观察变化方案,由于每个数字的约数和唯一,那么最后的变化方案一定呈现"V"型
所以变化方案一定是以某个节点为分界点,分界点右边的数字递增,分界点左边的数字递减
现在我们考虑把每个数字的约数和当成父节点,向这个数字连一条单向边,那么一定能够建出森林(不一定只有一棵树)
而这个森林中所有树的最长链一定和这个"V"型对应。
为什么对应呢?因为从树的某个点出发,向下的点都是子节点,数字在变大,即从a的约数和到a,那么我们看最长链,就可以拆成最长边和次长边
次长边从叶节点到根节点为"V"型分界点左边递减的部分,最长边从跟节点到叶节点为"v"型右边递增的部分。
那么我们只需要对每个树求一次最长链即可

本题处理的时候有很常用的小技巧:从每个数的角度出发,看这个数字可以作为哪个数字的约数和,这样时间为O(nlogn)
*/

#include 

using namespace std;

int const N = 5e4 + 10;
int sum[N], n;
int e[N], ne[N], h[N], idx;
int st[N], ans;

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// 求最长链
int dfs(int u)
{
    st[u] = 1;
    int dist = 0, d1 = 0, d2 = 0;  // d1为最深,d2为次深, dist为深度
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (st[j]) continue;  // 保证把所有树都dfs完只需要O(n+m)
        int d = dfs(j);
        dist = max(dist, d);
        if (d >= d1)
        {
            d2 = d1, d1 = d;
        }
        else if (d > d2) d2 = d;
    }
    ans = max(ans, d1 + d2);  // 更新答案
    return dist + 1;
}

int main()
{
    cin >> n;
    memset(h, -1, sizeof h);
    for (int i = 1; i <= n; ++i)
    {
        for (int j = 2; j <= n / i; ++j)
        {
            sum[i * j] += i;
        }
    }
    
    for (int i = 2; i <= n; ++i)
        if (sum[i] < i) add(sum[i], i);
    
    for (int i = 1; i <= n; ++i)
    {
        if (!st[i]) dfs(i);  // 打个标记是为了能够遍历到森林在的每一棵树
    }
    
    cout << ans << endl;
    return 0;
}

2.2 树形背包问题

acwing10. 有依赖的背包问题
有 N 个物品和一个容量是 V 的背包。
物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。
如下图所示:
Snipaste_2020-03-18_12-36-57.png
如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。
每件物品的编号是 i,体积是 vi,价值是 wi,依赖的父节点编号是 pi。物品的下标范围是 1…N。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式
第一行有两个整数 N,V,用空格隔开,分别表示物品个数和背包容量。
接下来有 N 行数据,每行数据表示一个物品。
第 i 行有三个整数 vi,wi,pi,用空格隔开,分别表示物品的体积、价值和依赖的物品编号。
如果 pi=−1,表示根节点。 数据保证所有物品构成一棵树。

输出格式
输出一个整数,表示最大价值。

数据范围
1≤N,V≤100
1≤vi,wi≤100
父节点编号范围:
内部结点:1≤pi≤N;
根节点 pi=−1;
输入样例
5 7
2 3 -1
2 2 1
3 5 1
4 7 2
3 6 2

输出样例:
11

#include 

using namespace std;

int const N = 110;
int f[N][N], v[N], w[N];  // f[u][j]表示以u为根节点,体积最大为j的子树的最大价值
int idx, e[N], ne[N], h[N];  // 建立邻接表需要的数据结构
int n, m;  // 点数和体积

// 建立邻接表
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// dfs函数进行一次得到以u结点为根的树的各种体积的最大价值
// 即能够得到任何的f[u][j]
void dfs(int u)
{
    for (int i = h[u]; i != -1; i = ne[i])  // 遍历与u相连的所有点
    {
        int son = e[i];  // 得到与u相连的点为son
        dfs(son);  // 对son进行dfs,得到任意j的f[son][j]
        for (int j = m - v[u]; j >= 0; j--)  // 根结点u必须选上,所以最大能使用的体积为m-v[u],枚举体积
            for (int k = 0; k <= j; ++k)  // 枚举以son结点为根,任意体积k的子树,选择其中最大价值的一种
                f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);  // 状态转移
    }
    for (int i = m; i >= v[u]; --i) f[u][i] = f[u][i - v[u]] + w[u];  // 如果体积大于等于v[u]表明能放入根节点,那么给所有这种的点补上之前没有放入的根节点u
    for (int i = 0; i < v[u]; ++i) f[u][i] = 0;  // 如果体积小于v[u],那么无法放入根节点,那么不可能出现这种情况,则全为0
}

int main()
{
    memset(h, -1, sizeof h);  // 邻接表初始化
    int root;  // 标记根
    cin >> n >> m;  // 读入点数和体积
    for (int i = 1; i <= n; ++i)  
    {
        int pre;  // 父节点
        cin >> v[i] >> w[i] >> pre;  // 读入每个物品的体积、价值和父节点
        if (pre == -1) root = i;  // 如果父节点为-1,那么为根节点
        add(pre, i);  // 表示和父节点pre相连的点为i
    }
    dfs(root);  // 从根开始dfs,树一般都是从根开始dfs
    cout << f[root][m];  // 输出以root为根节点,体积最大为m的最大价值
    return 0;
}

acwing1074. 二叉苹果树

有一棵二叉苹果树,如果树枝有分叉,一定是分两叉,即没有只有一个儿子的节点。
这棵树共 N 个节点,编号为 1 至 N,树根编号一定为 1。
我们用一根树枝两端连接的节点编号描述一根树枝的位置。
一棵苹果树的树枝太多了,需要剪枝。但是一些树枝上长有苹果,给定需要保留的树枝数量,求最多能留住多少苹果。
这里的保留是指最终与1号点连通。

输入格式
第一行包含两个整数 N 和 Q,分别表示树的节点数以及要保留的树枝数量。
接下来 N−1 行描述树枝信息,每行三个整数,前两个是它连接的节点的编号,第三个数是这根树枝上苹果数量。

输出格式
输出仅一行,表示最多能留住的苹果的数量。

数据范围
1≤Q N≠1,
每根树枝上苹果不超过 30000 个。

输入样例:
5 2
1 3 1
1 4 10
2 3 20
3 5 20

输出样例:
21

/*
本题是分组背包的变形,本题问的是保留m条边可以得到的最大的苹果树,而分析树的性质我们可以知道,
如果想要保留子树,那么子树的父节点必须保留!!!
那么这题就变成了一道有依赖的背包问题,背包体积为要保留的树边+1(把边转化为点)
然后把每条边的权值转移到对应的子节点上去
*/

#include 

using namespace std;

int const N = 100 + 10, M = N * 2;
int e[M], ne[M], w[M], h[N], idx, f[N][N];
int n, q, m;

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

void dfs(int u, int fa, int weight)
{
    for (int i = h[u]; i != -1; i = ne[i])  // 遍历与u相连的所有点
    {
        int son = e[i];  // 得到与u相连的点为son
        if (son == fa) continue;

        dfs(son, u, w[i]);  // 对son进行dfs,得到任意j的f[son][j]
        for (int j = m - 1; j >= 0; j--)  // 根结点u必须选上,所以最大能使用的体积为m-v[u],枚举体积
            for (int k = 0; k <= j; ++k)  // 枚举以son结点为根,任意体积k的子树,选择其中最大价值的一种
                f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);  // 状态转移
    }
    for (int i = m; i >= 1; --i) f[u][i] = f[u][i - 1] + weight;  // 如果体积大于等于v[u]表明能放入根节点,那么给所有这种的点补上之前没有放入的根节点u
    f[u][0] = 0;
}

int main()
{
    // freopen("in.txt", "r", stdin);
    // freopen("out.txt", "w", stdout);

    cin >> n >> m;
    m++;
    memset(h, -1, sizeof h);
    for (int i = 1, a, b, c; i <= n - 1; ++i)
    {
        scanf("%d %d %d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }

    dfs(1, -1, 0);
    printf("%d", f[1][m]);
    return 0;
}

你可能感兴趣的:(树形dp)