P3574 [POI2014]FAR-FarmCraft(树形dp)

P3574 [POI2014]FAR-FarmCraft

洛谷
来源: P3574 [POI2014]FAR-FarmCraft
由于我也是这道题的被害人,所以前来提醒后来人

在这里插入图片描述在这里插入图片描述在这里插入图片描述----------------------------------------------------------------------------------------------------

在看洛谷题解的时候相信很多人看到了
在这里插入图片描述或者有很多类似的,其实是有点不准确的
这个真的很误导人,我也是看了一下午,试了又试才搞明白

这里明确一下f数组和g数组的具体含义

f[i]表示当i的子树全部安装好游戏(当然也包括i本身自己,但不包括跟节点)

g[i]表示遍历完i子树需要的时间,这个很简单,就是个偶数,因为要进子树出子树,但我们在用转移公示时,还并不是这个意思,这只是最终要的结果,下面会详细讲

在这里插入图片描述就看样例,我们考虑两种情况

一、我们从根节点1开始遍历完子树回到1然后等待根节点安装好游戏,这个时间我们算一下

二、计算从出发到几分钟后,所有子树(不包括根节点)把游戏安装好,不要求回来,但是能回来尽量回来,反正这个时间没有影响

为什么考虑这两种情况呢,我们不难可以想象到第一种情况应该是最少用时,不可能比第一种时间还短了,然后我们比较一和二,谁大结果就是谁

具体为什么呢,假设第一种时间为6分钟,第二种需要7分钟
分开看,我们知道,我们的6分钟是必要的,是我们的路程加根节点时间,那为什么第二种是7分钟呢,其实就是在子树中有一个点需要安装时间有点长,所以把总安装好(不包括根节点)时候拖到了7分钟,这种情况就是我们回到了根节点,并且根节点已经安装好了,不过我还要再等一分钟,让那个需要时间长的也安装好

当然我们在计算第二种时间的时候要求出最小时间,就是怎样可以尽量使第二种时间小,但又要符合全部安装好


考虑完根节点,下面挨个节点开始考虑

对于节点2,他有子节点3和4,那我们就要考虑先遍历4还是先遍历3了,这时我们就需要这样比较f[a] - g[a] > f[b] - g[b];一下,我上面说描述f[i] > g[i]不准确,其实是因为,f[i] 完全可以小于等于 g[i]

我们先考虑f[i] > g[i]时,那f[i] - g[i],就是我遍历这个i这个子树需要等待的时间,我自然希望这个等待时间长的在前面,所以会进行f[a] - g[a] > f[b] - g[b];这样排序

那当f[i] <= g[i]时,我们知道f[i] - g[i]是小于等于0的,也就是说,不需要等待的,我们遍历完子树i,他里面包括i就可以安装好游戏,那么这时,f[i] - g[i]在排序时一定会排在后面,也是我们希望在后面的,前面是需要等待的,那不需要等待的肯定不止一个,为什么他们之间不需要严格排序呢,我们继续往下看就可以看出来了

先给出转换方程

for (int i = 1; i <= cnt; i++)
    {
        f[u] = max(f[u], f[son[i]] + g[u] + 1);
        g[u] += g[son[i]] + 2;
    }

u表示节点,cnt表示u连接到的点数,然后挨个进行转换方程

用a[i]表示安装游戏的时间
对于f[u]刚开始等于a[u]这个不难理解,因为我们要找出可以把这个u子树覆盖掉(u包括里面子节点都安装好游戏)的时间的最小值,我们肯定要先用u这个点来跟后面的比较。

g[u]刚开始是为0的,表示我们遍历完son[i]前需要的时间,因为我们是一个一个开始遍历的,先遍历肯定不需要计算后面的遍历时间,就假设我先遍历3,其中有一个等待时间比较长总共需要1000分钟,那么后面可能算上遍历3的时间再加上他的时间都没有1000大,那我肯定还用1000,而且还必须先用这个1000,如果先遍历后面再加这个1000需要时间肯定会更大

我是一个节点一个节点往后看的,所以每次遍历g[u]加上这个子节点的遍历时间再加2,就是进出这个子节点的时间

f[u] = max(f[u], f[son[i]] + g[u] + 1);

这时候再看这句话,对于u里面全部安装好游戏的时间,与f[son[i]] + g[u] + 1相比较,对于我遍历的子节点,他里面安装好游戏的时间+遍历这个子节点前面子节点时间+1,这个1就是我从u再进入son[i]的时间

还有一个遗留问题,当f[i] <= g[i]时为什么不需要严格排序,其实往里面带一下就知道了,怎么排序是没有区别的,因为不需要等待,所以后面的时间,就是遍历树的时间,你用什么方式遍历后面一点影响没有

我们不需要计算回来的时间,具体看我最上面分析的两种情况的第二种

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long ll;

inline int read(void)
{
    register int x = 0;
    register short sgn = 1;
    register char c = getchar();
    while (c < 48 || 57 < c)
    {
        if (c == 45)
            sgn = 0;
        c = getchar();
    }
    while (47 < c && c < 58)
    {
        x = (x << 3) + (x << 1) + c - 48;
        c = getchar();
    }
    return sgn ? x : -x;
}
inline void write(ll x)
{
    if (x < 0)
        putchar('-'), x = -x;
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}

const int N = 5e5 + 10;
int e[2 * N], ne[2 * N], h[2 * N], idx; //链表
int a[N];
int f[N];   //子树最大值
int g[N];   //遍历完子树的路长
int son[N]; //子树

bool cmp(int a, int b) { return f[a] - g[a] > f[b] - g[b]; }//自定义排序

void add(int a, int b)//链表存边
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void dfs(int u, int fa)//两个参数,u表示当前节点,fa表示这个u是从fa过来的
{
    if (u != 1)//如果不是根节点
        f[u] = a[u];//就算上自己
    for (int i = h[u]; ~i; i = ne[i])//遍历所有可到的点
        if (e[i] != fa)//如果不是来的时候那个点,就交给递归
            dfs(e[i], u);//交给递归就完事
    int cnt = 0;
    for (int i = h[u]; ~i; i = ne[i])
        if (e[i] != fa)//找出所有可到的点,除了自己的父节点
            son[++cnt] = e[i];//存入数组中
    sort(son + 1, son + 1 + cnt, cmp);//排序
    for (int i = 1; i <= cnt; i++)//树形dp
    {
        f[u] = max(f[u], f[son[i]] + g[u] + 1);
        g[u] += g[son[i]] + 2;
    }
}

int main()
{
    // freopen("in.txt", "r", stdin);
    // freopen("out.txt", "w", stdout);
    memset(h, -1, sizeof h);//初始化链表第一个为-1
    int n = read();
    for (int i = 1; i <= n; i++)
        a[i] = read();
    for (int i = 1; i < n; i++)
    {
        int x = read(), y = read();
        add(x, y);
        add(y, x);
    }

    dfs(1, 0);//递归

    cout << max(f[1], g[1] + a[1]) << endl;//比较两种情况
    return 0;
}

如果有不理解的欢迎讨论,以前纯属我自己思考,有误的话可以提出来

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Miss .

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值