其他分享
首页 > 其他分享> > 【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)\)

标签:ll,16,题解,sum,dfs,节点,100005,ZOJ,dp
来源: https://www.cnblogs.com/tomoyo13/p/16100197.html