BZOJ.4910.蘋果樹(樹形依賴揹包 DP 單調佇列)
\(shadowice\)已經把他的思路說的很清楚了,可以先看一下會更好理解?
這篇主要是對\(Claris\)題解的簡單說明。與\(shadowice\)的做法還是有差異的(比如並沒有明顯用到後序遍歷的性質),而且用這種寫法可能跑的比較輕鬆?
問題等價於樹形依賴揹包,允許一條鏈每個點各免費取一次。
免費取一條鏈即\(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)\)
不妨將\((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],\ f[5][j-1]+val_5\}\)。
因為\(f[5]\)就是由\(f[4]\)轉移來的(\(f[5][j]=\max\{f[4][j],\ f[4][j-1]+val_5\}\)),所以\(f[4][j]=\max\{f[4][j],\ f[5][j-1]+val_5\}\)就可以了。(是這樣理解吧...並不是很確定...反正\(f[5][j]\)確實不需要管)
然後用\(f[4]\)更新\(f[6]\)。其實就是先把\(f[4]\)複製給\(f[6]\),然後用\(f[6]\)和\(a_6-1\)個\(6\)做多重揹包。
這裡對\(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]\):
等遍歷完兒子後再放入必選的物品和不能免費的物品,那麼\(i\)到根路徑上所有點都沒有算。
更新方式與\(f\)很類似,只是需要在遍歷完這個子樹後再用\(i\)更新\(h[i]\)(這樣在用\(h[i]\)更新子樹時是沒有計算當前鏈的,只計算了鏈右邊的點)。不細說了。
如此一來,對於每個葉子\(i\),用 \(f[i][j]+h[i][k-j]+取一次i到根節點的路徑的權值\) 更新答案即可。
對於不能免費的物品,需要用單調佇列優化轉移。
時間複雜度\(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;
}