|
|
|
|
##[$AcWing$ $1074$ . 二叉苹果树](https://www.acwing.com/problem/content/1076/)
|
|
|
|
|
|
|
|
|
|
**[视频讲解](https://www.bilibili.com/video/av668731451)**
|
|
|
|
|
|
|
|
|
|
### 一、题目描述
|
|
|
|
|
|
|
|
|
|
有一棵二叉苹果树,如果树枝有分叉,一定是分两叉,即没有只有一个儿子的节点。
|
|
|
|
|
|
|
|
|
|
这棵树共 $N$ 个节点,编号为 $1$ 至 $N$,树根编号一定为 $1$。
|
|
|
|
|
|
|
|
|
|
我们用一根树枝两端连接的节点编号描述一根树枝的位置。
|
|
|
|
|
|
|
|
|
|
一棵苹果树的树枝太多了,需要剪枝。但是一些树枝上长有苹果,**给定需要保留的树枝数量,求最多能留住多少苹果**。
|
|
|
|
|
|
|
|
|
|
这里的保留是指最终与$1$号点连通。
|
|
|
|
|
|
|
|
|
|
**输入格式**
|
|
|
|
|
第一行包含两个整数 $N$ 和 $Q$,分别表示树的节点数以及要保留的树枝数量。
|
|
|
|
|
|
|
|
|
|
接下来 $N−1$ 行描述树枝信息,每行三个整数,前两个是它连接的节点的编号,第三个数是这根树枝上苹果数量。
|
|
|
|
|
|
|
|
|
|
**输出格式**
|
|
|
|
|
输出仅一行,表示最多能留住的苹果的数量。
|
|
|
|
|
|
|
|
|
|
**数据范围**
|
|
|
|
|
$1≤Q<N≤100$.
|
|
|
|
|
$N≠1$,
|
|
|
|
|
每根树枝上苹果不超过 $30000$ 个。
|
|
|
|
|
|
|
|
|
|
**输入样例:**
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
5 2
|
|
|
|
|
1 3 1
|
|
|
|
|
1 4 10
|
|
|
|
|
2 3 20
|
|
|
|
|
3 5 20
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**输出样例:**
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
21
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**\~手绘美图方便理解\~**
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
### 二、题目分析
|
|
|
|
|
|
|
|
|
|
这题的题面其实就是 **有依赖的背包** 模型,不同的是把 **物品价值** 分给了 **边** 而不是 **点**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
不过,对于一棵树来说,任意节点的**入边**(**连向父节点的边**) 都是 **唯一** 的
|
|
|
|
|
|
|
|
|
|
所以 **边权** 和 **点权** 在确定树的 **根节点** 以后,是可以视作一个东西的(**入边价值** 视作 **该点价值**)
|
|
|
|
|
|
|
|
|
|
**树枝上苹果的数量** 可以理解成 **该边的权值**
|
|
|
|
|
|
|
|
|
|
#### 闫氏$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;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|