[校内测试]宝藏(树形DP)

=== ===

这里写图片描述
这里写图片描述

=== ===

题解

比较好想的树形DP。。但是实现起来很多地方不是一般恶心。。。也许是ATP的方法太愚蠢了?总之好像当时考试的时候ATP最后半个小时想出来了但是没码完只好写了50pts部分分= =醉死了

那就先从50pts的开始说起吧。。可以想到这个数据范围允许我们把每个节点当做根每次都重新进行dfs,那就用一个f[i][0]表示从i号节点出发到它的子树里去转一圈最后还要回来的最大收益,f[i][1]表示从i号节点出发到它的子树里转一圈最后不回来的最大收益。那么分别以每个点u为根dfs的时候最大收益就是f[u][0]和f[u][1]的Max。

但是100pts的话肯定不能每次重新dfs,就要考虑一遍dfs维护出所有需要的信息。因为它要的是一定经过某一个点的最大收益路线,一遍dfs的话只能维护出从这个点出发到它的子树里走一圈的最大收益,于是再维护一个g[i][0]数组代表从i这个点出去到它的子树外面走一圈并且回来的最大收益,g[i][1]表示从这个点出去走一圈并且回来的最大收益。但是注意的情况是递推g数组的时候因为要分成在它父亲外面走一圈和在它兄弟节点里走一圈两种情况,所以搞它的兄弟节点的时候不能包括它本身。这个需要注意。

那么到底如何来维护这两个数组呢?对于f[i][0],反正它每次走出去都要回来,所以就一个子树一个子树地考虑。对于当前子树v,f[v][0]是已知的,那么跑到这个子树里面走一圈再回来的收益就可以算出来。如果这个收益大于0,显然选上v这个子树肯定会让解变得更优。也就是说f[i][0]是贪心维护的,枚举每一棵子树的时候只要它的收益大于0就一定要选。

对于f[i][1],它需要在i的所有子树中选一棵子树,当跑到这棵子树里面的时候它就不再回到点i了。除了这棵子树以外的其它子树还是可以用f[v][0]来贪心选择的。那么在递推f[i][1]的时候每次枚举到一个子树v,显然目前的f[i][1]里面存储的结果对应的那棵不回来的子树在v前面。于是就要分成仍然让v之前的那棵子树不回来和让v不回来两种情况讨论。如果仍然让v之前的那棵子树不回来,v就要回来,于是就用f[v][0]贪心地更新;否则因为v要回来,所以v之前所有的都不能回来,就用当前的f[i][0]加上f[v][1]来更新结果——这就要求每次f[i][0]要在f[i][1]之后更新否则f[i][0]里面存的就不是v之前的子树而有可能包括v了,就造成了重复更新。

g数组要求自顶向下而不是自底向上递推,因为只有知道了父亲的情况才能递推儿子。对于g[i][0],它是跑到外面转一圈再回来的最大收益,于是首先继承g[fa][0]的结果,表示它到它父亲节点的外面转了一圈再回来;然后还有它经过父亲到兄弟节点里面转一圈回来的情况,这个肯定不能枚举现算啊对吧,所以我们考虑利用之前的结果。注意到f[fa][0]就已经包括了i的所有兄弟节点的信息,但是问题就在于有可能也同时包括了i,这是不合法的。所以我们要判断一下i有没有对f[fa][0]产生贡献,如果有的话要减去。因为是贪心选择的所以也很好判断。

