1. 程式人生 > 其它 >【16年浙江省賽 B ZOJ 3937】More Health Points 題解

【16年浙江省賽 B ZOJ 3937】More Health Points 題解

題目連結

\(Describe\)

給定一棵以節點\(1\)為根的樹,節點間單向邊連線,每個節點有權值\(B_i\),找到其中一串子鏈(也可以為空),該子鏈最靠近\(1\)的節點為該子鏈的根節點,計算每個節點的深度(根節點深度為\(1\))與權值的乘積,並求和,即\(ans= \sum\limits_{i=1}^ndeep_i*B_i\)(其中\(n\)表示該子鏈節點數,\(deep_i\)表示第\(i\)個節點的深度),輸出最大的和(若所選子鏈為空,和為\(0\))

\(Solution\)

這道題是大一時我在校集訓隊\(vp\)訓練時做的一道題,當時只有我們隊出了這題,而我是負責這題的,也正是託這題的福,我們成功拿下了當時榜單\(rank1\)

,超過了大二大三的學長(霧)。
賽後我去網上搜了一下這個題,結果呢,好傢伙,居然這麼難?好多部落格的題解有斜率優化\(dp\)、動態維護下凸殼、凸包凸殼等我不會的演算法,當時一臉懵逼,因為在\(vp\)時,我根本沒有想這麼多,我就單純樹形\(dp\)\(dfs\)\(AC\)了這題。
具體講講我的做法,這題我的第一想法就是跑\(dfs\),最開始我是從根節點\(1\)開始\(dfs\),但是\(TLE\)了,演算法也會變得相當複雜。於是,我換了個思路,反向建邊,從葉子節點跑\(dfs\)到根節點,這樣對於答案求和的計算也會非常簡單,用\(sum[i]\)表示從葉子節點到\(i\)節點的\(B_i\)
樹上字首和,\(dp[i]\)表示跑到\(i\)節點的求和答案值,不難推出狀態轉移方程

\(dp[v]=dp[u]+sum[v]\)

\(dp[v]=max(dp[v],B[v])\)

另外,要一邊跑\(dfs\)一邊維護字首和\(sum\)的值。但是,這樣子去寫會\(TLE\),於是我開始了剪枝優化,對於一些已經跑過了的點,可以省略不跑,用\(vis\)陣列記錄跑過的點,如果滿足

\(vis[v]=1,dp[u]+a[v]+sum[u]\le dp[v],sum[u]+a[v]\le sum[v]\)

就可以停止\(dfs\)了,直到把所有的葉子節點跑完,答案輸出為\(dp\)陣列的最大值,最小輸出\(0\)

.因為對於\(n\)個節點的樹,其葉子節點最多\((n+1)/2\)個,所以不剪枝優化時,時間複雜度是\(O(t*nlogn)\)\(TLE\),但是優化後其複雜度降了下來,於是乎就\(AC\)了這題。(其實資料還是比較水的,加強點可能會\(T\))

\(Code\)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll t,n,a[100005],b,head[100005],tot,ans,dp[100005],sum[100005],in[100005],vis[100005];
struct node
{
    ll v,next;
}e[100005];//鏈式前向星存圖
void add(ll u,ll v)
{
    e[++tot].v=v;
    e[tot].next=head[u];
    head[u]=tot;
}//加邊操作
void dfs(ll u)
{
    vis[u]=1;
    if(head[u]){ll v=e[head[u]].v;//因為從葉子節點出發能到的點最多隻有1個,所以相當於一條鏈,不需要for迴圈
    if(vis[v]==1&&dp[u]+a[v]+sum[u]<=dp[v]&&sum[u]+a[v]<=sum[v])return;//此解法的關鍵,剪枝優化,否則TLE
    else
    {
        sum[v]=a[v]+sum[u],dp[v]=dp[u]+sum[v],ans=max(ans,max(dp[v],a[v]));//狀態轉移,記錄最大答案
        if(dp[v]>a[v])dfs(v);
        else
        {
            dp[v]=a[v];
            sum[v]=a[v];
            dfs(v);
        }
    }
    }
}
signed main()
{
    scanf("%lld",&t);
    while(t--)
    {
        memset(head,0,sizeof head);
        memset(in,0,sizeof in);
        memset(vis,0,sizeof vis);
        memset(dp,0x80,sizeof dp);
        ans=tot=0;
        scanf("%lld",&n);
        for(ll i=1;i<=n;++i)scanf("%lld",&a[i]);
        for(ll i=2;i<=n;++i)
        {
            scanf("%lld",&b);
            add(i,b);//反向建邊
            in[b]++;//有點類似拓撲排序,其實就是記錄葉子節點
        }
        for(ll i=1;i<=n;++i)if(in[i]==0)
        {
            sum[i]=a[i];
            dp[i]=a[i];
            dfs(i);
        }
        printf("%lld\n",ans);
    }
    return 0;
}

看了其他解法的題解程式碼都比較長,有上百行,我的程式碼只有\(60\)\((qwq)\)