树的直径

定义:树中最远的两个节点之间的距离被称为树的直径。 

怎么求呢?有两种官方的算法(不要问官方指谁我也不晓得):

1.两次搜索。首先任选一个点,从它开始搜索,找到离它最远的节点x。然后从x开始搜索,找到离x最远的点y,那 么E(x, y)的长度就是树的直径。时间复杂度为O(n)。

2.树形dp。这种其实更好写。我们可以对于某个节点x,分别求出经过它的最长链的长度。怎么求呢?首先,枚举x 所连接的k个节点yi(i ∈[1,k]),都求出以yi为根的子树最大深度d[yi]。那么,经过x的最长链长度f[x] = max{d[yi] + E(x, yi)}(i ∈[1, k])。~好理解吧~那么这个算法的复杂度就是O(n)。

实际上,搜索求直径的复杂度为O(2n),是树形dp的两倍。不过影响不大,并且搜索可以节省内存。搜索美滋滋。

//搜索求解
int mmax, pos;
void dfs(int u, int pre, int w){
if(w >= mmax){
mmax = w;
pos = u;
}
int v;
for(int i = head[u]; ~i; i = e[i].next){
v = e[i].to;
if(v != pre) dfs(v, u, w + e[i].dis);
}
}
//main函数中
dfs(, , );
dfs(pos, , );
//dp求解
int d[maxn], ans;
bool vis[maxn];
void dp(int u){
vis[u] = true;
int v;
for(int i = head[u]; ~i; i = e[i].next){
v = e[i].to;
if(!vis[v]){
dp(v);
ans = max(ans, d[u] + d[y] + e[i].dis);
d[u] = max(d[u], d[y] + e[i].dis);
}
}
}
//main函数中
dp();

 我们首先证明一个定理:

给定一棵树,对于它的任一直径,若取其几何意义上的中点,叫做这条直径的中点。那么, 一棵树的所有直径的中点必定是同一点。

证明:

显然,当这棵树只有一条直径时,定理成立。但很多情况下一棵树不止一条直径。但所有直径必定都经过同一点。为 什么呢?(下面证明的过程中有关长度与距离之类的量都为几何意义上的量)

假设有两条直径不经过同一点。设两条直径分别有一点A,B,并且A能够在不经过这两条直径上的边的情况下到达B。 那么这两个节点就将所在的直径分成了两段。我们取每条直径上较长的那段,加上A和B之间的长度,显然大于原来 的直径长度。

那么任意两条直径必定会相交于一点。现在再假设有两条直径满足它们的中点A,B不是同一点。显然对于中点来说, 它将其所在直径分成了相同长度的两段。不妨设有一条不经过直径的路径连接A和另一条直径上的C点,那么:A所在 直径长度的1/2 + E(A,C) + E(B,C) + B所在直径长度的1/2 ≥ 任意一条直径的长度 = A所在直径长度的1/2 + B所在直径 长度的1/2,这样就存在一条新的路径,其长度大于原来的直径,这有悖直径定义。

证毕。

(Φ皿Φ)证出来了~

回到直径这道题,我们可以先随便求一条直径出来,然后设法搞出它的中点(直径长度/2的位置)。现在有两种情况:

1.(软柿子)中点就是树的某个节点。我们可以直接以这个点为根进行计算。

2.中点在树的某条边上。酱紫的话就把这条边扔了,在剩下的两棵子树上计算。

怎么计算呢?

第一种情况,由于从根节点可以连接出许多棵子树。我们取出深度最大的三棵子树a,b,c,并用d[x]表示x子树的最大 深度。

1.d[a] > d[b] > d[c]。显然直径会经过a子树和b子树。然后我们递归,在a子树和b子树的根上进行相同计算,算出直 径必然经过的边。

2.d[a] > d[b] = d[c]。那么只需在a子树的根上进行相同计算即可。

3.d[a] = d[b] = d[c]。那么没有被所有直径经过的边。

如法炮制,对于第二种情况,我们只需在去掉边之后的两颗子树上分别进行上述操作即可。

总结与拓展:

对于树的直径问题,我们会有两种做法:搜索与dp,两者复杂度相似。在看过例题“直径”之后我们可以证明出了一条 定理:一棵树的所有直径必定以同一点为中点。实际上,在做树上问题,特别是树的直径时,证明是少不了的。当然看上去正确的定理水一水也就过了。然而,树的直径通常不会单独在一道题里出现,它经常伴随着其它的算法, 如“直径”中最后计算时整的什么鬼算法,还有经常会和树的直径一起用的二分答案。多练吧~

基环树

顾名思义,基环树就是基于环的树。

对于一棵树,若它有n个点,则一定有n-1条边。而如果在其之上添加一条边,那么就会形成一个环(好理解吧~溜 ~)。加了环之后,这个什么鬼就叫基环树。

基环树的题目并不多,但是对于大部分的题,我们可以很容易判断出它的图是否为基环树。通常会有两种特征:

1.点数为n,边数也为n