最后是g[i][1]。需要分成不回来的那条路在它父亲外面还是在它兄弟节点里面两部分讨论。如果不回来的那条路在父亲外面的话就是用g[fa][1]加上它到兄弟节点里转一圈然后回来的收益,这个和上面g[i][0]计算的时候是一样的。关键就是不回来的那条路再兄弟节点里面的这种情况比较麻烦。不能直接用f[fa][1]啥的来更新,因为在计算f[fa][1]的时候所使用的不回来的那个儿子可能本身就是当前要计算的i。也就是用于递推i的那个东西必须保证它不回来的那个儿子必须在i外面。所以在递推f数组的时候又循环了一遍,一开始递推f[u][1]的时候记录了一下它不回来的儿子是哪一个,然后再强制让这个儿子一定拐回来,递推一个f[u][2]。这样在递推g[i][1]的时候,f[fa][1]和f[fa][2]一定至少有一个它选择的不回来的路径是在i外面的,就用那个来更新。更新的时候仍然记得减去可能存在的f[i][0]的贡献。

madan这题解好TM长啊

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n,p[300010],a[600010],nxt[600010],tot;
long long f[300010][3],g[300010][2],val[300010],w[600010],rec[300010];
void add(int x,int y,long long v){
    tot++;a[tot]=y;nxt[tot]=p[x];w[tot]=v;p[x]=tot;
}
void dfs(int u,int fa){
    int tmp=val[u],e;
    f[u][0]=f[u][1]=f[u][2]=val[u];//0:back;1:not back
    for (int i=p[u];i!=0;i=nxt[i])
      if (a[i]!=fa){
          int now,last;
          dfs(a[i],u);
          last=f[u][1]+max((long long)0,f[a[i]][0]-2*w[i]);
          now=f[u][0]+max((long long)0,f[a[i]][1]-w[i]);
          if (now>last){
              f[u][1]=now;rec[u]=a[i];
          }else f[u][1]=last;
          if (f[a[i]][0]>=2*w[i]) f[u][0]+=f[a[i]][0]-2*w[i];
      }
    for (int i=p[u];i!=0;i=nxt[i])
      if (a[i]!=fa&&a[i]!=rec[u]){
          int now,last;
          last=f[u][2]+max((long long)0,f[a[i]][0]-2*w[i]);
          now=tmp+max((long long)0,f[a[i]][1]-w[i]);
          f[u][2]=max(now,last);
          if (f[a[i]][0]>=2*w[i]) tmp+=f[a[i]][0]-2*w[i];
      }else if(a[i]==rec[u]) e=w[i];//记下最大值不回来节点的父边来更新
    f[u][2]=max(f[u][2],f[u][2]+f[rec[u]][0]-2*e);//最后要判断一下
}
void dfs_again(int u,int fa,int edge){
    int son;
    g[u][0]=g[u][1]=0;
    if (rec[fa]==u) son=f[fa][2];
    else son=f[fa][1];//判断选择最大值还是次大值
    son-=max(f[u][0]-2*edge,(long long)0);//减去当前子树的影响
    g[u][1]=max(g[u][1],g[fa][0]+son-edge);//停在兄弟节点里的情况
    if (f[u][0]>2*edge) son=f[u][0]-2*edge;
    else son=0;//计算所有兄弟节点回来的价值和
    son=f[fa][0]-son;//停在父亲外面的情况
    g[u][1]=max(g[u][1],son+g[fa][1]-edge);
    g[u][0]=max(g[u][0],son+g[fa][0]-2*edge);
    for (int i=p[u];i!=0;i=nxt[i])
      if (a[i]!=fa) dfs_again(a[i],u,w[i]);
}
int main()
{
    freopen("treasure.in","r",stdin);
    freopen("treasure.out","w",stdout);
    scanf("%d",&n);
    for (int i=1;i<n;i++){
        int x,y,w;
        scanf("%d%d%d",&x,&y,&w);
        add(x,y,w);add(y,x,w);
    }
    for (int i=1;i<=n;i++) scanf("%d",&val[i]);
    dfs(1,0);dfs_again(1,0,0);
    for (int i=1;i<=n;i++)
      printf("%I64d\n",max(f[i][0]+g[i][1],f[i][1]+g[i][0])); 
    return 0;
}

偏偏在最后出现的补充说明

树形DP中对于“一定经过某一个点的啥啥啥”这种问题来说,这是一种常用的思路啦。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值