(虽然连续依赖可能构成森林, 但我们可以虚构一个超级根节点解决这个问题)
普通的做法:
HDU 1561
(我的朴素做法(
n
V
2
nV^2
nV2)用排序逃避了写
d
f
s
dfs
dfs…实际上是完全没有必要的…用
d
f
s
dfs
dfs反而更加好写, 并且稍微 改变就能改成更高效的算法.)
对于一棵树, 我们考虑从叶子节点一路推到根节点来计算总的最大价值. 按深度从深到浅依次枚举每个节点, 每个节点枚举子节点的每个泛化物品更新当前节点的泛化物品. 这样的做法很容易想到, 但是复杂度是
O
(
n
V
2
)
O(nV^2)
O(nV2)的.
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示考虑了
i
i
i节点及其子树, 当容量为j时, 得到的最大价值------注意, 这里的答案严格意义上是无效的, 因为并没有考虑其祖先, 而树上背包要求我们必须得到祖先才能选择后代------当然, 当我们考虑完所有的节点(一直到根节点)这个答案自然就成正确的了.
实际上, “枚举儿子的泛化物品, 更新当前节点的泛化物品”, 这个过程被称作"求泛化物品的和", 复杂度的确是
O
(
n
V
2
)
O(nV^2)
O(nV2)的, 核心代码大概长这个样子:
for (auto x:lp)//枚举父亲
for (auto i:son[x])//枚举儿子
for (int j = m + 1; j >= 0; --j)//枚举父亲的容量
for (int k = 1; k <= m + 1 && j - k >= 0; ++k)//枚举儿子的容量
checkMax(dp[x][j], dp[x][k] + dp[i][j - k]);//泛化物品求和
导致这个复杂度的原因在于枚举了两层容量, 更确切地说, d p [ i ] [ j ] dp[i][j] dp[i][j]维护了i向下这棵完整子树的信息, 而向上更新的过程中没有利用到"x(i的父亲)想要取到 i i i的贡献必须先取i"这个特性, 以至于只能把 s s s和 i i i看成各自独立的两个个体暴力合并, 这一点限制了我们必须求泛化物品的和. 那有没有更好的办法呢?
O ( n V ) O(nV) O(nV)的做法:
d p [ i ] [ j ] dp[i][j] dp[i][j]有了新的含义: 考虑将第 i i i个节点不包含i本身的子树看作泛化物品, 当其容量为 j j j时价值为 d p [ i ] [ j ] dp[i][j] dp[i][j]. 这样做有什么好处呢? 对于一个节点 i i i,我们先强制取它的儿子 s s s计入贡献,并且假设我们已经通过某种方法知道了 F s Fs Fs, 那么 F ( i , j ) F(i,j) F(i,j)就由 F ( s , j − w [ s ] ) + v [ s ] F(s,j-w[s])+v[s] F(s,j−w[s])+v[s]转移. 从这里就能看出把 d p [ i ] [ j ] dp[i][j] dp[i][j]定义为将自己排除在外的子树的好处: 能够直接用泛化物品(儿子)和普通物品(儿子本身)更新泛化物品(父亲). 这个过程被称作"求泛化物品的并", 复杂度是 O ( n V ) O(nV) O(nV)的, 核心代码是这样:
void dfs(int now, int V) {
for (auto son:to[now]) {
for (int i = 0; i <= V; ++i)
dp[son][i] = dp[now][i];
dfs(son, V - v[son]);
for (int i = v[son]; i <= V; ++i)
checkMax(dp[now][i], dp[son][i - v[son]] + w[son]);
}
}
因为不可避免有两种操作: “push down”(把父亲当前的泛化物品传给儿子)和"push up"(用儿子计算的值和儿子本身求并更新父亲), 所以必须用
d
f
s
dfs
dfs解决.
实际上,
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]维护的是考虑前若干个儿子的贡献, 当前
i
i
i节点的泛化物品—只有当儿子都枚举完后, 才是除自己外的子树泛化物品.
这里是 H D U 1561 HDU1561 HDU1561的 O ( n V ) O(nV) O(nV)代码:
int n, m,dp[205][205],w[205];
vector<int> to[205];
void dd() {
for (int i = 0; i <= n; ++i) {
for (int j = 0; j <= m + 1; ++j) {
printf("[%d,%d]:%d ", i, j, dp[i][j]);
}
enter;
}
}
void dfs(int now, int V) {
for (auto son:to[now]) {
for (int i = 0; i <= V; ++i)
dp[son][i] = dp[now][i];
dfs(son, V - 1);
for (int i = 1; i <= V; ++i)
checkMax(dp[now][i], dp[son][i - 1] + w[son]);
}
}
void init() {
while (~scanf("%d", &n) && n) {
Mem(dp, 0);
m = read();
for (int i = 0; i <= n; ++i)to[i].clear();
for (int i = 1; i <= n; ++i) {
int a = read(), b = read();
to[a].emplace_back(i);
w[i] = b;
}
dfs(0, m);
write(dp[0][m]), enter;
}
}
用树上背包的写法, 洛谷 P1064 金明的预算方案 就可以直接秒了. 其实依赖背包完全可以全写成树上背包, 只不过空间要开大一点罢了.
int V, n,w[70],v[70],dp[70][40000];
vector<int> to[70];
void dfs(int now, int W) {
for (auto son:to[now]) {
for (int i = 0; i <= W; ++i) {
dp[son][i] = dp[now][i];
}
dfs(son, W - w[son]);
for (int i = w[son]; i <= W; ++i) {
checkMax(dp[now][i], dp[son][i - w[son]] + v[son]);
}
}
}
void init() {
V = read(), n = read();
for (int i = 1; i <= n; ++i) {
w[i] = read(), v[i] = read();
v[i]*=w[i];
int fa = read();
to[fa].emplace_back(i);
}
dfs(0, V);
int ans=0;
for (int i = 0; i <=V; ++i) {
checkMax(ans,dp[0][V]);
}write(ans),enter;
}