[SDOI2017]苹果树
题解
先扯点前置知识:
单调队列优化多重背包:
这玩意儿其实也可以用二进制拆分来优化
但是复杂度会多一个log
所以大致说一下单调队列怎么优化多重背包
假设物品个数为\(n\),每种重量为\(w_i\),数量为\(Num_i\),价值为\(val_i\),背包总容积为\(m\)
假设物品个数为\(n\),每种重量为\(w_i\),数量为\(Num_i\),价值为\(val_i\),背包总容积为\(m\)
那么有一个很简单的\(dp\)就是\(f[i][j] = \max( f[i][j] , f[i-1][j - k\times w_i]+k \times val_i )\)
那么我们可以考虑枚举倍数,也就是枚举这种物品装了几个
设\(d=j\%w_i\)也就是余数,\(b=\min(Num_i,j/w_i)\)也就是选择这个东西的上限
那么我们可以从\(0 \sim w_i - 1\)枚举\(d\)
然后对于每一个\(d\)枚举倍数,可以发现这样形成的背包容量\(d+j\times w_i\)是相互独立且互不相同的
这样\(dp\)式子就成了\(f[i][d + j \times w_i] = \max( f[1][j] , f[i-1][d+k\times w_i] + (j - k) \times w_i )\)
这样可以发现就满足单调队列的形式了
对于每个\(j\)维护\(f_j-j\times w_i\)即可
再扯一句就是这个单调队列的话最好先放进去原来的\(f_j-j\times w_i\)并且直接在单调队列里面存dp值然后再更新
这样可以省去一维
大致代码长这样
for(int i = 1 , b ; i <= n ; i ++) {
b = min( Num[i] , m / w[i] ) ;
for(int d = 0 ; d < w[i] ; d ++) {
int head = 1 , tail = 0 ;
for(int j = 0 , temp ; j * w[i] + d <= m ; j ++ ) {
temp = f[d + j * w[i]] - val[i] * j ;
while(head <= tail && q[tail] <= temp) -- tail ;
q[++ tail] = temp ; k[tail] = j ;
while(head < tail && j - k[head] > b) ++ head ;
f[d + j * w[i]] = max( f[d + j * w[i]] , q[head] + val[i] * j ) ;
}
}
}
然后再来看这个题
可以发现题目就是要求我们先可以"免费"的选择一条链,然后再有依赖的选择\(k\)个物品
那么显然这条"免费"的链我们是要一直选到叶子节点的
那么我们要怎么处理这条链?
似乎把这条链的信息加入\(dp\)的状态中并不容易
所以我们可以通过直接枚举所有的叶子节点的方式来枚举这条链
那么对于这条链
我们把原树分成了四个部分:
这条"免费"的链+这条链左边的部分+这条链右边的部分+在这条链上额外选的部分
那么我们每枚举一条链就要快速的计算这几个东西
可以发现这条链左边的部分和在这条链上额外选的部分可以合在一起计算
那么我们设\(f[i][j]\)表示对边表前序遍历到点\(i\)且选择了\(1 \sim i\)这条链上的部分点时用了\(j\)的最大贡献
\(g[i][j]\)表示对边表后序遍历到点\(i\)且没有选择\(1 \sim i\)这条链上的点用了\(j\)的最大贡献
其实这玩意儿就肥肠类似于普通背包类问题中的去掉一个必须选择的东西后的最大收益问题中的预处理出前缀后缀背包然后合并
如果我们能够预处理出\(f,g\),那么就可以直接枚举分配给\(f\)多少,分配给\(g\)多少直接计算了
那么怎么预处理?
其实本质上预处理过程就是不断枚举每条"免费的链"的过程
首先这个\(f\)数组可以\(dp\)的时候先继承\(ta\)的\(father\)的当前的\(f\)
然后对点\(u\)做完全背包
因为我们枚举的当前链还经过点\(u\),所以我们这个点的能选的次数应该是\(Num[u]-1\)
然后枚举儿子\(v\)
处理处儿子的\(dp\)值以后我们该处理另外的儿子了
那么现在这条免费的链发生改变了
这条链要从这个儿子\(son1\)中抽出来去另一个儿子\(son2\)了
那么我们先把\(son1\)这个儿子的相关信息给\(f_u\)
并且刚刚由于链经过\(son1\),我们正好是让\(son1\)最多取\(Num-1\)次
那么这次我们如果我们还要选择\(son1\)的子树的一些点的话由于依赖关系就必须强制的选择一个这个儿子(这样加上之前完全背包的\(Num-1\)次选择正好让\(son1\)最多取了\(Num\)次)
感觉这有点像一个拆点的思想?
就是对于一个点,按照功能的不同把这个点拆成若干点
就比如这个题就把一个选择次数为\(Num\)的点拆成了\(Num-1\)次的点和只能选一次的点
那么\(Num-1\)次的点就是普通的完全背包时使用的点
只能选择一次的点就可以用来表示如果要选择某个点的子树就必须强制的去选一个这个点
然后对于\(g\)
当前已经到了点\(u\)
那么我们先继承\(fa[u]\)的当前的\(g\)
然后依次逆着边表枚举每个儿子
每次枚举一个儿子就也相当于链是从根到这个儿子的子树里的叶子
那么当我们\(dp\)完了\(son1\)该\(dp\)另外一个儿子\(son2\)了
我们就可以还是利用拆点的思想
先用\(Num-1\)个\(son1\)来更新\(g_u\)
然后再强制的去选那一个\(son1\)来决定是否选择\(son1\)的子树
代码
#include<vector>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
# define LL long long
const int N = 20005 ;
const int M = 500005 ;
const int NK = 30000005 ;
using namespace std ;
inline int read() {
char c = getchar() ; int x = 0 , w = 1 ;
while(c>'9'||c<'0') { if(c=='-') w = -1 ; c = getchar() ; }
while(c>='0'&&c<='9') { x = x*10+c-'0' ; c = getchar() ; }
return x*w ;
}
int n , m ;
int fa[N] , val[N] , Num[N] , d[N] ;
int sum[N] , k[M] ;
LL ans , q[M] , dp[M] , f[NK] , g[NK] ;
vector < int > vec[N] ;
inline void Clear() {
ans = 0 ;
memset(d , 0 , sizeof(d)) ;
memset(f , 0 , sizeof(f)) ;
memset(g , 0 , sizeof(g)) ;
for(int i = 1 ; i <= n ; i ++) vec[i].clear() ;
}
inline void add_edge(int u , int v) {
vec[u].push_back(v) ;
}
inline int p(int i , int j) {
return (i - 1) * (m + 1) + j ;
}
void dfs1(int u) {
if(fa[u])
for(int i = 0 ; i <= m ; i ++)
f[p(u , i)] = f[p(fa[u] , i)] ;
int head = 1 , tail = 0 ;
int b = min( m , Num[u] - 1 ) ; LL temp ;
for(int i = 0 ; i <= m ; i ++) {
temp = f[p(u , i)] - 1LL * i * val[u] ;
while(head <= tail && q[tail] <= temp) -- tail ;
q[++tail] = temp ; k[tail] = i ;
while(head < tail && i - k[head] > b) ++ head ;
f[p(u , i)] = max ( f[p(u , i)] , q[head] + 1LL * i * val[u] ) ;
}
for(int i = 0 , v , sz = vec[u].size() ; i < sz ; i ++) {
v = vec[u][i] ;
dfs1(v) ;
for(int j = m ; j >= 1 ; j --)
f[p(u , j)] = max(f[p(u , j)] , f[p(v , j - 1)] + val[v]) ;
}
}
void dfs2(int u) {
if(fa[u])
for(int i = 0 ; i <= m ; i ++)
g[p(u , i)] = g[p(fa[u] , i)] ;
for(int i = 0 , v , sz = vec[u].size() ; i < sz ; i ++) {
v = vec[u][i] ;
dfs2(v) ;
int head = 1 , tail = 0 , b = min(m , Num[v] - 1) ; LL temp ;
for(int j = 0 ; j <= m ; j ++) {
dp[j] = 0 ;
temp = g[p(v , j)] - 1LL * j * val[v] ;
while(head <= tail && q[tail] <= temp) -- tail ;
q[++tail] = temp ; k[tail] = j ;
while(head < tail && j - k[head] > b) ++ head ;
dp[j] = q[head] + 1LL * j * val[v] ;
}
for(int j = m ; j >= 1 ; j --)
g[p(u , j)] = max(g[p(u , j)] , dp[j - 1] + val[v]) ;
}
}
int main() {
int Case = read() ;
while(Case --) {
n = read() ; m = read() ;
for(int i = 1 ; i <= n ; i ++) {
fa[i] = read() ; Num[i] = read() ; val[i] = read() ;
++ d[fa[i]] ; ++ d[i] ; sum[i] = sum[fa[i]] + val[i] ;
if(fa[i]) add_edge(fa[i] , i) ;
}
dfs1(1) ;
for(int u = 1 ; u <= n ; u ++)
reverse(vec[u].begin() , vec[u].end()) ;
dfs2(1) ;
for(int u = 1 ; u <= n ; u ++)
if(d[u] == 1) {
for(int i = 0 ; i <= m ; i ++)
ans = max(ans , f[p(u , i)] + g[p(u , m - i)] + sum[u]) ;
}
printf("%lld\n",ans) ;
Clear() ;
}
return 0 ;
}