2.且每条边都至少连出去一条边(出度≥0)

举个例子:

eg.1

很明显的基环树

eg.2

简直在告诉我们它是基环树

通过这两道考试的原题你可以看出,基环树是多么重要的一个乱七八糟的结构

那么,对于基环树的问题,我们怎么解决呢?(这里直接讲刚才的例题因为没有官方算法)

第一题就先不看了

我们看岛屿这题:

这题显然是有不只一棵基环树,因此我们可以对每棵树都求一下可以走过的最大长度。

那么,这就有点类似于求树的直径了。那么我们引入一个新的概念(不是官方的):基环树的直径——基环树上最长 的简单路径(不自交的路径)。其实也是最远两点之间的距离(这样会比较高端)。

如何求呢?对于一棵基环树,它的直径会有两种情况:

1.去掉环上所有边之后的某棵子树的直径。

2.环上分别以两点为根的两棵子树的直径加上它们环上的距离,这个环上距离可以是逆时针的,也可以是顺时针的。

所以,想要AC岛屿这道题,我们只需要求出每棵基环树的直径,再累加起来就是答案了。

怎么求呢?——为了逃避第二种情况我们先求解第一种情况。

显然,我们可以首先dfs一次图,找出图中的环。然后枚举环上每个点,分别求出以这个点为根的子树的直径d[i]。那 么答案就累加上直径。

第二种情况。我们枚举环上每个点,分别求出以之为根的子树的最大深度d[i]。那么答案就累加上max{d[i] + d[j] + dist(i, j)},其中i,j∈环,dist(i, j)表示i和j的环上距离(顺时针和逆时针的较大者)。怎么算dist呢?可以用一个熟悉的 技巧——拆环(合并石子里面的),即把环从一个点断开并拉成一条链,再把这条链复制一份塞到后面去。然后枚举这 条链上的点,算出其前缀和sum[x],那么dist(i, j) = max(sum[i] - sum[j], sum[j + lenth] - sum[i])。其中lenth是 环的长度。但不用这么算,因为枚举得到点j+lenth。所以,ans += {d[i] + d[j] + sum[i] - sum[j]}。那么这个算法 就是O(n²)。然而数据范围是10^6,这种算法显然过不了。那就不做了。不过优化是有的。怎么优化呢?单调队列牛 批!

所以我们先学一下单调队列

单调队列

何为单调队列?答:单调的队列。(啪!)

单调队列是用来找区间最值用的又一个乱七八糟的数据结构。可以STL里面的双端队列deque来实现,但那种方式的 开销不如手写的少。怎么找最值呢?我们来看看单调队列通常用来解决的问题:

给定一个数列a[n],求出区间[i - k, i]中的最大值,其中i∈[1, n], k为常数。

我们首先开一个数组,并用两个int变量作为它的首尾l和r。我们枚举数列的每个下标。当枚举到i下标时,我们将a[i] 入队,并判断一下a[i]是否大于之前队列的队尾值。若是大于,那就有悖单调队列的定义(单调的队列),那就不停将 队尾出队,直到q[r] >= a[i]为止。这样就维护了队列的单调性。再看,根据题意,我们只需要计算长度为k + 1的区 间。所以,我们再判断一下:若l < r - k,那么将l加到r - k为止。那么,一个单调队列就维护好了。

给出代码:

#include <iostream>
#define maxn 1000000 + 5
using namespace std; int a[maxn], n, k;
int list[maxn], l, r; int main(){
scanf("%d%d", &n, &k);
for(int i = ; i <= n; i++){
scanf("%d", &a[i]);
}
l = r = ;
for(int i = ; i <= n; i++){
while(l <= r && list[r] < a[i]) r--;
list[++r] = a[i];
while(l <= r && l < r - k) l++;
printf("%d\n", list[l]);
}
return ;
}
Sample in:

最小值的求法也是类似的~

封坟————————————

挖~

然后我们回到岛屿这题。看到之前得出的表达式:ans += {d[i] + d[j] + sum[i] - sum[j]}。其中,我们只需枚举i∈[1, n],然后对于已经枚举过的j∈[1, i - 1],我们可以用单调队列来维护这段区间的max(d[j] - sum[j])。然后,因为我 们复制了环,所以当i > lenth时,我们只需在[i - lenth + 1, i - 1]里面找最大值即可。这些就巧妙地对应了刚才单调 队列里面的所有操作,因此整个算法的复杂度为O(n)。快不快?

总结:

对于基环树的问题,我们一般的做法是先处理环上部分,再处理以环上节点为根的每棵子树。但这也不是普遍适用 的,比如:

就是这货,它的解法是从一点开始贪心,找到环时再特判一下去最优值,根本不是上面所的一般情况,所以这里不做 过多讨论。其实是没AC

End

