连接性问题
问题概述
让我们先看看图片
在连接或断开的这个点网络中,可以找到从P点到Q点的路径。可以抽象为连接问题,包括在计算机网络中判断两台主机是否连接在一起,以及在社交网络中两个用户是否有间接社交关系。
问题抽象
网络上的点(主机、人)可以抽象成对象,p-q表示P连接到Q,连接关系可以传递。p-q q-r=p-r;为了简要说明问题,如果将两个对象标记为整数对,则指定的整数对序列将描述点网络。
下图中的节点数N=5网络(使用0 ~ N-1表示对象)可以用整数对序列0-1 1-3 2-4来说明连接关系。其中0和3也连接在一起,有两个连接组件,{0,1,3}和{
问题:将描述连接关系的整数对序列指定为两个整数P和Q,以确认是否可以连接。
问题的例子
输入未连接
3-4 3-4
4-9 4-9
8-0 8-0
2-3 2-3
5-6 5-6
2-9 2-3-4-9
5-9 5-9
7-3 7-3
4-8 4-8
5-6 5-6
0-2 0-8-4-3-2
6-1 6-1
相应的连接图如下:黑线表示两个节点次连接,绿线表示两个节点已经有连接关系。
算法1:快速搜索算法
使用阵列id[i]储存节点值。I是节点序号。也就是说,初始状态序列号和数组值相同。
输入前两个连接后,id[i]将更改为:
Id[i]的值表示连接完成后I是连接的结束节点。如果连接了p和q,则id[p]和id[q]的值必须相同。
4-9完成后,id[3]和id[4]的值都是结束节点9。这时直接判断3和9是否连接,id[3]和id[9]的值是否相同,如果相同,是否连接,如果不相同,是否存在连接关系。显然,id[3]==id[9]==9意味着存在连接关系。
算法实现
/* * file 3360 1.1-quick _ find . go */
Package main
Import.
Const N=10
Var id [N]int
Func main() {
reader :=buf io . new reader(OS . stdin)
//初始化元素值与节点序列号相同的id数组
FOR I :=0;I N;I {
Id[i]=I
}
//读取命令行输入
For {
Data,_,_ :=reader。ReadLine()
Str:=string(数据)
If str==' \ n ' {
Continue
}
If str==' # ' {
布雷克
}
Values :=字串。Split(str,')。
p,_ :=strconv.atoi(值[0])
q,_ :=strconv.atoi(值[1])
If Connected(p,q) {
FMT . Printf(“Already连接节点3360% d-%d \ n”,P,Q)
Continue
}
union(p,q)
}
}
//决定整数p和q的节点是否连接在一起
Func Connected(p,q int) bool {
Return id[p]==id[q]
}
//连接的p-q节点
Func Union(p,q int) {
Pid :=id[p]
Qid :=id[q]
重复//id阵列,将值为id[p]的所有节点替换为id[q]
FOR I :=0;I N;I {
If id[i]==PID {
Id[i]=qid
}
}
Fmt.printf(“未连接节点3360% d-%d \ n”,P,Q)
}
运营效果:可以判断2-9已经连接。
复杂性
快速查找算法在判断P和Q是否连接时,只需要判断id[p]和id[q]是否相同即可。但是,当P和Q不连接时合并,因此每次合并时都需要遍历整个数组。特性:快速查找,缓慢合并
算法2:快速合并算法
概述
每次合并时,快速查找算法都会完全遍历数组,使其效率低下。如果不每次遍历id[],而是优化数组中的部分值,则复杂性会降低。
这时要考虑树形结构。在连接关系的传递性上,p-r q-r=p-q可以看作root,P和Q可以看作子点。因为P和Q有相同的根R,所以P和Q是相连的。这里的树是连接关系的抽象。
数据结构
要将数组用作树实现,请执行以下操作:
节点阵列id[N],id[i]储存I的父节点
I的根节点是ID[ID].id[i].]。父节点的父节点.根节点(父节点是自身)
使用树的好处
将整数到序列表示从数组更改为树,每个节点存储父节点的位置。这个树有两个优点。
请检查p和q是否连接:是否存在同一个根节点
从p合并到q:将p的根节点更改为q的根节点(快速合并,无需整个遍历)
示例:
对于上述整数对序列,查找、合并过程如下:橙色是合并操作,灰色是连接状态,绿色是存储树的阵列。
红色2-3不是直接使用2作为3的子节点,而是找到3的根节点9,合并2-3和3-4-9,生成2-9
算法实现:
/* *文件3360 1.2-quick _ union.go */
//p和q具有相同的根节点,并且已连接。
Func Connected(p,q int) bool {
Return getRoot(p)==getRoot(q)
}
//连接的p-q节点
Func Union(p,q int) {
PRoot :=getRoot(p)
QRoot :=getRoot(q)
Id[pRoot]=qRoot //q树的根现在有父节点(p树的根)以完成合并
Fmt.printf(“未连接节点3360% d-%d \ n”,P,Q)
}
获取//节点I的根节点
Func getRoot(i int) int {
//在没有根节点的情况下继续向上查找
For I!=id[i] {
I=id[i]
}
Return I
}
算法3:具有权值的快速合并算法
概述
快速合并算法有一个缺陷。数据量大时,随机合并子树会提高树,在找到根节点时,需要遍历数组的大部分值。还是很慢。在下图中,如果判断P,Q是否连接,则需要找到13个节点。
如果树合并后仍然变短,子树之间保持平衡,则找到根节点后,少遍历很多节点,检查下图中是否连接了P、Q,只需要找到7个节点。
构建平衡树
要想构建平衡的树,合并时要将小树合并到大树上,不要让合并的树慢慢上升或上升,从而大大减少大部分合并需要通过的节点。(大卫亚设,Northern Exposure(美国电视剧),女性)区分小树,大树使用树的权重。子树包含节点数。
数据结构
树节点的存储仍然使用id[i],但需要额外的数组大小[i]来记录节点I的子节点数。
算法实现
/* *
文件: 1.3-weighted _ version.go
基于快速合并算法,合并操作将小树合并为大树即可
*/
Var id [N]int
Var size [N]int
Func main() {
//初始化元素值与节点序列号相同的id数组
FOR I :=0;I N;I {
Id[i]=I
大小[I]=I
}
.
}
.
//连接的p-q节点
Func Union(p,q int) {
PRoot :=getRoot(p)
QRoot :=getRoot(q)
//p树是大树
If size[pRoot] size[qRoot] {
Id[pRoot]=qRoot根
Size[qRoot]=size[pRoot]
} else {
Id[qRoot]=id[pRoot]
Size[pRoot]=size[qRoot]
}
Id[pRoot]=qRoot //q树的根现在有父节点(p树的根)以完成合并
Fmt.printf(“未连接节点3360% d-%d \ n”,P,Q)
}
算法4:路径压缩的加权快速合并算法
概述
加权快速合并算法即使大部分整数对都是直接连接的,结果树也仍然像序列一样高。
10-8 8-6 11-9 12-9 9-6 6-3 7-3 3-1 4-1 5-1 1-0 2-0
结果树如下:
此时,要判断9-2的连接关系,必须分别找到9和2的根节点。找到9的根节点时,它会经过6,3,1树。6、3和1树的子节点与9一样,根节点都是0,因此将6、3和1树直接更改为0的子树。下面是:
优化
每次计算节点的根节点时,沿路径检查的节点也指向根节点。如果尽可能地展平树,则在检查连接状态时巡回的节点数将大幅减少。
算法实现
/* *
file 3360 1.4-path _ compression _ by _ halving . go
更改的代码很少,但很精致
*/
获取//节点I的根节点
Func getRoot(i int) int {
//在没有根节点的情况下继续向上查找
For I!=id[i] {
Id [I]=id [id [I]//节点,继续向上移动,直到节点的父节点连接到根节点为止
I=id[i]
}
Return I
}
复杂性
n是节点集合的大小,t是树的高度。
算法初始化的复杂性集成复杂性检索复杂性
快速查找N N(完整遍历)1(数组值比较)
快速合并遍历树(N T)遍历树(T)
具有加权的快速合并N LG N LG N
路径压缩的权重将n快速合并到接近1(树的高度接近2)。
摘要
如上所述,有四种算法可以解决连接问题,从效率低下的基本功能的快速搜索到复杂性接近1的路径压缩带的快速合并。算法可以学习解决程序问题的一般步骤。也就是说,您可以完成基本功能,然后针对效率低下的任务进行优化,从而降低复杂性。