1. 程式人生 > 其它 >P3780 [SDOI2017]蘋果樹

P3780 [SDOI2017]蘋果樹

P3780 [SDOI2017]蘋果樹

題目大意:

給定一個有根樹,每個節點有權值 \(v_i\),節點值至多取 \(a_i\) 次,選兒子節點一定要同時選父親節點一次,取 \(k\) 個節點值。

除此之外,還可以取一條最長鏈,求最大權值。

思路:

拿到這道題,先轉化成理解的形式:樹上揹包 \(dp\) +一堆不知道什麼東西。

————————————————————————
此時需要一個知識點: 樹的後序遍歷:

其實就是求彈棧的順序,此時有兩個特殊性質可以利用:

  1. 子樹 \(dfs\) 序連續 ,與前序遍歷相同
  2. 任意一個點 \(i\) 的子樹在這個序列上都是一個區間,而且 \(i\) 是區間的右端點。

————————————————————————

最長鏈一定是從根節點到葉子的一條路徑。又因為點權是正的,肯定取越多越好。

對於所有點有一個隱藏的性質:

我如果要第二次取這個節點的值,首先要取一次值才行,這跟題目中要取兒子一定先取父親的邏輯關係是一樣的。

我們進行拆點:如果 \(a_i > 1\)\(i\) 拆成兩個點:第一個 \(a_i=1\) ,第二個點是 \(i\) 的兒子節點 \(i'\)\(a_{i'}=a_i-1\),且不和其他點連線。

這樣就可以直接列舉路徑,考慮 \(dp\) ,有 \(dp[i][j]\) 表示決策到了後序遍歷第 \(i\) 個點,選取 \(j\)

個物品的最大收益。

那麼我們列舉這個物品選還是不選,如果不選的話,它的子樹全部不可選,而不可選的子樹是一段區間,只能從 \(dp[i-size_i][j]\) 轉移過來

則有轉移方程:

\[dp[i][j]=max^{a_i}_{k=1}(dp[i-1][j-k],dp[i-size_i][j]) \]

我們後續跑一遍 \(dp\) ,然後把鄰接表反轉一遍再跑一遍,此時 \(dp[i][k]\) 就是再後序遍歷前 \(i\) 個點中選擇 \(k\) 個物品的最大收益的答案。

玄學卡常:

  1. 運用單向邊儲存
  2. 利用一維陣列模擬二維,減少訪問,節約時間。
  3. 不要寫雙端佇列,儘量手寫
// P3780 [SDOI2017]蘋果樹
#include<bits/stdc++.h>
const int N=4e4+10,K=5e5+10,NK=6e7+10;

using namespace std;

inline void clear(vector <int>& ve){vector<int>emp; swap(emp,ve);}

vector<int> v[N];
int w[N],a[N],fa[N],dfn1[N],dfn2[N],df1,df2,sizes[N],line[N];
int dp1[NK],dp2[NK],nfd1[N],nfd2[N],q1[K<<1],q2[K<<1];
bool lf[N];
int T,n,k,res,cnt,h,head=1,tail=0;

void init(){
    for(int i=0;i<=cnt;i++) clear(v[i]),lf[i]=line[i]=sizes[i]=0;
    for(int i=0;i<=(cnt+1)*(k+1);i++) dp1[i]=dp2[i]=0;
    df1=df2=res=cnt=h=0;
}

void dypr(int *dfn,int *dp){//dp過程,單調佇列進行手寫優化
    for(int i=1;i<=cnt;i++){
        int v=dfn[i];head=tail=1; q1[1]=q2[1]=0;
        for(int j=1;j<=k;j++){
            if(q1[head]<j-a[v]) head++;//剩餘更多,更加優秀
            int val=dp[(i-1)*(k+1)+j]-j*w[v]; //這裡是運用了二維轉一維的寫法
            dp[i*(k+1)+j]=max(q2[head]+j*w[v],dp[(i-sizes[v])*(k+1)+j]);//單調佇列優化轉移
            while(head<=tail&&q2[tail]<=val) tail--;
            q1[++tail]=j; q2[tail]=val;
        }
    }
}

void dfs1(int x){
    sizes[x]=1;
    for(int i=0;i<v[x].size();i++){
        int y=v[x][i];
        dfs1(y); 
        sizes[x]+=sizes[y];
    }
    dfn1[++df1]=x;
    nfd1[x]=df1;//後序遍歷,記錄入點
}

void dfs2(int x){//逆鄰接表
    for(int i=v[x].size()-1;i>=0;i--){
        int y=v[x][i]; line[y]=line[x]+w[y];
        dfs2(y);
    }
    dfn2[++df2]=x;
    nfd2[x]=df2;
}


int main(){
    cin>>T;
    while(T--){
        
        scanf("%d%d",&n,&k); cnt=n;
        for(int i=1;i<=n;i++) scanf("%d%d%d",&fa[i],&a[i],&w[i]),lf[fa[i]]=true;
        for(int i=1;i<=n;i++){//拆點,加邊
            v[fa[i]].push_back(i);
            if(a[i]>1){
                a[++cnt]=a[i]-1; a[i]=1;
                w[cnt]=w[i]; v[i].push_back(cnt);
            }
        }
        line[1]=w[1];
        dfs1(1); dfs2(1);
        dypr(dfn1,dp1); dypr(dfn2,dp2);
        for(int i=1;i<=n;i++){
            if(lf[i]) continue;
            for(int j=0;j<=k;j++){
                res=max(res,dp1[(nfd1[i]-1)*(k+1)+j]+line[i]+dp2[(nfd2[i]-sizes[i])*(k+1)+(k-j)]);
                res=max(res,dp1[(nfd1[i]-1)*(k+1)+j]+line[i]+dp2[(nfd2[i]-sizes[i])*(k+1)+(k-j)]);
            }
        }
        printf("%d\n",res);
        init();
    }

    system("pause");
    return 0;
}