与图论的邂逅01:树的直径&基环树&单调队列的更多相关文章

  1. 【bzoj1999】[Noip2007]Core树网的核 树的直径+双指针法+单调队列

    题目描述 给出一棵树,定义一个点到一条路径的距离为这个点到这条路径上所有点的距离的最小值.求一条长度不超过s的路径,使得所有点到这条路径的距离的最大值最小. 输入 包含n行: 第1行,两个正整数n和s ...

  2. tyvj:1520 树的直径 spfa/树的直径

    tyvj:1520 树的直径 Time Limit: 1 Sec  Memory Limit: 131072KiBSubmit: 9619  Solved: 3287 题目连接 http://www. ...

  3. 【bzoj3362/3363/3364/3365】[Usaco2004 Feb]树上问题杂烩 并查集/树的直径/LCA/树的点分治

    题目描述 农夫约翰有N(2≤N≤40000)个农场,标号1到N,M(2≤M≤40000)条的不同的垂直或水平的道路连结着农场,道路的长度不超过1000.这些农场的分布就像下面的地图一样, 图中农场用F ...

  4. 树形DP 学习笔记(树形DP、树的直径、树的重心)

    前言:寒假讲过树形DP,这次再复习一下. -------------- 基本的树形DP 实现形式 树形DP的主要实现形式是$dfs$.这是因为树的特殊结构决定的——只有确定了儿子,才能决定父亲.划分阶 ...

  5. P4949 最短距离(树链剖分+树状数组+基环树)

    传送门 一个中午啊-- 本来打算用仙人掌搞的,后来发现直接基环树就可以了,把多出来的那条边单独记录为\((dx,dy,dw)\),剩下的树剖 然后最短路径要么直接树上跑,要么经过多出来的边,分别讨论就 ...

  6. HDU - 6393 Traffic Network in Numazu(树链剖分+基环树)

    http://acm.hdu.edu.cn/showproblem.php?pid=6393 题意 给n个点和n条边的图,有两种操作,一种修改边权,另一种查询u到v的最短路. 分析 n个点和n条边,实 ...

  7. BZOJ3124 [Sdoi2013]直径 【树的直径】

    题目 小Q最近学习了一些图论知识.根据课本,有如下定义.树:无回路且连通的无向图,每条边都有正整数的权值来表示其长度.如果一棵树有N个节点,可以证明其有且仅有N-1 条边. 路径:一棵树上,任意两个节 ...

  8. HDU 4123(树的直径+单调队列)

    Bob’s Race Time Limit: 5000/2000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total ...

  9. 『Two 树的直径求解及其运用』

    树的直径 我们先来认识一下树的直径. 树是连通无环图,树上任意两点之间的路径是唯一的.定义树上任意两点\(u, v\)的距离为\(u\)到\(v\)路径上边权的和.树的直径\(MN\)为树上最长路径, ...

随机推荐

  1. Android Studio报错Error:Failed to open zip file. Gradle's dependency cache may be corrupt

    Android Studio导入项目后,Gradle编译失败,报错如下. Error:Failed to open zip file. Gradle's dependency cache may be ...

  2. go语言字符串的连接和截取

    字符串的连接: https://studygolang.com/articles/12281?fr=sidebar 字符串的截取: https://studygolang.com/articles/9 ...

  3. 联想项目结束了,聊聊华为SAP HANA项目八卦

    联想项目结束了,聊聊华为SAP HANA项目八卦 [转] 本文目录 [隐藏] 1.故事线 2.华为的文化我们不懂 3.分分钟的文化冲突 4. 项目到底要做什么(待更新) 5.项目咋样了(待更新) 1. ...

  4. 2.静态AOP实现-装饰器模式

    通过装饰器模式实现在RegUser()方法本身业务前后加上一些自己的功能,如:BeforeProceed和AfterProceed,即不修改UserProcessor类又能增加新功能 定义1个用户接口 ...

  5. apache 压力测试ab

    1.安装了apache服务器 2.进入命令行 3.模拟并发级别为100,请求数为1000个的api数据请求数量测试

  6. dos 打开计算机管理

    一. 首先打开[运行]程序:二. 运行中输入‘CMD’:三. 然后在上面输入‘compmgmt.msc’,就可以打开“计算机管理”命令了.

  7. gym 101858

    我这个傻逼被治了一下午. 大好的橘势,两个小时6T,去看L,哇傻逼题.然后我跑的最短路T到自闭 最后十几分钟去想了下A,一直在想如何表示状态..就是想不到二进制搞一下... 然后游戏结束了..以后我就 ...

  8. 项目中的java文件没有在WEB-INF\classes中生成class文件

    https://blog.csdn.net/u011008029/article/details/49303723 病因: 我在eclipse 上面  编的web  项目 并没有错 但是 一直出现 5 ...

  9. rails 杂记 - erb 中的 form_helper

    原文 1. form_tag 1) 基础 Form <%= form_tag do %> Form contents <% end %> 生成 html <form ac ...

  10. 寻求js

    寻找登录的post地址 在form表单中寻找action对应的url地址 post的数据是input标签中的name值作为键,真正的用户名密码作为值得字典,post的url地址就是action对应的u ...