把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

【题解】$test0628$ 大逃杀 - 树形 $dp$ - 树上背包

ybt 1774 大逃杀

题目描述

将地图上的所有地点标号为 \(1\)\(n\) ,地图中有 \(n−1\) 条双向道路连接这些点,通过一条双向道路需要一定时间,保证从任意一个点可以通过道路到达地图上的所有点。

有些点上可能有资源,到达一个有资源的点后,可以获取资源来增加 \(w_i\) 的武力值。资源被获取后就会消失,获取资源不需要时间。可选择不获取资源。

有些点上可能有敌人,到达一个有敌人的点后,必须花费 \(t_i\) 秒与敌人周旋,并将敌人消灭。敌人被消灭后就会消失。不能无视敌人。

如果一个点上既有资源又有敌人,必须先消灭敌人才能获取资源。

游戏开始时Y君可以空降到任意一个点上,接下来,有 \(T\) 秒时间行动,Y君希望游戏结束时,武力值尽可能大。

\(\\\)


\(\\\)

\(Solution\)

奇怪——の前言

这!道!题!真!的!太!妙!辣!

由于太妙了所以看完之后总有虚幻之感(?感觉看题解代码跟吸毒似的,上一秒飘飘然下一秒就忘了(?

所以为了深刻贯彻周树人的“拿来主义”精神,把这道题变成“我的”, 真正地弄懂这道题,于是来写篇题解吧!

\(\\\)

正解

首先,发现这是个无根树,所以大致有两个方向,一个是换根,一个是用一些奇怪的状态包揽所有情况

先考虑用状态包含所有情况

正常能想到的有两个状态,\(f[i][j]\) 表示从 \(i\) 往子树内走并且回到 \(i\)\(g[i][j]\) 表示从 \(i\) 往子树内走但不用回到 \(i\)

但是 \(i\) 还可以往父亲结点走,这怎么设状态?显然,假设 \(i\) 往外走到 \(j\) ,那么这条路上一定有一个转折点 \(LCA(i, j)\) ,若 \(j = LCA(i, j)\) 就包含在上面的 \(g[LCA(i, j)][j]\) 中了

由于考虑要在 \(i\) 结点设状态,所以就设 \(h[i][j]\) 表示从 \(i\) 子树中某个结点走到 \(i\) 再走到子树中另一个结点

是否包含了所有状态?建议多画图想想。严谨证明嘛...待以后填坑233

\(\\\)

与换根千丝万缕的联系

为什么这道看似要换根的题目,直接用三个状态简洁明了的就实现了?

一般来说,换根都会分 \(up\)\(down\) 两个方面分别考虑 \(i\) 子树外和 \(i\) 子树内,上面的 \(f[i][j]\)\(g[i][j]\) 就是 \(down\) 这部分

而在这道题中,对于 \(i\) 结点,\(up_i\) 其实就是 \(h[LCA(i,k)][j]\) ,只不过计算 \(up_i\) 的时候并不是递归到 \(i\) 点时,是在 \(LCA(i, k)\) 那里是就考虑完了。

也就是说,\(up\) 在这道题里并不是精准的,一个 \(h[i][j]\) 的状态其中包含了很多的 \(up_i\) ,一个 \(up_i\) 又包含了很多个 \(h[LCA(i, k)][j]\) ,相当于把 \(up_i\) 拆散了计算的。

为什么可以这样做?

归根结底来说,是因为这道题中并不特地强调根节点的作用,而只强调在树中的路径。

而在 ybt 1773 消息传递ybt 1771 仓库选址 中,它们所求的答案都要求了必须确定根节点,这就要求我们必须通过换根精确地得到结点的值,所以不能拆开计算,再统计答案。

\(\\\)

具体实现

每次我碰上这种两个及两个以上的状态互相更新的时候,往往都会东添西补两三个小时以上,最后重构代码...

所以趁这道题来尝试着缕清一下,各个状态之间是如何转移的,怎样才能写得完整且逻辑清晰。

.....................好了我咕了..........理不清楚kkk,等过一段时间再填坑吧反正题解没人看233
\(\\\)

完结撒花✿✿ヽ(°▽°)ノ✿

\(\\\)


\(\\\)

\(Code\)

以下代码参考 此位大佬 der!

#include<bits/stdc++.h>
#define F(i, x, y) for(int i = x; i <= y; ++ i)
using namespace std;
int read();
const int N = 305;
int n, T, u, v, e, ans;
int t[N], w[N];
int f[N][N], g[N][N], h[N][N];
int head[N], cnt, ver[N << 1], edge[N << 1], nxt[N << 1];
void add(int x, int y, int z){ver[++ cnt] = y, edge[cnt] = z, nxt[cnt] = head[x], head[x] = cnt;}
void dfs(int x, int fa)
{
   for(int i = head[x]; i; i = nxt[i])
      if(ver[i] != fa)
      {
         dfs(ver[i], x);
         for(int j = T; j >= t[x]; -- j)
         {
            for(int k = j - edge[i]; k >= t[x]; -- k)
            /*为什么 k 也要逆序枚举?当 edge[i] 为 0 时,如果顺序枚举,那 f[x][j] 
                等都被 k 更新完了,最后 k=j 时就无法再用原来的 f[x][j] 等更新了*/
            {
               int tmp = j - k - edge[i];
               int fx = f[x][k], gx = g[x][k], hx = h[x][k];
               /*为什么要提前记录函数值?不完全是为了代码简洁。当 edge[i] 为 0 时,
                   fx,gx,hx 是可能被更新的,如果要用它们自己原本的值更新自己,就  
                  要提前记录原本的值*/
               if(tmp >= t[ver[i]])
               {
                  g[x][j] = max(g[x][j], g[ver[i]][tmp] + fx);
                  h[x][j] = max(h[x][j], g[ver[i]][tmp] + gx);
               }
               tmp -= edge[i];
               if(tmp >= t[ver[i]])
               {
                  g[x][j] = max(g[x][j], f[ver[i]][tmp] + gx);
                  f[x][j] = max(f[x][j], f[ver[i]][tmp] + fx);
                  h[x][j] = max(h[x][j], f[ver[i]][tmp] + hx);
                  h[x][j] = max(h[x][j], h[ver[i]][tmp] + fx);
               }
            }
            ans = max(ans, h[x][j]);
         }
      }
}
int main()
{
   n = read(), T = read();
   F(i, 1, n) w[i] = read();
   F(i, 1, n)
   {
      t[i] = read();   
      if(t[i] > T) continue;
      f[i][t[i]] = g[i][t[i]] = h[i][t[i]] = w[i];//提前初始化不容易忘
   }
   F(i, 1, n - 1) u = read(), v = read(), e = read(), add(u, v, e), add(v, u, e);
   dfs(1, 0), printf("%d", ans);
   return 0;
}
/*--------------- Bn_ff 2020.7.3 ybt1774 ---------------*/
int read()
{
   int x = 0, f = 1;
   char c = getchar();
   while(c < '0' || c > '9') {if(c == '-') f = -1; c = getchar();}
   while(c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
   return x * f;
}
posted @ 2020-07-04 07:50  Bn_ff  阅读(148)  评论(0编辑  收藏  举报
浏览器标题切换
浏览器标题切换end