免责声明

本文仅为个人学习笔记,请谨慎参考,如有错误欢迎批评指正。

参考文章

第一篇文章主要看树的重心的部分

第二篇文章才是和本题完全一致

https://blog.csdn.net/a_forever_dream/article/details/81778649

https://blog.csdn.net/jackypigpig/article/details/69808594

要求:

(1)用伪代码描述求树重心的算法。

(2)将下面的树作为输入时,写出求解上述问题的求解过程以及求解结果。要求写出求解过程中主要变量的变化过程。

(3)编写程序求解该问题,并分析算法的时间复杂度。

分析

整个流程:

1、找树的重心。

2、算出每个点到重心的距离数组。

3、所有经过重心的点对数 减去 所有经过重心子结点的点对数 得到 合法点对数

4、对重心的子结点重复1、2、3的操作(递归)

1、找树的重心。

树的重心,也叫树的质心。即树的一个结点,把它删掉后的所有子树的最大结点数(相比于删掉其他结点)最小。如图所示:

要求树的重心,也许第一想法是遍历一次所有结点,把每个结点都当成重心,计算每棵子树的结点数,再来进行比较。但这种暴力法是不可取的。它会重复计算很多次相同的路径,我们可以仅扫描一次整棵树就能得到以每个点作为重心的最大子树结点数。这里要用到树的点分治法。分治法,我把它理解成后序遍历的递归调用,但是递归也只能以一个结点为起点进行递归呀,怎么能算出以所有点为起点的最大子树结点数呢?这里我们还是以上面的树为例子。用n表示整棵树的结点数,这里n=5;用size[i]代表以i为根的树的结点数;用max_child[i]代表以i为根的最大子树结点数;用min来更新max_child[i]中的最小值,它用来更新重心,min起始值是很大的数。

按照后续遍历的递归,我们首先能算出点4的size[4]=1,红色部分是n-size[4]=4个结点。之所以能这么分,是因为点4只会有一个父结点,所以红色部分始终能作为它的一棵子树。比较size[4]和n-size[4]的大小,以点4为根的最大子树结点数max_child[4]=4,min更新为max_child[4]的4。

然后是算出点5的size[5]=1,也是类似点4。红色部分是n-size[5]=4个结点,以点5为根的最大子树结点数max_child[5]=4,min不变。

接着是算出点2,也就是它自身的1个结点,加上它的子树点4和点5的size。绿色部分为它的子树,分别为size[4]=1个结点、size[5]=1个结点,红色部分是n-size[2]=2个结点,所以以点2为根的最大子树结点数max_child[2]=2,min更新为2。

接着是算出点3,也就是它自身的1个结点。红色部分是n-size[3]=4个结点,所以以点3为根的最大子树结点数max_child[3]=4,min不变。

接着是算出点1,也就是它自身的1个结点,加上它的子树点2和点3的size。绿色部分为两棵子树,size[2]=3个结点、size[3]=1个结点。红色部分已经没有了,n-size[1]=0个结点。所以以点1为根的最大子树结点数max_child[1]=3,min不变。

min一旦更新,重心gravity也会更新为对应的结点。只是这里没写出来,看代码就知道了。

所以说,这个递归的开始点选谁不重要,选择任意点都能算出以每个点为根的最大子树结点数,进而求出重心。

求树的重心的代码如下:

// 全局变量
int n=17; // 所有结点数
int size[n];// 以n为根的树的结点数
int max_child[n];// 以n为根的树的最大子树的结点数
int min;// max_child中最小的那个
int first[n+1],edge[(n-1)*2]// 顶点表,边表
int gravity=0;// 被选为重心的结点// 传入参数
// start 代表当前结点
// parent 代表当前结点的父结点,这里是为了防止遍历start的子结点的时候把父结点也遍历进去了
void getGravity(int start, int parent)
{// size[start]代表当前结点的个数,初始为1是算上本身size[start]=1;// max_child[start]代表当前结点的最大子树结点数max_child[start]=0;// 遍历以start结点为起点的所有边(除去连接父结点的边)for(int i=first[start]; i; i=edge[i].next){// end为当前边的终点(也是start点的子结点)int end=edge[i].end;// 如果这个终点已经被遍历或者这个终点是父结点,就跳过if(visited[end] || end==parent){continue;}// 继续遍历终点的子结点,这里其实就是遍历start的一棵子树的所有结点数getGravity(end, start);// 遍历完这棵子树的所有结点后,把子树的结点数加起来size[start]+=size[end];// 如果这颗子树的结点数大于max_child,就更新它if(size[end] > max_child[start]){max_child[start]=size[end];}}// 上面的循环是用来遍历start的每一棵子树并比较出最大子树结点// 接下来就是算“红色部分”,也就是n-size[i]部分的结点数,并比较出最终的最大子树结点if(n-size[start] > max_child[start]){max_child[start]=n-size[start];}// 从max_child中比较出最小的,以找出重心if(min < max_child[start]){min=max_child[x];gravity=start;}
}

