Nowcoder 5278G.血压游戏 第18届上海大学网络友谊赛(虚树+dp)

题目描述

Compute 有一棵 n 个点,编号分别为 1∼n 的树,其中 s 号点为根。
Compute 在树上养了很多松鼠,在第 i 个点上住了 ai个松鼠。

因为某些缘故,它们开始同时向根节点移动,但它们相当不安分,如果在同一个节点上,它们就会打起来,简单地来说以下事件会依序发生:

·如果一个节点上有 2 只或 2 只以上的松鼠,他们会打架,然后这个节点上松鼠的数量会减少 1;

·根节点的所有松鼠移动到地面,位于地面上的松鼠不会再打架;

·所有松鼠同时朝它们的父节点移动。

所有事件各自都在一瞬间完成,直至树上没有松鼠。

现在 Compute 想知道最终有多少只松鼠到达了地面。

输入描述:

第一行包含两个整数 n, s (1≤n≤2e5,1≤s≤n),中间以空格分隔,分别表示点的数量和根的编号。

第二行包含 n 个整数 a1, a2, an(0≤ai≤1e9),中间以空格分隔,分别表示每个点上一开始的松鼠数量。

接下来 n-1 行,每行包含两个整数 u, v (u!=v, 1≤u,v≤n),中间以空格分隔,表示 u 和 v 之间有一条边。

输入保证是一棵树。

输出描述:

在一行输出一个整数,表示最终到达地面的松鼠数量。

示例1

输入

3 1
2 4 6
1 2
1 3

输出

8

示例2

输入

3 1
0 1 1
1 2
1 3

输出

1

算法标签:虚树、dp

首先读完题发现,可以将这棵树逐层分离,深度不同的结点不会互相影响,因此只需要枚举层数,每次取出深度相同的点做一次dp就可以了。然后先总结自己的一个错误吧。训练的时候当然先考虑暴力的复杂度,就是O(max{deep}*n)。然后很理想地构造了一个满二叉树,发现复杂度就是O(nlogn)了。。然后居然就觉得暴力是可做的。

在这里插入图片描述
然而其实只要一个数据点给一条链就退化成O(n^2)了。其实做的时候也知道,只是抱侥幸心理。但是树形题的数据怎么可能没有链呢。。 所以以后不可以这样了.jpg

训练完看题解才知道要用虚树做,于是正好回过头借这题补一下虚树的知识。当然建虚树的算法基于dfs序和lca(最近共同祖先),这里就先不展开,重点放在虚树上面。

虚树的基本思想就是从原来完整的树上取出需要的结点,同时取出任意两个结点的lca和根节点,在保证原先的祖先——后代关系的前提下,将这些点再连成一个新树。比如下图,我想要取出左图红色的三个点,那么就需要取出它们和它们两两的lca,建成右图的树状关系。
在这里插入图片描述
这样的好处很明显,如果打绿叉的点或链对答案无关紧要(或者可以通过O(1)快速计算),我们直接在虚树上进行计算的速度会远快于原来完整的树。

在这里插入图片描述
具体会快多少其实不太好算,但是我们知道k个结点两两之间的lca最多有k-1个(极端情况如图),那么虚树中结点最多是2k个。所以即使考虑最坏的情况,对整个树做dp的复杂度也是O(n)级别的。
在这里插入图片描述
当然这里还没有考虑建树的复杂度。那么怎么建虚树呢?朴素思想当然是枚举任意两个点,将这些点和它们的lca都放入集合里,再根据祖先——后代关系连边。但是想想就知道太慢了。。所以就需要dfs序,并引入一个单调堆栈sta。这里的单调性指的是确保堆栈中的元素都在原树的一条链上,并且自底向上保持祖孙关系。比如说堆栈的情况如左图所示,那么可以得出虚树结点间的关系如右图。
在这里插入图片描述
第二个问题是怎样插入结点呢。如果当前将要插入的结点是5号结点,且5号结点是2号结点的后代,则只要直接将5号加入堆栈即可。
在这里插入图片描述
但如果5号结点不是2号结点的子孙呢?这时候事情就会变得比较麻烦。比如当5号结点是3号结点的右儿子时,直接将5号加入堆栈显然不能保持堆栈的单调性。正确的做法是先依次将2号、1号、4号结点弹出堆栈(在出栈的同时连边),再将5号加入堆栈。
在这里插入图片描述
当然,这样讲只是便于一个直观的理解,因为还有许多没有考虑周全的情况。比如怎样确保链的有序性?如果祖先结点还未在虚树中,应该如何同时添加两个结点?解决这些问题的答案是dfs序。严谨的构建方法如下:

