DFS的树上应用

目录

一、前言

二、树上的DFS

1、树的重心

2、树的重心例题

3、树的直径

4、树的直径例题

(1)做两次DFS

三、拓扑排序与DFS

1、用DFS解拓扑排序

2、欧拉路与DFS

3、用DFS输出一个欧拉回路


一、前言

本文主要讲了树上的DFS、树的重心、树的直径、拓扑排序与DFS、欧拉路等理论内容。

二、树上的DFS

1、树的重心

  • 树的重心 u:以树上任意一个结点为根计算它的子树的结点数,如果结点 u 的最大的子树的结点数最少,那么 u 就是树的重心。
  • 删除点 u 后得到两棵或更多棵互不连通的子树,其中最大子树的结点数最小。u 是树上最平衡的点。

  • 如何计算以结点 i 为根的子树的结点数量?
  • 对 i 做 DFS:从 i 出发,递归到最底层后返回,每返回一个结点,结点数加 1,直到所有结点都返回,就得到了子树上结点总数。
  • 因为每个结点只返回 1 次,所以这个方法是对的。

那么如何寻找树的重心呢?

暴力法:

删除树上的一个结点 u,得到几个孤立的连通块,可以对每个连通块做一次 DFS,分别计算结点数量。对整棵树逐一删除每个结点,重复上述计算过程,就得到了每个结点的最大连通块。

优化:只需要一次 DFS,就能得到每个结点的最大连通块。

删除 u 得到三个连通块:

(1)包含 1 的连通块; (2)包含 2 的连通块; (3)包含 3 的连通块。

这三个连通块的数量如何计算?

从任意一个点开始 DFS,假设从 1 开始,1 是 u 的父结点。DFS 到结点 u 后,从 u 开始继续 DFS,得到它的子树 2 和 3 的结点数量 (2) 和 (3) ,设 u 为根的子树的结点数量是 d[u],则 d[u] = (2) + (3) + 1。那么 (1) 的数量等于 n-d[u],n是结点总数。记录 (1)、(2)、(3) 的最大值,就得到了 u 的最大连通块。

这样通过一次 DFS,每个结点的最大连通块都得到了计算,总复杂度 O(n)。 

2、树的重心例题

【问题描述】

城里有一个黑手党组织。把黑手党的人员关系用一棵树来描述,教父是树的根,每个结点是一个黑手党徒。为了保密,每人只和他的父结点和他的子结点联系。警察知道哪些人互相来往,但是不知他们的关系。警察想找出谁是教父。

警察假设教父是一个聪明人:教父懂得制衡手下的权力,所以他直属的几个小头目,每个小头目属下的人数差不多。也就是说,删除根之后,剩下的几个互不连通的子树(连通块),其中最大的连通块应该尽可能小。帮助警察找到哪些人可能是教父。

【输入】

第一行是 n,表示黑手党的人数,2 <= n <= 50000。黑手党徒的编号是 1 到 n。下面有 n-1 行,每行有 2 个整数,即有联系的 2 个人的编号。

【输出】

输出疑似教父的结点编号,从小到大输出。

【输入样例】

6

1 2

2 3

2 4

4 5

3 6

【输出样例】

2 3

import sys
sys.setrecursionlimit(300000)    #设置递归深度,否则不能通过100%的测试

def dfs(u,fa):
    global num,maxnum   #num:教父的数量
    d[u]=1              #递归到最底层时,结点数加1
    tmp=0
    for v in edges[u]:  #遍历u的子节点
        if v==fa:
            continue
        dfs(v,u)        #递归子节点,计算v这个子树的结点数量
        d[u]+=d[v]      #计算以u为根的节点数量
        tmp=max(tmp,d[v])   #记录u的最大子树的节点数量
    tmp=max(tmp,n-d[u]) #tmp=u 的最大连通块的结点数
        #以上计算出了u的最大连通块
        #下面统计疑似教父。如果一个结点的最大连通块比其他结点的都小,它是疑似教父
    if tmp<maxnum:      #一个疑似教父
        maxnum=tmp      #更新"最小的"最大连通块
        num=1
        ans[1]=u        #把教父记录在第1个位置
    elif tmp==maxnum:
        num+=1
        ans[num]=u      #疑似教父有多个,记录在后面