这里对树的存储方法是,用顶点表first来存每个点,first[i]代表编号为i的结点的对应的第一条边的编号。边表edge的每一行代表一条边,包含这条边的起点、终点、权重和下一条相同起点的边。因为这些边是无向的,所以一条边在边表里存了两次。

添加树结点和边的代码如下:

int first[n+1],edge[(n-1)*2]// 顶点表,边表
int num=0;
void addNodeAndEdge(int start,int end,int weight)
{num++;// 编号,没错,要从1开始edge[num].start=start;// 起点edge[num].end=end;// 终点edge[num].weight=weight;// 权重edge[num].next=first[start];// 下一条相同起点的边first[start]=num;// 加入顶点
}

2、算出每个点到重心的距离数组。

在选出树的重心之后,我们计算所有点到这个重心的距离(即权重),也是用递归的办法。这个应该很好理解,不做过多解释。

int t=0;
// start是传入的点,parent是start的父结点,weight是start和parent连线的权重
// parent在这里是为了防止遍历start的子结点的时候把父结点也遍历进去了
// 因为每个点到start的距离是自上而下地累加,所以传入weight
// 该递归函数的主要作用是返回每个点到重心的距离,所以一开始调用递归函数的时候
// start默认是重心,parent和weight默认是0。
void getDistance(int start, int parent, int weight)
{// dis数组保存了每个点到重心的距离(权重),为什么用t来做下标而不是点的编号呢// 因为后面的做法只用数点对的个数,不在乎是谁到谁// t是从1开始的dis[++t]=weight;for(int i=first[start]; i; i=edge[i].next){int end=edge[i].end;// 如果这个终点已经被遍历或者这个终点是父结点,就跳过// 这点很重要,因为如果传入的start不是根结点而是根结点的子树的时候// 它就不会把根结点再遍历一次if(visited[end] || end==parent){continue;}getDistance(end, start, weight+edge[i].weight);}
}

这一步我们拿到了dis数组,每个点到重心的距离都保存在dis里面了。再次强调,dis的下标和结点编号没关系。还是以前面的例子,如下图所示:

3、所有经过重心的点对数 减去 所有经过重心子结点的点对数 得到 合法点对数

接下来要考虑求长度小于K的路径数。我的第一想法是把dis中小于等于K的点找到,这样我们就选出点到重心距离小于K的点对数,然后再算经过重心且距离小于K的点对数。但是根据第二篇参考文章,并不是这样做的。引用部分第二篇参考文章:

(得到dis数组)之后,这棵子树中相连的路径会经过重心且对答案有贡献(就是距离小于k)的点对(i,j) (i<j)就会是这样: dis[i]+dis[j] <= K 且在去除重心后,i 与 j 不在同一个联通块里

不过显然要满足“不在同一个联通块里”这个条件有点突兀,于是就有了一个小技巧:先不管在不在一个联通块这个条件,算出当前这棵树的符合路径数,之后再将得出的个数减去 以重心的儿子节点为根的子树内 的 点对 路径距离(经过重心)小于等于K的个数,就行了。

它的意思就是我们现在有了每个点到重心的距离之后,我们把dis按照从小到大排序(排序是为了好算出比K小的点对),进行两两相加,把除了重心外的点全部组合,比如:

dis[2]+dis[5]对应2——1——3;

dis[3]+dis[5]对应4——2——1——3;

但是有一种情况是不妙的,比如dis[3]+dis[4]对应4——2——1——2——5。

点4和点5他们之间本来是不用经过重心点1,这怎么排除掉呢?首先找出这种点对的特点。很容易发现,这种点对它们都在一个子树中。4和5都会通过2与重心连接。也就是说,一个点对如果要经过重心的孩子结点,那么它们就是不合法的。有了这个判断条件,我们就可以进行排除。

明白排除的方法之后,现在来讲work函数。它的传入参数是结点start和start到父结点的距离(权重)。