我们需要将准备插入的点根据dfs序从小到大排序(令结点n的dfs序为dfn[n]),如果当前要加入的点为x,当前栈顶为y,计算出x和y的lca(x,y),此时分两种情况:
1. lca=y, 意味着x是y的子孙,直接将x入栈即可
2. lca!=y,说明x和y分别在lca的两棵子树中,此时y所在的子树已经构建完毕(由dfs序的性质可知,假设y所在的子树里还有结点k未加入,则dfn[k] < dfn[x],应该先访问k),此时需要同时进行出栈和连边的操作:

while(TOP>=2 && dfn[sta[TOP-1]]>=dfn[lca]){
    E[sta[TOP-1]].push_back(sta[TOP]);
    TOP--;
}

一开始写这段代码的时候不太理解为什么要将lca与堆栈的第二个元素sta[TOP-1]而不是栈顶元素sta[TOP]进行比较,可以考虑下图的情形:
在这里插入图片描述
此时最关键的问题是lca并不在栈中,且dfs序的关系有sta[TOP-1] < lca < sta[TOP]。因此如果将lca与栈顶元素比较,while条件成立,会连一条蓝点sta[TOP-1]->绿点sta[TOP]的边,但明显我们需要的是lca->sta[TOP]这条边。而如果和第二个元素比较,进行到这一步时while循坏就停止了,我们只需要再额外加一个判断,连上lca->sta[TOP],再将栈顶元素替换成lca即可。

if (sta[TOP]!=lca) E[lca].push_back(sta[TOP]),sta[TOP]=lca;

此时y所在的子树所有边都以连好并全部出栈,lca也已入栈,最后只需要将x入栈,该结点的插入操作就完成了。

sta[++TOP]=x;

最后当所有结点都插入完成后,清空堆栈并连边,建出最后一条链。

简而言之,虚树的构建是以链为单位,从左至右进行,并由dfs序保证其有序性。

dp部分比较简单,也不是这篇的重点,就直接放在代码里了。感觉这题还挺适合做模板的(dfs序、lca、建虚树),所以也借鉴了一些别人的代码,专门把各个部分优化了一下。(主要是lca部分,对lg数组的预处理)也算是存个档吧。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N=2e5+5;

int pre[N][25],dfn[N],deep[N],sta[N],TOP,lg[N];

int A[N],n,s,NODE,x,y;

ll ans=0;

vector<int>G[N];
vector<int>E[N];
vector<int>D[N];

bool cmp(int x,int y){ return dfn[x]<dfn[y]; }

void DFS(int x,int fa)
{
    int to;
    dfn[x]=++NODE;
    deep[x]=deep[fa]+1;
    D[deep[x]].push_back(x);
    pre[x][0]=fa;
    for (int i=1;(1<<i)<=deep[x];i++) pre[x][i]=pre[pre[x][i-1]][i-1];
    for (int i=0;i<G[x].size();i++)
    {
        to=G[x][i];
        if (to!=fa)
            DFS(to,x);
    }
}

int LCA(int x,int y)
{
    if (deep[x]<deep[y]) swap(x,y);
    while(deep[x]>deep[y]) x=pre[x][lg[deep[x]-deep[y]]-1];
    if (x==y) return x;
    for (int i=lg[deep[x]]-1;i>=0;i--)
        if (pre[x][i]!=pre[y][i])
            x=pre[x][i],y=pre[y][i];
    return pre[x][0];
}

void Insert(int x)
{
    if (TOP==1){
        if (sta[TOP]!=x) sta[++TOP]=x;
        return;
    }
    int lca=LCA(x,sta[TOP]);
    while(TOP>=2 && dfn[sta[TOP-1]]>=dfn[lca]){
        E[sta[TOP-1]].push_back(sta[TOP]);
        TOP--;
    }
    if (sta[TOP]!=lca) E[lca].push_back(sta[TOP]),sta[TOP]=lca;
    sta[++TOP]=x;
}

