\(shadowice\)已經把他的思路說的很清楚了,可以先看一下會更好理解?
這篇主要是對\(Claris\)題解的簡單說明。與\(shadowice\)的做法還是有差異的(比如並沒有明顯用到后序遍歷的性質),而且用這種寫法可能跑的比較輕松?
(另外你只要想明白\(f,h\)是代表啥,就很好理解了...)
問題等價於樹形依賴背包,允許一條鏈每個點各免費取一次。
免費取一條鏈即\(t\leq h+k\)的限制。這樣最優解一定會免費取了一條從葉子到根節點的鏈。
現在考慮一下怎么做。不妨枚舉這條鏈(也就是枚舉葉子)。
假如我們枚舉的葉子是7,也就是這樣:
同樣我們可以考慮這么4部分:
\((1)\) \(1-4-6-7\)這條鏈可以免費取一次
\((2)\) \(1-4-6-7\)這條鏈還可以付費取\(a_i-1\)次
\((3)\) 鏈左邊的點可以各付費取\(a_i\)次
\((4)\) 鏈右邊的點可以各付費取\(a_i\)次
\((1)\)只需要在葉子處算一下到根節點路徑上的權值和就可以了(就是\(val_1+val_4+val_6+val_7\))。
\((2)\)就是對當前鏈做多重背包(不過每個物品的個數為\(a_i-1\))。
\((3)\)是對前邊枚舉過的非鏈上的點做背包。如果把鄰接表反過來,\((4)\)和\((3)\)的求法是一樣的(也是對前面的點背包)。
單是枚舉葉子就是\(O(n)\)的了。所以我們八成需要DP預處理每個葉子處\((2)(3)(4)\)三個值。
不妨DP的時候將\((2)(3)\)合並到一起算,\((4)\)在反轉邊表后再計算。
設\(f[i][j]\)表示按DFS序考慮到\(i\),體積為\(j\)的最大收益。
\(f[i][j]\)就是到\(i\)節點,已用體積為\(j\),同時考慮了\((2)(3)\)兩種情況的最大值(只考慮了當前點到根節點的鏈和鏈左邊的點)。
比如\(f[7][j]\)就是對\(2,3,5,1,4,6,7\)做完多重背包,已用體積為\(j\)的最大價值(背包時\(1,4,6,7\)的個數分別都是\(a_i-1\))。
怎么求呢?可以先看一下\(Claris\)的代碼。
先放入不能免費的物品,等遍歷完兒子后再放入必選的物品,那么\(i\)到根路徑上所有點都只算了不能免費的部分。
舉個例子:從\(4\)訪問完\(5\)子樹后,然后訪問\(6\),顯然\(f[6]\)就是\(f[5]\)再加入\(1\)個\(5\)和\(a_6-1\)個\(6\)做一次多次背包(\(5\)此時就不能免費取了,因為之前是免費且必選的所以並沒有計算;而\(6\)此時會免費取一次,最后加到鏈上就可以了,所以此時計算\(a_6-1\)個,即不能免費的)。
當然我們不能直接去修改\(f[5]\)這個數組,但我們可以更新數組\(f[4]\),因為它不是葉子,最后就用不到它的\(f\)值。而且\(4\)的其它兒子比如\(11\),也可以直接用\(f[4]\)更新它。
也就是我們要用\(f[5]\)和\(1個5\)更新\(f[4]\),即\(f[4][j]=\max\{f[4][j],\ f[5][j-1]+val_5\}\)。
注意此時\(f[5][j]\)是考慮過\(5\)子樹內的,不能直接用\(f[5][j]\)轉移(可能一個\(5\)也沒有選),要強制選一個\(5\)再轉移。
然后用\(f[4]\)更新\(f[6]\)。其實就是先把\(f[4]\)復制給\(f[6]\),然后用\(f[6]\)和\(a_6-1\)個\(6\)做多重背包。
那么算法流程大概是:記\(v\)的父節點為\(x\),先把\(f[x]\)復制給\(f[v]\),然后用\(a_v-1\)個\(v\)更新\(f[v]\),遞歸到\(v\)。處理完\(v\)子樹后,再用\(f[v]\)加入一個\(v\)做背包來更新\(f[x]\),以便更新后面的子節點。
這里對\(f[i]\)做多重背包是\(f[i][j]=\max_{k=1}^{a_i-1}\{f[i][j-k]+k*val_i\}\),可以用單調隊列維護,\(O(k)\)更新。
然后將DFS序翻轉,設\(h[i][j]\)表示按DFS序考慮到\(i\),體積為\(j\)的最大收益。
\(h[i][j]\)表示到節點\(i\),已用體積\(j\),情況\((4)\)的最大價值,也就是只考慮它到根節點的鏈的右邊的點的最大價值。
比如\(h[7]\),就是對\(9,10,11,8\)做多重背包后的dp數組:
等遍歷完兒子后再放入必選的物品和不能免費的物品,那么\(i\)到根路徑上所有點都沒有算。
轉移方式與\(f\)很類似,只是需要在遍歷完\(i\)的子樹后,才用\(a_i,val_i\)更新\(h[i]\)(這樣在用\(h[i]\)更新子樹時並沒有計算當前鏈上的,只考慮了鏈右邊的點)。不細說了。(和后序遍歷也挺類似的?)
如此一來,對於每個葉子\(i\),用 \(f[i][j]+h[i][k-j]+免費取一次到根節點的路徑的權值\) 更新答案即可。
對於不能免費的物品,需要用單調隊列優化轉移。
時間復雜度\(O(nk)\)。
細節:
二維數組最好用一維數組代替(注意每個點的空間是\(k+1\))。
其它注意一下就好了。
背包也能出成這樣orz。
//204792kb 33008ms
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
//#define gc() getchar()
#define MAXIN 300000
#define gc() (SS==TT&&(TT=(SS=IN)+fread(IN,1,MAXIN,stdin),SS==TT)?EOF:*SS++)
typedef long long LL;
const int N=20005,MAXK=5e5+5,M=25000005+N+MAXK;
int n,m,K,Sz,Ans,A[N],val[N],fa[N],Enum,H[N],nxt[N],F[M],G[M];
char IN[MAXIN],*SS=IN,*TT=IN;
inline int read()
{
int now=0;register char c=gc();
for(;!isdigit(c);c=gc());
for(;isdigit(c);now=now*10+c-'0',c=gc());
return now;
}
#define AE(u,v) nxt[v]=H[u], H[u]=v//只需要to且是單向邊就用不着了to[]啊
void Calc(int *f,int A,int val)
{
static int v[MAXK],q[MAXK];
for(int i=0,s=0,h=1,t=0,tmp; i<=m; ++i,s+=val)
{
tmp=f[i]-s;
while(h<=t && tmp>v[t]) --t;
q[++t]=i, v[t]=tmp;
while(i-q[h]>A) ++h;
f[i]=v[h]+s;
}
}
void DFS1(int x)
{
int *Fx=F+x*K;
if(A[x]) Calc(Fx,A[x],val[x]);
for(int v=H[x]; v; v=nxt[v])
{
memcpy(F+v*K,Fx,Sz);
DFS1(v);
int *fx=Fx+1,*fv=F+v*K,val=::val[v];//f[x][j]=max(f[x][j],f[v][j-1]+val[v])
for(int j=1; j<=m; ++j,++fx,++fv) *fx=std::max(*fx,*fv+val);
}
}
void DFS2(int x,int sum)
{
int *Gx=G+x*K;
for(int v=H[x]; v; v=nxt[v])
{
memcpy(G+v*K,Gx,Sz);
DFS2(v,sum+val[v]);
int *gx=Gx+1,*gv=G+v*K,val=::val[v];//從v這棵子樹中轉移要強制至少選一個v
for(int j=1; j<=m; ++j,++gx,++gv) *gx=std::max(*gx,*gv+val);
}
if(!H[x])
{
int *Fx=F+x*K,*Gx=G+x*K+m,ans=0;
for(int i=0; i<=m; ++i,++Fx,--Gx) ans=std::max(ans,*Fx+*Gx);
Ans=std::max(Ans,ans+sum);
}
if(A[x]) Calc(Gx,A[x],val[x]);
}
int main()
{
for(int T=read(); T--; )
{
n=read(),m=read(),K=m+1,Sz=K<<2;//m+1!
memset(H,0,n+1<<2), memset(F,0,Sz), memset(G,0,Sz);
for(int i=1; i<=n; ++i)
{
if((fa[i]=read())) AE(fa[i],i);
A[i]=read()-1, val[i]=read();
}
DFS1(1);
memset(H,0,n+1<<2);
for(int i=n; i>1; --i) AE(fa[i],i);
Ans=0, DFS2(1,val[1]);
printf("%d\n",Ans);
}
return 0;
}