它是用来计算经过start点的所有组合(合法和不合法的都算进去)。在第一次dfs函数中,我们有了重心。work函数,会被重心调用一次,传入的start是重心,weight为0,算出所有经过重心的点对。回到dfs中,遍历重心的所有孩子结点,分别调用一次work,传入的start是孩子结点,weight为孩子结点到重心的权重,算出所有经过当前孩子结点的点对,虽然是经过孩子结点的点对,但是算的距离还是到重心的。因为不管是经过重心的点对,还是经过孩子结点的点对,它们都要和k进行比较,所以它们算的距离都应该是到重心的。

// 传入的start要么是重心,weight=0
// 要么是重心的孩子结点,weight是重心与孩子结点的距离(权重)
int work(int start,int weight) {t=0;// 如果start是重心,算出 以重心为根的树中的结点 到start的距离,然后两两组合,选出加起来小于k的点对// 如果start是重心的孩子结点,算出 以该孩子结点为根 的子树中的结点 到重心的距离,然后两两组合,选出加起来小于k的点对// eg:重心是S的孩子结点是a,那么算出的点对数是 以a为根的树 的所有结点 两两组合,但是距离算的是 所有结点 到S的距离,因为仍然要判断小于k,和经过重心的点对要一致// getDistance需要传入weight就是为了 重心的孩子结点的 孩子结点的 dis是到重心的距离getDistance(start, 0, weight);// 得到dis数组后,对其进行从小到大排序// 注意,这里的t已经不是0了,它是全局变量,在getDistance里面遍历了start出发的所有结点sort(dis+1,dis+1+t);// pair_num表示经过重心的点对数量int pair_num=0;int i=1,j=t;// 这个while循环就是把两个dis相加的和小于等于K的点对数量计算出来while (i<j){while (i<j && dis[i]+dis[j]>K) j--;pair_num+=j-i;i++;}return pair_num;
}

4、对重心的子结点重复1、2、3的操作(递归)

第一次dfs函数往往随意传入一个结点,找出整棵树的重心,比如就是上图的点1。在这里面,它会用点1调用work函数,求出经过点1的所有点对数ans,包括合法和不合法的。接着在for循环中遍历点1的孩子结点,就是上图的点2和点3,则用点2,点3分别再调用一次work,分别求出经过点2和点3的所有点对数,再用ans减去,就得到合法点对。

也许你会觉得,本来4和5是满足小于k的点对,这里减去了不就没有了?所以减去之后,又要将点2和点3分别传入dfs进行递归,里面会把点4和点5的对给算上。ans是全局变量,会在第一次dfs之后继续进行累加和累减。

// 起始点是start,递归调用会dfs所有的结点
void dfs(int start){// 以start为起点找到重心// 注意,虽然一开始我们说了从树的任何一个点开始遍历都能找出一个确定的重心,// 但是这里从start开始,如果它有父结点,就不要再遍历的,只找以它为根的树的重心getGravity(start,0);// 用这个重心算出所有跨过该重心的路径数ans += work(gravity,0);// 标记这个重心已访问visited[gravity]=1;// 从重心开始访问子结点for(int i=first[start]; i; i=edge[i].next){int end=edge[i].end;if (visited[end]){continue;}// 减去以子结点为根的树的所有跨过子结点的路径数ans -= work(end, edge[i].weight);// 从子结点开始继续递归dfs(end);}return;
}

然后放上main函数:

int main()
{// 输入结点个数scanf("%d",&n);// 输入每个结点的信息for(int i=1;i<n;i++){int start, end, weight;scanf("%d %d %d",&start,&end,&weight);addNodeAndEdge(start,end,weight);addNodeAndEdge(end,start,weight);}dfs(1);printf("%d\n", ans);return 0;
}

最后是整体代码,没有进行测试过,主要是没时间了,但是也完全理解代码了,足够应试。