maxnum=int(1e9)
n=int(input())
d=[0]*(n+1)     #以u为根的子树的结点数量
ans=[0]*(n+1)   #记录教父
num=0           #教父的数量
edges=[[] for i in range(n+1)]      #邻接表
for i in range(n-1):
    a,b=map(int,input().split())
    edges[a].append(b)
    edges[b].append(a)
dfs(1,0)
s=sorted(ans[1:num+1])      #对教父排序
for i in range(num):
    print(s[i],end=' ')     #按顺序打印所有教父

3、树的直径

树的直径是指树上最远的两点间的距离,又称为树的最远点对。

有两种方法求树的直径:

(1)做两次DFS(或BFS);

(2)树形DP。

复杂度都是 O(n)。

-----------------------------------------------------------------------------------------------

优点和缺点:

(1)做两次DFS(或BFS)

优点:能得到完整的路径。它用搜索的原理,从起点 u 出发一步一步求 u 到其他所有点的距离,能记录路径经过了哪些点。

缺点:不能用于有负权边的树。

(2)树形DP

优点:允许树上有负权边。

缺点:只能求直径的长度,无法得到这条直径的完整路径。

4、树的直径例题

【问题描述】

求树的直径。

【输入描述】

第一行是整数 n,表示树的 n 个点。点的编号从 1 开始。后面 n-1 行,每行 3 个整数 a、b、w,表示点 a、b 之间有一条边,边长为 w。

【输出描述】

一个整数,表示树的直径。

【输入样例】

5

1 2 5

1 3 19

1 4 23

4 5 12

【输出样例】

54

(1)做两次DFS

当边权没有负值时,计算树的直径可以通过做两次 DFS 解决,步骤是:

(1)从树上的任意一个点 r 出发,用 DFS 求距离它最远的点 s。 s 肯定是直径的两个端点之一。(2)从 s 出发,用 DFS 求距离 s 最远的点 t 。t 是直径的另一个端点。

s、t 就是距离最远的两个点,即树的直径的两个端点。

证明:

  • 把这棵树所有的边想象成不同长度的柔性绳子,并假设已经找到了直径的两个端点 s 和 t。
  • 双手抓住 s 和 t,然后水平拉直成一条长线,这是这棵树能拉出来的最长线。
  • 这时,其他的绳子和点会下垂。
  • 任选一个除 s 和 t 以外的点 r,它到 s(或t)的距离肯定是最远的。
  • 如果不是最远的,那么下垂的某个点就能替代s,这跟假设 s 是直径的端点矛盾。

但是这个方法不能用于有负权边的树。例:

  • 第一次 DFS,若从点1出发,得到的最远端点 s 为点 2;
  • 第二次 DFS 从点 2 出发,得 t 为点 4。
  • 但是,实际上这棵树的直径的两个端点应该是 3、4。

总结:以贪心原理进行路径长度搜索的 DFS,当树上有负权边时,只能在局部获得最优,而无法在全局获得最优。

import sys
sys.setrecursionlimit(300000)

def dfs(u,fa,d):        #用dfs计算从u到每个子结点的距离
    dist[u]=d
    for v,w in edges[u]:
        if v!=fa:       #很关键,不回头搜父结点
            dfs(v,u,d+w)

n=int(input())
dist=[0]*(n+1)  #记录距离
edges=[[] for i in range(n+1)]      #邻接表
for i in range(n-1):
    a,b,w=map(int,input().split())
    edges[a].append((b,w))
    edges[b].append((a,w))
dfs(1,-1,0)
s=1
for i in range(1,n+1):      #找最远的结点s,s是直径的一个端点
    if dist[i]>dist[s]:
        s=i
dfs(s,-1,0)         #从s出发,计算以s为起点,到树上每个结点的距离
t=1
for i in range(1,n+1):      #找直径的另一个端点t
    if dist[i]>dist[t]:
        t=i
print(dist[t])      #打印树的直径的长度

三、拓扑排序与DFS

设有 a、b、c、d 等事情,其中 a 有最高优先级,b、c 优先级相同,d 是最低优先级,表示为a → (b,c) → d,那么 abcd 或者 acbd 都是可行的排序。