ll solve(int x)
{
    if (E[x].empty())
        return 1LL*A[x];
    ll ret=0;
    for (int i=0;i<E[x].size();i++)
    {
        int to=E[x][i];
        ll sol=solve(to);
        if (sol!=0)
            ret+=max(1LL,sol-(deep[to]-deep[x]));
    }
    E[x].clear();
    return ret;
}

int main()
{
    cin>>n>>s;
    for (int i=1;i<=n;i++) lg[i]=lg[i-1]+(1<<(lg[i-1])==i);
    for (int i=1;i<=n;i++) scanf("%d",&A[i]);
    for (int i=1;i<=n-1;i++)
    {
        scanf("%d%d",&x,&y);
        G[x].push_back(y);
        G[y].push_back(x);
    }
    DFS(s,0);
    for (int i=1;!D[i].empty();i++)
    {
        sort(D[i].begin(),D[i].end(),cmp);
        TOP=0;
        sta[++TOP]=s;
        for (int j=0;j<D[i].size();j++)
        {
            Insert(D[i][j]);
        }
        while(TOP>1)
        {
            E[sta[TOP-1]].push_back(sta[TOP]);
            TOP--;
        }
        ll sol=solve(s);
        if (sol) ans+=max(1LL,sol-1);
    }
    printf("%lld",ans);
    return 0;
}
内容概要:本文档是详尽的 Android SDK 中文帮助文档,介绍了 Android SDK 的核心概念、组件、开发环境搭建、基础开发流程及常用工具使用指南。首先解释了 Android SDK 的定义及其核心价值,即提供标准化开发环境,使开发者能高效构建、测试、优化 Android 应用。接着详细列出了 SDK 的核心组件,包括 Android Studio、SDK Tools、Platform Tools、Build Tools、Android 平台版本和系统镜像。随后,文档提供了详细的环境搭建步骤,适用于 Windows、macOS 和 Linux 系统,并介绍了基础开发流程,以“Hello World”为例展示了从创建项目到运行应用的全过程。此外,还深入讲解了 ADB、AVD Manager 和 SDK Manager 等核心工具的功能和使用方法。最后,文档涵盖了调试与优化工具(如 Logcat、Profiler 和 Layout Inspector)、关键开发技巧(如多版本 SDK 兼容、Jetpack 库的使用和资源文件管理)以及常见问题的解决方案。 适合人群:具有初步编程知识,希望深入了解 Android 应用开发的开发者,尤其是新手开发者和有一定经验但需要系统化学习 Android SDK 的技术人员。 使用场景及目标:①帮助开发者快速搭建 Android 开发环境;②指导开发者完成基础应用开发,理解核心工具的使用;③提高开发效率,掌握调试与优化技巧;④解决常见开发过程中遇到的问题。 阅读建议:此文档内容全面且实用,建议读者按照章节顺序逐步学习,结合实际开发项目进行练习,尤其要注意动手实践环境搭建和基础开发流程,同时参考提供的扩展学习资源,进一步提升开发技能。
listen1 是一款优秀的音乐播放软件,在一些音乐平台上可以直接搜索并在线播放音乐。但有时候,我们可能需要将喜欢的音乐保存到本地,以便在没有网络连接的情况下也能欣赏音乐。这时,listen1 的 zip 文件就能派上用场了。 listen1 的 zip 文件实际上是一种压缩文件格式,其中包含了 listen1 软件的安装文件和必要的资源文件。我们可以通过下载 listen1 的 zip 文件,将其解压到计算机的指定目录中,然后运行解压后的文件来安装 listen1 软件。一旦成功安装,我们就能通过 listen1 来搜索、播放和保存喜欢的音乐。 使用 listen1 的 zip 文件安装软件的好处是,它能够以较小的文件体积将软件和资源文件打包起来,方便用户下载和传输。而且,zip 文件的解压步骤也相对简单,对于不具备技术背景的用户来说也能够轻松完成安装过程。 在使用 listen1 的过程中,我们可以将喜欢的音乐保存到本地,形成一个音乐库。这样,即使在没有网络连接的情况下,我们也能够通过 listen1 来随时欣赏自己喜欢的音乐。同时,listen1 还支持将保存的音乐导出为本地文件,以便在其他设备上进行播放和分享。 总之,listen1 的 zip 文件提供了一种方便快捷的安装方式,让用户能够轻松地使用 listen1 软件来搜索、播放和保存喜欢的音乐。无论是在有或没有网络连接的情况下,listen1 都能为用户提供优质的音乐体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值