#include <stdio.h>
#define MAX 10000;// 全局变量
int n; // 所有结点数
int size[n],max_child[n],min=MAX;// 以n为根的树的结点数,以n为根的树的最大子树的结点数
int first[n+1],edge[(n-1)*2]// 顶点表,边表
int visited[n+1];// 标记已访问过的点
int dis[];// 每个点到重心的距离
int gravity=0;// 重心
int num=0,t=0;// 结点编号
int ans=0;// 最终结果:小于等于K的点对数量void addNodeAndEdge(int start, int end, int weight)
{num++;// 编号edge[num].start=start;// 起点edge[num].end=end;// 终点edge[num].weight=weight;// 权重edge[num].next=first[start];// 下一条相同起点的边first[start]=num;// 加入顶点
}// 传入重心,以及重心和它的父结点之间的权重
int work(int start,int weight) {t=0;// 算出start到各个结点的距离getDistance(start, 0, weight);// 得到dis数组后,对其进行从小到大排序// 注意,这里的t已经不是0了,它是全局变量,在getDistance里面遍历了start出发的所有结点sort(dis+1,dis+1+t);// pair_num表示点对数量int pair_num=0;int i=1,j=t;// 这个while循环就是把两个dis相加的和小于等于K的点对数量计算出来while (i<j){while (i<j && dis[i]+dis[j]>K) j--;pair_num+=j-i;i++;}return pair_num;
}void getDistance(int start, int parent, int weight)//fa表示x的父亲,z表示x到目标点的距离
{// dis数组保存了每个点到重心的距离(权重),为什么用t来做下标而不是点的编号呢// 因为后面的做法只用数点对的个数,不在乎是谁到谁dis[++t]=weight;for(int i=first[start]; i; i=edge[i].next){int end=edge[i].end;// 如果这个终点已经被遍历或者这个终点是父结点,就跳过// 这点很重要,因为如果传入的start不是根结点而是根结点的子树的时候// 它就不会把根结点再遍历一次if(visited[end] || end==parent){continue;}getDistance(end, start, weight+edge[i].weight);}
}
// 传入参数
// start 代表当前结点
// parent 代表当前结点的父结点,这里是为了防止遍历start的子结点的时候把父结点也遍历进去了
void getGravity(int start, int parent)
{// size[start]代表当前结点的个数,初始为1是算上本身size[start]=1;// max_child[start]代表当前结点的最大子树结点数max_child[start]=0;// 遍历以start结点为起点的所有边(除去连接父结点的边)for(int i=first[start]; i; i=edge[i].next){// end为当前边的终点(也是start点的子结点)int end=edge[i].end;// 如果这个终点已经被遍历或者这个终点是父结点,就跳过if(visited[end] || end==parent){continue;}// 继续遍历终点的子结点,这里其实就是遍历start的一棵子树的所有结点数getGravity(end, start);// 遍历完这棵子树的所有结点后,把子树的结点数加起来size[start]+=size[end];// 如果这颗子树的结点数大于max_child,就更新它if(size[end] > max_child[start]){max_child[start]=size[end];}}// 上面的循环是用来遍历start的每一棵子树并比较出最大子树结点// 接下来就是算“红色部分”,也就是n-size[i]部分的结点数,并比较出最终的最大子树结点if(n-size[start] > max_child[start]){max_child[start]=n-size[start];}// 从max_child中比较出最小的,以找出重心if(min < max_child[start]){min=max_child[x];gravity=start;}
}// 递归地求每个树的经过重心的点对数量
// 起始点是start
void dfs(int start){// 以start为起点找到重心// 注意,虽然一开始我们说了从树的任何一个点开始遍历都能找出一个确定的重心,// 但是这里的意思是,从start开始,它的父结点就不要再遍历的,只找它和它的子树的重心getGravity(start,0);// 用这个重心算出所有跨过该重心的路径数ans += work(gravity,0);// 标记这个重心已访问visited[gravity]=1;// 从重心开始访问子结点for(int i=first[start]; i; i=edge[i].next){int end=edge[i].end;if (visited[end]){continue;}// 减去以子结点为根的树的所有跨过子结点的路径数ans -= work(end, edge[i].weight);// 以子结点为根的树继续求重心、所有跨过路径数dfs(end);}return;
}int main()
{// 输入结点个数scanf("%d",&n);// 输入每个结点的信息for(int i=1;i<n;i++){int start, end, weight;scanf("%d %d %d",&start,&end,&weight);addNodeAndEdge(start,end,weight);addNodeAndEdge(end,start,weight);}dfs(1);printf("%d\n", ans);return 0;
}

时间复杂度暂时没算。。。