把事情看成图的点,先后关系看成有向边,问题转化为在图中求一个有先后关系的排序,这就是拓扑排序。

  • 出度:以点 u 为起点的边的数量,称为 u 的出度。
  • 入度:以点 v 为终点的边的数量,称为 v 的入度。
  • 一个点的入度和出度,体现了这个点的先后关系。如果一个点的入度等于 0,说明它是起点,是排在最前面的;如果它的出度等于0,说明它是排在最后面的。
  • 图中,点 a、c 的入度为 0,它们都是优先级最高的事情;d 的出度为 0,它的优先级最低。

1、用DFS解拓扑排序

  • DFS 天然适合拓扑排序。
  • DFS 深度搜索的原理,是沿着一条路径一直搜索到最底层,然后逐层回退。
  • 这个过程正好体现了点和点的先后关系,天然符合拓扑排序的原理。
  • 如果只有一个点 u 是 0 入度的,那么从 u 开始 DFS, DFS 递归返回的顺序就是拓扑排序(是一个逆序)。
  • DFS 递归返回的首先是最底层的点,它一定是 0 出度点,没有后续点,是拓扑排序的最后一个点;然后逐步回退,最后输出的是起点 u;输出的顺序是一个逆序。
  • 从 a 开始,递归返回的顺序见点旁边的划线数字:cdba,是拓扑排序的逆序。

  • 如果有多个入度为 0 的点?
  • 想象有一个虚拟的点 v,它单向连接到所有其他点。这个点就是图中唯一的 0 入度点,图中所有其他的点都是它的下一层递归;而且它不会把原图变成环路。从这个虚拟点开始 DFS,就完成了拓扑排序。
  • 图 (1) 有 2 个 0 入度点 a 和 f
  • 图 (2) 想象有个虚拟点 v,递归返回的顺序见点旁边划线数字,返回的是拓扑排序的逆序。

2、欧拉路与DFS

欧拉路:从图中某个点出发,遍历整个图,图中每条边通过且只通过一次。

欧拉回路:起点和终点相同的欧拉路。

欧拉路问题:是否存在欧拉路、打印出欧拉路。

3、用DFS输出一个欧拉回路

对一个无向连通图做 DFS,就输出了一个欧拉回路。

从图 (1) 中 a 点开始 DFS,DFS 的对象是边。图 (2) 边上的数字,是 DFS 访问的顺序。

以上,DFS的树上应用

祝好

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
DFS搜索是一种使用深度优先遍历算法实现的搜索方法,用于在树或图中查找特定节点或解决问题。在Python中,可以使用递归或栈数据结构来实现DFS搜索算法DFS搜索算法的基本思想是从起始节点开始,沿着树的深度优先遍历节点,并尽可能深入搜索树的分支。当搜索到达一个节点时,它会沿着未探索过的边继续向下搜索,直到无法继续深入为止。然后,搜索会回溯到上一个未探索的节点,并开始搜索其他分支。这个过程会一直进行下去,直到所有节点都被访问为止。 在Python中实现DFS搜索算法,可以使用递归方法或者显式地使用栈数据结构来模拟递归过程。递归方法通过递归函数来实现深度优先遍历,每次递归调用都会遍历当前节点的子节点。而显式地使用栈数据结构,则会将每个节点及其相关信息压入栈中,以便在回溯时能够继续搜索其他分支。 例如,可以使用以下Python代码实现DFS搜索算法: ```python def dfs(graph, start): visited = set() # 用于存储已访问过的节点 stack = [start # 使用栈来模拟递归过程 while stack: node = stack.pop() # 弹出栈顶节点 if node not in visited: visited.add(node) # 将节点标记为已访问 neighbors = graph[node # 获取当前节点的邻居节点 for neighbor in neighbors: stack.append(neighbor) # 将邻居节点压入栈中 return visited ``` 这段代码实现了一个简单的DFS搜索算法。其中,graph表示图的邻接关系,start表示起始节点。算法使用一个集合visited来记录已访问过的节点,使用一个栈stack来模拟递归过程。在每次循环中,算法从栈中弹出一个节点,检查它是否已访问过。如果没有访问过,则将其标记为已访问,并将其邻居节点压入栈中。算法重复这个过程,直到栈为空。 可以根据具体的应用场景和需求,对DFS搜索算法进行相应的改进和扩展。比如,可以添加目标节点的判断条件,以找到特定的节点或解决问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吕飞雨的头发不能秃

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值