You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

146 lines
6.4 KiB

2 years ago
##[$AcWing$ $1074$ . 二叉苹果树](https://www.acwing.com/problem/content/1076/)
**[视频讲解](https://www.bilibili.com/video/av668731451)**
### 一、题目描述
有一棵二叉苹果树,如果树枝有分叉,一定是分两叉,即没有只有一个儿子的节点。
这棵树共 $N$ 个节点,编号为 $1$ 至 $N$,树根编号一定为 $1$。
我们用一根树枝两端连接的节点编号描述一根树枝的位置。
一棵苹果树的树枝太多了,需要剪枝。但是一些树枝上长有苹果,**给定需要保留的树枝数量,求最多能留住多少苹果**。
这里的保留是指最终与$1$号点连通。
**输入格式**
第一行包含两个整数 $N$ 和 $Q$,分别表示树的节点数以及要保留的树枝数量。
接下来 $N1$ 行描述树枝信息,每行三个整数,前两个是它连接的节点的编号,第三个数是这根树枝上苹果数量。
**输出格式**
输出仅一行,表示最多能留住的苹果的数量。
**数据范围**
$1≤Q<N100$.
$N≠1$,
每根树枝上苹果不超过 $30000$ 个。
**输入样例:**
```cpp {.line-numbers}
5 2
1 3 1
1 4 10
2 3 20
3 5 20
```
**输出样例:**
```cpp {.line-numbers}
21
```
**\~手绘美图方便理解\~**
![](http://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/2023/03/743f8331ee9539e21a90b7508964c591.png)
### 二、题目分析
这题的题面其实就是 **有依赖的背包** 模型,不同的是把 **物品价值** 分给了 **边** 而不是 **点**
不过,对于一棵树来说,任意节点的**入边****连向父节点的边** 都是 **唯一** 的
所以 **边权** 和 **点权** 在确定树的 **根节点** 以后,是可以视作一个东西的(**入边价值** 视作 **该点价值**
**树枝上苹果的数量** 可以理解成 **该边的权值**
#### 闫氏$DP$分析法
**状态表示**
- **集合**$f[u][i][j]$:以$u$为根的子树,在前$i$个子树中选,总体积不超过$j$的所有集
合。
- **属性**:边权值和的最大值
**状态转移**
$\large f[u][i][j] = f[u][i-1][j] \\
\large f[u][i][j] = max(f[u][i][j],f[u][i-1][j-k-1] + f[v][v_i][k]+w[i])$
> <font color='red' size=4><b>解析:</b></font>
> - 一个普适情况,讨论第$i$棵子树,也就是以$v$为根的子树,下面简称为$v$子树,怎么处理,当前剩余的分配资源是$j$条边。
> - 如果$v$子树不选,那么就简单: $f[u][i][j] = f[u][i-1][j]$
> - 如果$v$子树选择,需要多支付$1$条边的代价:$u->v$,同时带来$w[i]$的利益
> 此时,事情变化为:为$v$子树内部继续提供多少条边呢?最小是$0$,最多是$j-1$条, 也就是$0<=k<j$
> 当为$v$子树选择$k$条边时:
> ① $f[u][i-1][j-k-1]$:以$u$为根的树中,在前$i-1$个子树,在$j-k-1$条边的限定情况下,可以获取到的最大价值
> ② $f[v][v_i][k]$:以$v$为根的树中,在所有子树情况下($v_i$表示$v$为根的树中所有子树数量),在有$k$条边限定的情况下,可以获取到的最大价值
> ③ $w[i]$:不管怎样,选择了$v$子树,可以带来$u->$这条边$w[i]$的价值
> $$f[u][i][j] = max(f[u][i][j],f[u][i-1][j-k-1] + f[v][v_i][k]+w[i])$$
**$Q$:这里面的$f[v][v_i][k]$里的$v_i$是啥呢?**
**答**:是指$v$子树有多少个子节点。有同学可能会表示不理解,因为创建的是一个无向图,会不会造成这些子节点和向上的父节点混淆呢?是不会的,原因是因为当根$=1$固定时,再加$st[u]$数组的使用,使得只能向叶子逼近,不能反向向根逼近,不用担心。
**$Q$:$v_i$这东西怎么计算?**
**答**:瞪眼大法出场!仔细观察可知,上面的状态转移方程,每一个$i$对是对$i-1$的依赖,也就是可以使用滚动数组或者倒序枚举的办法进行降维,这是$01$背包中由大到小的枚举顺序是完全一致的,只要使用了倒序枚举,就不会造成对上一行数据依赖的被覆盖掉。
当思考完可能降维的思路后,再仔细观察,发现$f[v][v_i][k]$其实本意表达的就是$f[v][k]$,也就是在$v$为根节点的子树中,给出的边限定数量是$k$的情况下可以获取到的最大边权和。那个中间的$v_i$就是$v$节点的一级子节点个数,是一个不变量,是可以在降维过程中去掉的,不会影响结果。
**三维转二维**
$$\large f[u][j] = max(f[u][j],f[u][j-1-k]+f[v][k]+w[i])$$
### 三、实现代码
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
const int M = N << 1;
int n; // 表示树的节点数
int m; // 表示要保留的树枝数量(背包容量)
int h[N], e[M], w[M], ne[M], idx;
int st[N]; // 是不是访问过
int f[N][N]; // f[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++;
}
// 树形DP
void dfs(int u) {
st[u] = 1;
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i]; // 子节点最好设置为v,不要使用j,因为j一般后面会参于循环用的变量节约资源
if (st[v]) continue;
dfs(v); // 因为父亲需要使用子树的信息来更新自己的信息所以必须先递归子树才能使用子树提供的信息子树提供的信息保存在f[v][1~k]中
// 枚举体积 (u的所有可能体积)
for (int j = m; j >= 0; j--)
// 枚举决策 (子节点v中分配k个体积,需要给u->v留一个v子树中就少1个,k<j)
for (int k = 0; k < j; k++)
// f[u][j - 1 - k]:让u从其它子树中去查找,在空间j-1-k的限定下的最大价值
// f[v][k]+w[i]:在v子树中资源上限是k的情况下返回的最大数量加上u->v这边条上的苹果数w[i]
f[u][j] = max(f[u][j], f[u][j - 1 - k] + f[v][k] + w[i]);
}
}
int main() {
// 初始化
memset(h, -1, sizeof h);
cin >> n >> m;
// 读入n-1条边
int a, b, c;
for (int i = 1; i < n; i++) {
cin >> a >> b >> c;
add(a, b, c), add(b, a, c); // 只能是无向图,因为没说谁离根更近
}
dfs(1); // 已知根是1号点
printf("%d", f[1][m]); // DP从根出发最大限制是m的情况下可以取得的最大值
return 0;
}
```