算法分析课设(四)采用分治法求树中任意两点之间距离小于K的点对及路径相关推荐

  1. 采用分治法求一个整数序列中的最大值和最小值

    #include <iostream> using namespace std; void MaxMin(int a[], int l, int r, int &maxe, int ...

  2. 深大算法设计与分析实验二——分治法求最近点对问题

    源代码: 深大算法设计与分析实验二--分治法求最近点对问题代码-C/C++文档类资源-CSDN下载 目录 实验问题 一.实验目的: 二.内容: 三.算法思想提示 产生不重复的随机点算法: 蛮力算法: ...

  3. 分治法求平面最近点对入门

    一.平面最近点对问题. 平面最近点对:在一个平面上有 n n n个点,求出距离最近的两个点. 平面最近点对是计算几何中一个十分经典且基础的问题,通常采用分治法来解决. 二.直线最近点对的分治法. 在用 ...

  4. python众数问题给定含有n个元素的多重集合s_分治法求众数 给定含有n个元素的多重集合S 联合开发网 - pudn.com...

    分治法求众数 所属分类:数据结构 开发工具:C/C++ 文件大小:240KB 下载次数:3 上传日期:2018-01-04 20:19:09 上 传 者:九鼎 说明:  给定含有n个元素的多重集合S, ...

  5. java分治法求数列的最大子段和_同事为进大厂天天刷Java面试题,面试却履败!究其原因竟是它在捣鬼。...

    写在前面 疫情过后,招聘与求职受影响到底有多大?我不知道,但我的真实感受是,即使有疫情的影响,最近还是持续有朋友来跟我说他们今年工作的新动向.有人跳槽去了大厂,有人下定决心出来创业,也有人还在观望,等 ...

  6. 程序员的算法课(13)-分治法

    一.什么是分治 [百度百科]分治法((Divide and Conquer))可以通俗的解释为:把一片领土分解,分解为若干块小部分,然后一块块地占领征服,被分解的可以是不同的政治派别或是其他什么,然后 ...

  7. c语言分治法求众数重数_分治法求众数 - osc_twlari2q的个人空间 - OSCHINA - 中文开源技术交流社区...

    分治法求众数 Problem Description 给定含有n个元素的多重集合S,每个元素在S中出现的次数称为该元素的重数.多重集S中重数最大的元素称为 众数.例如,S={1,2,2,2,3,5}. ...

  8. c语言分治法求众数重数_分治法求众数问题 (配图)

    标签: 采用分治法,以中间为界限, 先计算围绕中间这个数字的众数情况,然后左右分开递归计算结果,取最值即可. 左右递归计算的时候要先做判断,假如左边或是右边的个数都比已求的重数小,就没必要计算了,即使 ...

  9. 分治法求一个N个元素数组的逆序数

    背景  逆序数:也就是说,对于n个不同的元素,先规定各元素之间有一个标准次序(例如n个 不同的自然数,可规定从小到大为标准次序),于是在这n个元素的任一排列中,当某两个元素的先后次序与标准次序不同时, ...

最新文章

  1. python以读写方式打开文件_python读写文件操作详细介绍【传智播客】
  2. 学校为什么要单位接收函_温江会计培训学校:为什么要学习会计实操
  3. mysql新增json_MySQL5.7中新增的JSON类型的使用方法
  4. 根据 设备名(br0/eth0/em0)称获取 当前机器的IP地址与子网掩码信息
  5. Python核心团队计划2020年停止支持Python2,NumPy宣布停止支持计划表
  6. 《剑指Offer》 最小的K个数
  7. 方法 -------JavaScript
  8. 幻灯片形式设计:从方法到技巧
  9. 测试项目实战----RuoYi后台管理系统
  10. godot初体验,开源跨平台的神奇游戏引擎。支持python和rust,甚至推出了android端。
  11. swagger配置及注解详解
  12. 计算机英语二国家线,历年考研英语国家线一览表
  13. 挨踢人生路 记我的10年18家工作经历
  14. c语言中begin用法,C++ set cbegin() 使用方法及示例
  15. python外星人入侵(游戏开发)
  16. linux 内核调试 booting the kernel.,booting the kernel后无内核启动信息的调试方法
  17. 移动软件开发-设计app首页
  18. H.264基础知识总结
  19. 这个AI算法,可以帮“元宇宙”虚拟人进行虚拟更换衣服
  20. 1.口袋西游人物属性(01)

热门文章

  1. 查看linux snmp状态,Linux开启snmp及查询
  2. linux服务器相关配置查看
  3. @Value读取properties中文乱码解决方案
  4. 苹果手机密码设置在哪里_sim卡密码设置在哪里-sim卡密码设置介绍
  5. mac 命令行小技巧--mas 替代appstore
  6. 项目管理之项目干系人
  7. Call From hadoop102/192.168.10.102 to hadoop102:8020 failed on connection exception: java.net.Connec
  8. uniapp引用外部在线js
  9. input标签能换行么?textarea标签属性
  10. 纯JavaScript二维码在线生成网页源码