这题我一看的时候是没有什么思路的。题目意思很明确,但是不知道如何高效地求出来。

看到数据范围,感觉 40 分的暴力应该是可以的。

但是写暴力也要有技巧。经过我一番慎重的思考和比较,发现从“当前节点能控制多少后代”和“当前节点能被多少祖先控制”两个角度考虑是不一样的。

虽然题目的设问是从前者的角度,但是每个节点可能会有很多后代,而每个结点只有唯一的父亲,唯一的父亲的父亲……

有了这个前提,我就很快想到了一种简单粗暴的方法:将每个节点作为 vv 考虑,各跑一遍 dfs,记一个参数 ss,表示“剩余的距离量”,当 s<0s 时停止。

也就是说,题目要求的是

dis(u,v)≤av

dis(u,v) \leq a_v即

av−dis(u,v)≥0

a_v - dis(u,v) \geq0不妨设当前点为 v′v',所记的 ss 就是到目前为止的 av−dis(v′,v)a_v - dis(v',v)。

每次去向 v′v' 的父亲 uu 时,有 s′=a[v]−dis(u,v)   =a[v]−(dis(v′,v)w(u,v′))   =(a[v]−dis(v′,v))−w(u,v′)   =s−w(u,v′)s'=a[v]-dis(u,v)\\\text{ }\text{ }\text{ }=a[v]-(dis(v',v) w(u,v'))\\\text{ }\text{ }\text{ }=(a[v]-dis(v',v))-w(u, v')\\\text{ }\text{ }\text{ }=s-w(u, v')

也就是每次递归的时候更新参数只需减去当前节点与父亲之间的边权即可。这种方法编程十分容易,我只写了 10 分钟不到,代码 31 行,拿了 50 分。
代码:

#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>using namespace std;const int MAXN = 1e5 + 100;int N, M;
int a[MAXN], par[MAXN], ans[MAXN];
long long len[MAXN]; //注意数据范围,答案可能很大,建议使用 long long 类型void dfs(int root, long long dis) {if (dis < 0 || !root) return; //边界条件++ans[root]; //当前的 i 可以被 root 控制dfs(par[root], dis - len[root]); //剩余距离减去相应的边权
}int main(void) {freopen("2095.in", "r", stdin);freopen("2095.out", "w", stdout);scanf("%d", &N);for (int i = 1; i <= N; i++) scanf("%d", &a[i]);for (int i = 2; i <= N; i++) scanf("%d%lld", &par[i], &len[i]);for (int i = 2; i <= N; i++) dfs(par[i], a[i] - len[i]);for (int i = 1; i <= N; i++) printf("%d ", ans[i]);return 0;
}

但是,我们当然不能就此止步。

上面这个想法,“考虑后代对祖先的贡献”其实是非常好的,反题目之道而行之,而正解也是要从这一步入手。

首先分析一下,上面的算法为什么会慢,其实关键就在 dfs 部分,一步一步往上找,太浪费时间。

我们知道,在树上,每个节点 ii 到根节点的距离是易求的,跑一遍 dfs 就可以算出来了,不妨记为 sumisum_i。

而在控制关系中,uu 为 vv 的祖先,显然有

dis(u,v)=sumv−sumu

dis(u,v) = sum_v - sum_u根据上面提到的,可以将

dis(u,v)≤av

dis(u,v) \leq a_v 变形为

av−dis(u,v)≥0

a_v - dis(u,v) \geq0再代入得到

av−(sumv−sumu)≥0

a_v - (sum_v - sum_u) \geq0整理可得

sumu≥sumv−av

sum_u \geq sum_v - a_v这里用到了一个常用的技巧,即将关于两个量的限制变形,使每个量各独立在等号或不等号的一边。

现在不难发现,对于所枚举的一个确定的 vv,sumv−avsum_v - a_v 为定值

则任务可转化为:找到满足 sumusum_u 大于等于这个定值且深度最小的 u。求得后,uu 到 vv 路径上的点(除去 vv 自身)都可以控制 uu。

通过计算还可以发现,对于一条从祖先到后代的路径上的节点 ii,sumisum_i 随着 ii 的深度增加而递增,也就是说可以认为是具有单调性的。

二分!

现在来看要找的要求,不是正好满足求一个 lower_bound 的特征嘛!

如何实现二分呢?有两种方法。

第一种是我在考试的时候现场手推的倍增(以前学过,但是忘了),Ghastlcon 也用的是这种方法。关于倍增这里不再详细介绍,详细资料请自行查询。

记 par[i][j]par[i][j] 为已经算得的节点 ii 的第 2j2^j 代父亲(第 1 代父亲就是父亲,第 ii 代父亲是指第 (i−1)(i-1) 代父亲的第 1 代父亲),则可以在 O(log2n)O(log_2n) 时间内求出节点 ii 的任意代父亲。限于篇幅,且方法不难实现,这里不再详细写出如何算。再记 depidep_i 表示结点 ii 的深度,可以 O(n)O(n) 跑一遍 dfs 算出。

在二分的时候 l,rl, r 组成树上一条祖先到后代的路径,uu 在这个路径上,位置待定。而这条路径的中点 midmid 就是 rr 的第 depr−depl2\frac{dep_r-dep_l}{2} 代父亲。

如果 summidsum_{mid} 小于之前所说的定值,说明 midmid 在太高的位置了,已经控制不到 vv,因此 l←midl \leftarrow mid;否则 midmid 目前还可以控制到 vv,尝试更高些,r←midr \leftarrow mid。

最后就可以求得满足条件且深度最小的 uu。之后怎么做,暂时先不说。

第二种方法我感觉比较巧妙,是 aspe 提出的,不用预处理,只需借助一个栈。

不用直接按编号枚举 vv,而是通过跑一遍 dfs,在这个过程中用一个栈保存下已被遍历过但子树未被遍历完的节点,则从栈底到栈顶的节点就组成了从根节点到当前节点的一条路径,直接在每个节点入栈的时候顺便求 sum[]sum[],就可以在这上面做二分,求出 uu 了。

但现在我们也仅仅只是对于每一个 vv 求出了对应的 uu,知道了 uu 到 vv 这条路径上的点(除 vv 外)都能控制 vv。如何计算答案呢?

显然,如果再去一步一步往上,给这条路径上的点加答案,那么这一步最坏情况都要花 n2n^2 的时间,岂不是前功尽弃?

这里就要用到差分思想。

回顾这样一个问题:在一个数轴上,每次指定一段 [li,ri][l_i,r_i],给这个区间内的数加上 1,最后询问某个点的值。相信很多人都会想到在 lil_i 加 1,在 ri+1r_i+1 的位置减 1,从左往右做一遍关于这些标记的前缀和,就可以了。这样一来就把对区间的操作转化成对端点的操作,其实就是差分思想。具体的应用,可以结合这篇文章的讲解,并参考 GCOI2015 小学六年级组的“计时器”一题。

现在回顾到本题,其实也可以在树上做差分(事实上,这还将会是一种相当常用且好用的技巧)。可以把一条条路径就想象为数轴上的一个个区间,就可以通过在 vv 的父亲结点打一个 +1 标记,在 uu 的父亲结点打一个 -1 标记,之后再跑一遍 dfs(可以毫不夸张地说,在处理树的相关问题中,dfs 真的是利器)。

最后跑的这遍 dfs,先递归到叶子结点,则它们的 ansians_i 都为 0(显然它们没有后代,不可能控制其他节点)。而对于非叶节点,它的 ansians_i 就等于它所有儿子的 ansjans_j 之和(想象一下,相当于 ii 延续着一条从下面一直到 jj 并可能将继续往上的路径)加上自身的标记(自己开始/结束某条路径)。

到这里,本题就被完美解决了。时间复杂度(我用的是方法一)为 O(nloglogn)O(nloglogn)。

参考代码:

#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>using namespace std;const int MAXN = 1e5 + 100;struct EDGE { int to, next; long long cost; } edges[MAXN << 1]; //此处其实开一倍足矣,但在保证不 MLE 的前提下开两倍更保险。int N, M;
int maxd, dep[MAXN], upper_lim;
long long a[MAXN], par[MAXN][20], len[MAXN], sum[MAXN], mark[MAXN], ans[MAXN];int head[MAXN];
void add_edge(int u, int v, long long w) {edges[M++] = (EDGE){v, head[u], w};head[u] = M - 1;
}void dfs1(int pre, int root, long long dis) { //负责求出各节点到根节点的距离并统计深度sum[root] = dis; maxd = max(maxd, dep[root]);for (int i = head[root]; i != -1; i = edges[i].next) {int v = edges[i].to;if (v != pre) {dep[v] = dep[root] + 1;dfs1(root, v, dis + len[v]);}}
}int query(int r, int d) { //倍增思想,借助 par 数组询问节点 r 的第 d 代父亲for (int p = 0; d; p++, d >>= 1) if (d & 1) r = par[r][p];return r;
}void solve(int cur) {if (len[cur] > a[cur]) return; //连父亲都控制不了自己int l = 0, r = par[cur][0]; //(l, r]while (dep[l] + 1 < dep[r]) {int mid = query(r, dep[r] - dep[l] >> 1);if (sum[cur] - sum[mid] <= a[cur]) r = mid; else l = mid;//这里我在比赛的时候犯了一个致命的错误,原先我的 l,r 表示的是 u 在 cur 的第 2^l 代父亲和第 2^r 父亲之间//后来直接用 l 和 r 表示具体节点,结果忘记把 sum[par[cur][mid]] 改成 sum[mid] 了,浪费了大量时间进行调试}++mark[par[cur][0]]; //从节点 cur 的父亲开始可以控制 cur--mark[par[r][0]]; //到节点 r 的父亲处停止控制
}void dfs2(int pre, int root) {ans[root] = mark[root]; //自身标记for (int i = head[root]; i != -1; i = edges[i].next) {int v = edges[i].to;if (v != pre) {dfs2(root, v);ans[root] += ans[v]; //各儿子答案之和}}
}int main(void) {freopen("2095.in", "r", stdin);freopen("2095.out", "w", stdout);scanf("%d", &N);for (int i = 1; i <= N; i++) scanf("%lld", &a[i]);memset(head, -1, sizeof head);for (int i = 2; i <= N; i++) {scanf("%d%lld", &par[i][0], &len[i]);add_edge(par[i][0], i, len[i]); //可以通过 par[i][0] 直接去向父亲,因此只需连一条父亲到儿子的边。}dep[0] = -1; dfs1(0, 1, 0);upper_lim = log2(maxd);for (int i = 1; i <= upper_lim; i++) //预处理部分for (int j = 2; j <= N; j++)par[j][i] = par[par[j][i - 1]][i - 1];/*for (int i = 2; i <= N; i++) {for (int j = 0; j <= upper_lim; j++) printf("%d ", par[i][j]);putchar('\n');}
*/for (int i = 2; i <= N; i++) solve(i);dfs2(0, 1);for (int i = 1; i <= N; i++) printf("%lld ", ans[i]);return 0;
}

[SMOJ2095]Bug2相关推荐

  1. Bug2算法的实现(RobotBASIC环境中仿真)

    移动机器人智能的一个重要标志就是自主导航,而实现机器人自主导航有个基本要求--避障.之前简单介绍过Bug避障算法,但仅仅了解大致理论而不亲自动手实现一遍很难有深刻的印象,只能说似懂非懂.我不是天才,不 ...

  2. 开发避坑2——大鸟 pk Bug2(SVN报错:database disk image is malformed

    开发避坑2--大鸟 pk Bug2(SVN报错:database disk image is malformed) 报错信息 svn报错database disk image is malformed ...

  3. Javascript使用三大家族和事件来DIY动画效果相关笔记(一)

    1.offset家族 ◆offsetWidth和offsetHeight表示盒子真实的宽度高度,这个真实的宽度包括 四周的边框.四周的padding.及定义的宽度高度或内容撑开的高度和宽度,可以用来检 ...

  4. 从传感器到算法原理,机器人、视觉避障尽在此文

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 本文转自:计算机视觉life 避障是指移动机器人在行走过程中,通过 ...

  5. Tensorflow C++ 编译和调用图模型

    简介 最近在研究如何打通tensorflow线下 python 的脚本训练建模, 利用freeze_graph工具输出.pb图文件,之后再线上生产环境用C++代码直接调用预先训练好的模型完成预测的工作 ...

  6. 汇编和python-python与汇编

    广告关闭 2017年12月,云+社区对外发布,从最开始的技术博客到现在拥有多个社区产品.未来,我们一起乘风破浪,创造无限可能. 我一直在玩dis库来反汇编一些python源代码,但我看到这并不是递归到 ...

  7. 深入理解定位父级offsetParent及偏移大小

    偏移量(offset dimension)是javascript中的一个重要的概念.涉及到偏移量的主要是offsetLeft.offsetTop.offsetHeight.offsetWidth这四个 ...

  8. 第二次结对编程之软件测试

    必应缤纷桌面测试报告 报告人员:10061152----李嘉良,  10061183----谢永青 软件名称: 必应缤纷桌面 (http://desktop.bing.msn.cn/) 测试环境: 第 ...

  9. 我是如何去了解需求的

    半年没有接触.net了,最近公司又让我参与半年前就已经启动的一个.net web项目,这次我负责一部分编码和所有的测试.为了快速的融入团队的开发工作中,我就必须尽快熟悉整个项目的环境. 这个项目虽然提 ...

最新文章

  1. 一篇文章告诉你标准化和归一化的区别?
  2. 对标Mobileye!百度Apollo公布L4级自动驾驶纯视觉解决方案Apollo Lite
  3. 实操教程|使用图像分割来做缺陷检测的一个例子
  4. Linux 网络管理(1) - 网络配置文件
  5. dw读取access中的图片_怎样从Access数据库中读取图片?解决办法
  6. java 算法 排序算法_Java七种排序算法以及实现
  7. 【OpenCV入门学习笔记2】:Mat对象
  8. 通过Spring将继承树加载到List中
  9. [转载] python常用库
  10. OpenJudge NOI 1.6 07:有趣的跳跃
  11. node事件循环 EventEmitter 异步I/O Buffer缓冲区 模块
  12. ZLYD团队第一周项目总结
  13. 程序员面试宝典问题及解析
  14. python numpy 数据类型为python对象-关于Numpy数据类型对象(dtype)使用详解
  15. php居民小区物业管理系统
  16. 自然语言处理--基于规则(AIML)的问答机器人
  17. 使用 PHPMailer 配合 QQ邮箱 发送邮件
  18. CentOS 无法连接网络解决办法
  19. 自制Chrome绿色版
  20. 王姨劝我学HarmonyOS鸿蒙2.0系列教程之四Git搭建下载实例!

热门文章

  1. Android TV 8.0 Browser播放视频闪退
  2. itextpdf 添加折线图、饼图、柱状图
  3. 软件质量属性:可修改性
  4. Java中关于时间类的用法
  5. robocopy 遷移共享文件夾
  6. 全国计算机考试考ms还是wps,计算机wps和ms哪个简单?计算机一级office考试技巧有什么?...
  7. 日本最省女孩15年买下3套房的启示
  8. 解决 error: failed to push some refs to ‘https://github.com/mxp520/test-spring-cloud-base.git‘
  9. 解析几何复习(二)正交变换和仿射变换
  10. 网络攻防之dns劫持与网页挂马(实测)