【ybt金牌导航4-1-1】【luogu P1552】派遣(左偏树模板)

派遣

题目链接:ybt金牌导航4-1-1 / luogu P1552

题目大意

有一棵有根树,每个点有花费和贡献。
然后你可以选一个点,在它的子树中选一些点,使得它们的花费和不大于一个值的前提下,它们的个数乘你选的点的贡献这个值尽可能大。
输出这个值。

思路

这道题我们考虑贪心一下,对于你选定的点,它会有一些点可以选。
那你会发现选任何一个点产生的贡献都是一样的,那我们考虑贪心选费用更小的。

那这个我们可以用堆来搞,搞一个大根堆,把子树的所有点都放进去。然后一直取出堆顶的,知道堆里面数的和不超过规定的数。

那你不能每次枚举点都直接暴力建堆,我们考虑怎么优化。
会发现某个点对应的堆由它的所有子树和它自己这个点这些东西组合而成。
那如果在子树中已经被去掉的点放到这里的堆也一定会被去掉,因为规定的数不变。所以,它的子树对应的堆可以代替它的子树。

那这就涉及了堆的合并以及堆顶的弹出,容易想到用左偏树。

左偏树有两个性质:
首先,第一个是堆性质,就是假设是大根堆的话,那你某个点的键值就要大于它两个儿子的键值。
第二,定义距离是点到每个没有两个儿子的点(叫做外节点)的树上距离的最小值。那每个节点左儿子的距离不小于右儿子的距离。

第二个性质,就使得它得以合并。
首先,根据第二个性质,我们可以得到第三个性质:某个点的距离等于它右儿子的距离加一。为什么呢?因为按照第二个性质,离这个点最近的外节点就是一直往右走所能到达的点。那你就可以得出第三个性质了。

那如何合并呢?(这里以大根堆为例子)
你可以先根据性质一,把堆顶键值大的堆的堆顶作为新的堆顶,然后你可以想到,它左边就还是原样子,右边就要和另一个堆合并在一起。
那就递归右边的。

然后你这样一搞之后,可能会使得性质二不满足,那你就把左右儿子交换一下即可。

当然,你这么一搞,新的堆的堆顶距离也要重新求,那就根据第三个性质求即可。

接着来看看别的操作如何实现。

单点插入:
那一个节点可以看成也是一个左偏树,那单点插入就变成了两个左偏树的合并,那调用一下函数即可。

删除堆顶节点:
那你可以想想,你不要堆顶的之后,那就变成了两个堆,那就把这两个堆合并即可。

构建左偏树:(这题没有用到,就没写出来)
直接一个一个点插入也可以,但是有点慢,这里有 O(n) 的方法。
大致的想法就是倍增?之类的思想。
就是搞个队列,一开始就是 n 个单点堆,然后每次合并队列前两个堆,然后把合并后的堆放到队尾,把队头的两个踢掉。
到最后剩下一个堆,那就是我们要的了。

删除一个已知节点:(这题没有用到,就没写出来)
那删除之后,会有三棵树,分别是你删除的点的两个子树以及不包含你删除的点下的子树的点构成的数。
然后前两个树你可以直接合并,但是问题就在剩下的两个数怎么合并。

那你可以看到,如果合并之后的堆的父亲是根节点,那就直接好了。
但不一定啊。
然后如果合并后的堆堆顶的距离加一是它父亲的距离,那不过它是在左边右边,都是这个值,就直接好了。
然后如果上面是小了,那它就不满足性质二,就要交换。
然后如果上面是大了或者交换了,那就大了。那我们还要分开左右子树来看。
如果合并之后的那个堆是左子树,那就不会有影响,就直接好了。
那如果是右子树,那就调整一下合并后堆堆顶的父亲的距离,然后继续向上解决。

这个给下代码:

int delete_ry(int x) { int X = fa[x], Y = merge(tree[x].l, tree[x].r); fa[Y] = X; if (X && tree[Y].l == x) tree[Y].l = Y; if (X && tree[Y].r == x) tree[Y].r = Y; while (X) { if (tree[tree[X].l].dis < tree[tree[X].r].dis) swap(tree[X].l, tree[X].r); if (tree[tree[X].r].dis + 1 == tree[X].dis) return ; tree[X].dis = tree[tree[X].r].dis + 1; Y = X; X = fa[Y]; } }

代码

#include<cstdio> #include<algorithm> #define ll long long using namespace std; struct road { ll to, nxt; }e[300001]; struct node { ll l, r, key, dis, num, sum; }tree[2000001]; ll n, m, tot, y, rt[100001]; ll c[100001], l[100001]; ll le[100001], KK, fa[100001]; ll ans; void add(ll x, ll y) { e[++KK] = (road){y, le[x]}; le[x] = KK; } ll make_tree(ll x) { tree[++tot] = (node){0, 0, c[x], -1, 1, c[x]}; return tot; } ll merge(ll x, ll y) { if (!x) return y; if (!y) return x; if (tree[x].key < tree[y].key) { swap(x, y); } tree[x].r = merge(tree[x].r, y); if (tree[tree[x].l].dis < tree[tree[x].r].dis) swap(tree[x].l, tree[x].r); tree[x].dis = tree[tree[x].r].dis + 1; tree[x].num = tree[tree[x].l].num + tree[tree[x].r].num + 1; tree[x].sum = tree[tree[x].l].sum + tree[tree[x].r].sum + tree[x].key; return x; } void insert(ll x, ll Y) { ll X = make_tree(x); Y = merge(X, Y); } ll delete_max(ll x) { return merge(tree[x].l, tree[x].r); } void dfs(ll now, ll father) { fa[now] = father; for (ll i = le[now]; i; i = e[i].nxt) if (e[i].to != father) { dfs(e[i].to, now); rt[now] = merge(rt[now], rt[e[i].to]); while (tree[rt[now]].sum > m) { rt[now] = delete_max(rt[now]); } ans = max(ans, 1ll * l[now] * tree[rt[now]].num); } ans = max(ans, 1ll * l[now] * tree[rt[now]].num); } int main() { scanf("%lld %lld", &n, &m); for (ll i = 1; i <= n; i++) { scanf("%lld %lld %lld", &y, &c[i], &l[i]); add(y, i); } for (ll i = 1; i <= n; i++) { rt[i] = make_tree(i); if (tree[i].sum > m) { rt[i] = delete_max(rt[i]); } } dfs(1, 0); printf("%lld", ans); return 0; }

__EOF__

本文作者あおいSakura
本文链接https://www.cnblogs.com/Sakura-TJH/p/YBT_JPDH_4-1-1.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   あおいSakura  阅读(31)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示