闇の連鎖
闇の連鎖
传说中的暗之连锁被人们称为 Dark。
Dark 是人类内心的黑暗的产物,古今中外的勇者们都试图打倒它。
经过研究,你发现 Dark 呈现无向图的结构,图中有 $N$ 个节点和两类边,一类边被称为主要边,而另一类被称为附加边。
Dark 有 $N–1$ 条主要边,并且 Dark 的任意两个节点之间都存在一条只由主要边构成的路径。
另外,Dark 还有 $M$ 条附加边。
你的任务是把 Dark 斩为不连通的两部分。
一开始 Dark 的附加边都处于无敌状态,你只能选择一条主要边切断。
一旦你切断了一条主要边,Dark 就会进入防御模式,主要边会变为无敌的而附加边可以被切断。
但是你的能力只能再切断 Dark 的一条附加边。
现在你想要知道,一共有多少种方案可以击败 Dark。
注意,就算你第一步切断主要边之后就已经把 Dark 斩为两截,你也需要切断一条附加边才算击败了 Dark。
输入格式
第一行包含两个整数 $N$ 和 $M$。
之后 $N–1$ 行,每行包括两个整数 $A$ 和 $B$,表示 $A$ 和 $B$ 之间有一条主要边。
之后 $M$ 行以同样的格式给出附加边。
输出格式
输出一个整数表示答案。
数据范围
$N \leq 100000, \ M \leq 200000$,数据保证答案不超过#{2}^{31}−1$。
输入样例:
4 1 1 2 2 3 1 4 3 4
输出样例:
3
解题思路
首先只考虑主要边那么就会构成一棵树,其中对于任意一条附加边如果将其添加到树中那么就会构成一个环。如下图,其中红色的是附加边:
那么如果要砍掉这个环上的一条主要边(即图中绿色的边),为了使得整个图不连通那么就要砍掉这条红色的边(附加边)。
现在再往图中加一条附加边:
对于新的附加边所构成的环,如果砍掉这个环上的一条标记了$1$的主要边,那么只有再砍掉这条新的附加边才能使得整个图不连通。
其中边上的数字$x$表示如果要砍掉这条主要边,那么还需要砍掉$x$条附加边才能使得整个图不连通。因此可以枚举每一条附加边,由该条附加边与主要边所构成的环上的每一条主要边都累加上$1$,表示砍掉这条主要边后要使得整个图不连通还要再砍该条附加边。
在枚举完所有的附加边后,再遍历一遍由主要边构成的树。对于树上的每一条边,假设这条边累加的数字为$x$,
- 如果$x=0$,则表示砍完这条边后不需要再砍任何附加边就可以使得整个图不连通,而由于必须要砍一条附加边,因此可以任选一条附加边砍掉,因此答案累加$m$。
- 如果$x=1$,则表示砍完这条边后还要再砍一条附加边才使得整个图不连通,那么只能砍掉这条附加边,只有一种方案,答案累加$1$。
- 如果$x>1$,则表示砍完这条边后还要再砍$x$条附加边才使得整个图不连通,而由于只能砍一条附加边,因此无解。
因此这题的核心问题就是如何快速的给树上某条路径上的边都加上一个数。可以类比一维差分,可以实现给某个连续区间都加上同一个数。在树上的话就是树上差分的问题。
对于一棵树每个节点都有权值$w_u$,那么节点$u$所对应的差分数组是$d_u = w_u - \sum\limits_{v \in \text{son}(u)}{w_v}$,即节点$u$的权值减去所有儿子的权值。那么如何通过差分数组得到每个节点原始(或者修改后)的权值呢?对于节点$u$,只需求出以$u$为根的整个子树关于差分数组的和。
这个可以用数学归纳法来证明,如果$u$是叶子节点,那么$d_u = w_u$,以$u$为根的整个子树关于差分数组的和就是$d_u$。现在假设$u$不是叶子节点,其子节点为$v \in \text{son}(u)$,根据归纳法以$v$为根的整个子树关于差分数组的和为$w_v$,那么以$u$为根的整个子树关于差分数组的和就是$d_u + \sum\limits_{v \in \text{son}(u)}{w_v} = w_u$,归纳假设成立。
先介绍点差分的概念。
如果要给树中从点$s$到$t$的路径上的每一个点都加上$c$,那么需要先求出$s$与$t$的最近公共祖先$\text{lca}$,同时$\text{lca}$的父节点为$p$,然后$$\begin{cases} d_s \ \ \ \mathrm{+}\mathrm{=} \ c \\ d_{\text{lca}} \ \mathrm{-}\mathrm{=} \ c \\ d_t \ \ \ \mathrm{+}\mathrm{=} \ c \\ d_{p} \ \ \ \mathrm{-}\mathrm{=} \ c \end{cases}$$
这里借用OI Wiki的图:
可以认为公式中的前两条是对蓝色方框内的路径进行操作,后两条是对红色方框内的路径进行操作。并且这样做可以保证最终只有$s$到$t$的路径上的点加上$c$。首先对于节点$p$,根据树上差分的定义,以$p$为根的整个子树关于差分数组的和加上$2c$又减去$2c$,因此不变,同理包括从$p$往上走的节点也是。而不包含$s$与$t$的子树自然也不会受到影响,因此只有路径上的点都被加了一次$c$。
然后是边差分。
我们知道树中除了根节点外每个节点都含有一个父节点,那么将从节点$u$到其父节点的这条边归属到节点$u$,因此树中的$n-1$条边就可以用除根节点的$n-1$个节点编号来表示了。因此$s$到$t$的路径上的每一条边都加上$c$,就可以转换成给对应的点加上$c$,这就变成了上面点差分的问题。
比如下图,$s$到$t$的路径上的边就分别对应标记了$1$的节点,因此对这些边加上$c$就等价于对标记了$1$的节点加上$c$:
$$\begin{cases} d_s \ \ \ \mathrm{+}\mathrm{=} \ c \\ d_t \ \ \ \mathrm{+}\mathrm{=} \ c \\ d_{\text{lca}} \ \mathrm{-}\mathrm{=} \ 2c \end{cases}$$
其中这题用到的是边差分。
AC代码如下,时间复杂度为$O(n + m \log{n})$:
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 const int N = 1e5 + 10, M = N * 2; 5 6 int n, m; 7 int head[N], e[M], ne[M], idx; 8 int fa[N][17], dep[N]; 9 int q[N], hh, tt = -1; 10 int d[N]; 11 12 void add(int v, int w) { 13 e[idx] = w, ne[idx] = head[v], head[v] = idx++; 14 } 15 16 int lca(int a, int b) { 17 if (dep[a] < dep[b]) swap(a, b); 18 for (int i = 16; i >= 0; i--) { 19 if (dep[fa[a][i]] >= dep[b]) a = fa[a][i]; 20 } 21 if (a == b) return a; 22 for (int i = 16; i >= 0; i--) { 23 if (fa[a][i] != fa[b][i]) a = fa[a][i], b = fa[b][i]; 24 } 25 return fa[a][0]; 26 } 27 28 int dfs(int u, int pre) { 29 int ret = 0; 30 for (int i = head[u]; i != -1; i = ne[i]) { 31 if (e[i] != pre) { 32 ret += dfs(e[i], u); 33 d[u] += d[e[i]]; 34 } 35 } 36 if (pre != -1) { 37 if (!d[u]) ret += m; 38 else if (d[u] == 1) ret++; 39 } 40 return ret; 41 } 42 43 int main() { 44 scanf("%d %d", &n, &m); 45 memset(head, -1, sizeof(head)); 46 for (int i = 0; i < n - 1; i++) { 47 int v, w; 48 scanf("%d %d", &v, &w); 49 add(v, w), add(w, v); 50 } 51 memset(dep, 0x3f, sizeof(dep)); 52 dep[0] = 0, dep[1] = 1; 53 q[++tt] = 1; 54 while (hh <= tt) { 55 int t = q[hh++]; 56 for (int i = head[t]; i != -1; i = ne[i]) { 57 if (dep[e[i]] > dep[t] + 1) { 58 dep[e[i]] = dep[t] + 1; 59 q[++tt] = e[i]; 60 fa[e[i]][0] = t; 61 for (int j = 1; j <= 16; j++) { 62 fa[e[i]][j] = fa[fa[e[i]][j - 1]][j - 1]; 63 } 64 } 65 } 66 } 67 for (int i = 0; i < m; i++) { 68 int a, b; 69 scanf("%d %d", &a, &b); 70 d[a]++, d[b]++, d[lca(a, b)] -= 2; 71 } 72 printf("%d", dfs(1, -1)); 73 74 return 0; 75 }
参考资料
AcWing 352. 闇の連鎖(算法提高课):https://www.acwing.com/video/571/
前缀和 & 差分 - OI Wiki:https://oi-wiki.org/basic/prefix-sum/
原文地址:https://www.cnblogs.com/onlyblues/p/17360282.html
- 剑指OFFER之栈的压入、弹出序列(九度OJ1366)
- Python标准库03 路径与文件 (os.path包, glob包)
- AI人工智能时代已经到来 “北斗即时判”实现纯语音交互
- 剑指OFFER之链表中倒数第k个节点(九度OJ1517)
- 用Qt写软件系列四:定制个性化系统托盘菜单
- Linux简介与厂商版本
- 用Qt写软件系列三:一个简单的系统工具之界面美化
- VS编译链接时错误(Error Link2005)的解决方法
- HttpClient使用心得
- 剑指OFFER之重建二叉树(九度OJ1385)
- 记录visual Studio使用过程中的两个问题
- 剑指OFFER之二维数组中的查找(九度OJ1384)
- Python标准库02 时间与日期 (time, datetime包)
- PR&AE插件开发遇到的一个坑
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- iOS技术面试题及答案
- 虽然现在有可以去码的软件了,可视频是如何自动跟踪打码的?
- 2020-09-12:手撕代码:最小公倍数,复杂度多少?
- Mac App推荐
- 美团面试问ThreadLocal,学妹一口气给他说了四种!
- BFE.dev前端刷题#108. 用队列(Queue)实现栈(Stack)
- Kafka消费过程关键源码解析
- leetcode链表之两个链表的第一个公共节点
- 测试开发基础 mvn test | 利用 Maven Surefire Plugin 做测试用例基础执行管理
- 腾讯云Elasticsearch集群规划及性能优化实践
- 【赵渝强老师】在MongoDB中使用MapReduce方式计算聚合
- 2020-09-13:判断一个正整数是a的b次方,a和b是整数,并且大于等于2,如何求解?
- ASP.NET Core 性能优化最佳实践
- 如何在Vue中使用云开发的云函数,实现邮件发送
- 乐观锁与悲观锁