对到最近公共父节点的距离的算法的研究

CSS分类(需要构建一棵树)(全局的特征)

要求:输入两篇论文的名字(或其他标识符)就可以获得这两篇论文的分类所在节点到公共父节点的最短距离。

首先我们研究树中求得两个节点X,Y到最近公共父节点的距离。

方法一:暴力求解

思路:利用BFS或者DFS,从一个节点出发,沿着树的结构一层层查找,知道到达另一个节点,此时的距离为最短距离。
分析:时间复杂度 O ( n ) O(n) O(n)

方法二:简单LCA

思路:

d = d ( x , r o o t ) + d ( y , r o o t ) − 2 ∗ d ( z , r o o t ) d=d(x,root)+d(y,root)-2*d(z,root) d=d(x,root)+d(y,root)2d(z,root)
其中,x,y为两个待求节点,z为最近公共祖先。
我们需要提前预处理的是求得所有的 d ( ∗ , r o o t ) d(*,root) d(,root) ,这样,根据公式,我们只需要知道那个节点是z就可以根据公式求出d。

LCA假设永远满足 d ( x , r o o t ) < d ( y , r o o t ) d(x,root)<d(y,root) d(x,root)<d(y,root) ,若不满足,则将x,y互换即可。

步骤一:预处理深度数组。
维护数组 d e p [ ∗ ] dep[*] dep[] ,使得 d e p [ ∗ ] = d ( ∗ , r o o t ) dep[*]=d(*,root) dep[]=d(,root) ,做查表用。

步骤二:跳到相同高度。
d ( x , r o o t ) < d ( y , r o o t ) d(x,root)<d(y,root) d(x,root)<d(y,root) 时, y = f a [ y ] y=fa[y] y=fa[y] f a [ y ] fa[y] fa[y] 数组存储的是y的直接父亲节点),即当x,y不“同级”时, 将 f a [ y ] fa[y] fa[y]赋值给y,y“向上跳了一级”。

步骤三:xy同时跳。
d ( x , r o o t ) = d ( y , r o o t ) d(x,root)=d(y,root) d(x,root)=d(y,root) 后,我们使x和y同时“向上跳”。即,当 x ! = y x!=y x!=y 时: x = f a [ x ] x=fa[x] x=fa[x] y = f a [ y ] y=fa[y] y=fa[y]
找到z之后,就可以套公式求出距离。当然,我们也可以直接在每次跳的时候都 d = d + 1 d=d+1 d=d+1(假设边权重为1)。
分析:步骤一时间复杂度为 O ( n ) O(n) O(n),执行一次步骤二三的时间复杂度为 l o g n logn logn ,因此,对于需求为“树结构基本不变,需要多次查询”的任务来说,时间复杂度为 O ( q ∗ l o g n + n ) O(q*logn+n) O(qlogn+n)(q为查询次数)

时间复杂度的分析是很有必要的,因为我们最终是以网页的形式呈现的,我们需要点击后的快速响应,而对于较为庞大的数据量来说,时间复杂度非常重要。

  • 简单LCA对于结构较为“平衡”的树来说比较友好,但是对于比较“偏”的树来说步骤二三的时间复杂度可能上升为 O ( n ) O(n) O(n),因此,我们需要进一步加快“跳”的过程。我们利用“倍增”的思想来实现算法的改进。
方法三:改进LCA

思路:在简单LCA的基础上改进“跳”的过程。

步骤一:预处理深度数组。
同上

步骤二:跳到相同高度。
已知任何一个十进制的自然数都能拆分成若干个2的幂的和。比如: 17 = 2 4 + 2 0 17=2^4+2^0 17=24+20,那么我们是不是可以利用这种性质进行优化?
已知 f a [ i ] fa[i] fa[i]表示的是节点i的直接父亲节点,我们假设 f a [ i ] [ j ] fa[i][j] fa[i][j]表示节点i向root方向走 2 j 2^j 2j个边到达的父节点。我们需要做的预处理还有维护数组 f a [ i ] [ j ] fa[i][j] fa[i][j]。代码如下:

void GET(){
	for(int j=1;j<=23;j++){
		for(int i=1;i<=n;i++){
			fa[i][j]=fa[fa[i][j-1]][j-1];
		}
	}
}

我们利用以上函数求得数组 f a [ i ] [ j ] fa[i][j] fa[i][j]的所有值。
解析:已知 f a [ i ] [ j ] fa[i][j] fa[i][j]的含义是节点i向root方向走 2 j 2^j 2j个边到达的父节点,那么 f a [ i ] [ j − 1 ] fa[i][j-1] fa[i][j1]的含义是节点i向root方向走 2 j − 1 2^{j-1} 2j1个边到达的父节点。已知 2 j 2^j 2j= 2 j − 1 + 2 j − 1 2^{j-1}+2^{j-1} 2j1+2j1,因此我们想要得到 f a [ i ] [ j ] fa[i][j] fa[i][j],我们可以先得到 f a [ i ] [ j − 1 ] fa[i][j-1] fa[i][j1],然后再得到它的 f a [ i ] [ j − 1 ] fa[i][j-1] fa[i][j1],也就是关键代码 f a [ i ] [ j ] = f a [ f a [ i ] [ j − 1 ] ] [ j − 1 ] fa[i][j]=fa[fa[i][j-1]][j-1] fa[i][j]=fa[fa[i][j1]][j1]

由于我们已知 d e p [ x ] dep[x] dep[x] d e p [ y ] dep[y] dep[y],因此我们可以得到 d e p [ y ] − d e p [ x ] dep[y]-dep[x] dep[y]dep[x],设为 P P P(已保证 d ( x , r o o t ) < d ( y , r o o t ) d(x,root)<d(y,root) d(x,root)<d(y,root))。
为了优化“跳”的过程,我们可以把 P P P分解,例如,假设 P = 17 P=17 P=17,而 17 = 2 4 + 2 0 17=2^4+2^0 17=24+20,因此,我们 y y y向上“跳” P ( 17 ) P(17) P(17)的过程可以分解为“跳” 2 4 2^4 24 2 0 2^0 20的过程。那么关键的问题就变成如何寻找 P P P的分解。

for(int i=0;i<23;i++){
	if(P & (1<<i)){
		y=fa[y][i];
	}
}

已知 1 < < i 1<<i 1<<i表示 1 1 1左移 i i i位,例如,当 i = 5 i=5 i=5时, 1 < < i = ( 100000 ) 2 1<<i=(100000)_2 1<<i=(100000)2,而KaTeX parse error: Expected 'EOF', got '&' at position 3: P &̲ (1<<i)其实就是检测了 P P P的哪一位为 1 1 1。观察二进制表示与分解的关系可知,当 P = 17 P=17 P=17时, P P P的二进制表示为 ( 10001 ) 2 (10001)_2 (10001)2 17 = 2 4 + 2 0 17=2^4+2^0 17=24+20,因此可以看出,分解出来的各个指数的幂次就是原来数的二进制表示的 1 1 1的位置。因此,我们通过 P a n d ( 1 < < i ) P and (1<<i) Pand(1<<i)找到 P P P二进制表示为 1 1 1的位置,我们就找到了分解。

步骤三:xy同时跳。

假设树结构如下图:
树
则执行完步骤二时, x , y x,y x,y已经同深度了。此时我们需要做的是“同时跳”。

for(int i=23;i>=0;i--){
	if(fa[x][i]!=fa[y][i]){
		x=fa[x][i];
		y=fa[y][i];
	}
}

一旦 f a [ x ] [ i ] = f a [ y ] [ i ] fa[x][i]=fa[y][i] fa[x][i]=fa[y][i],那么我们有两种情况:

  • 恰好是LCA
  • 跳过了,是LCA的父节点

如果是第一种情况,那么非常好,这是我们想要的,但是我们没法判断是否出现了第二种情况,因此我们我们先 “尝试跳”,即我们使 i i i递减。这样我们找到的第一个大概率是跳过了,因此当 f a [ x ] [ i ] = f a [ y ] [ i ] fa[x][i]=fa[y][i] fa[x][i]=fa[y][i]时,我们不做改变,反而当 f a [ x ] [ i ] ! = f a [ y ] [ i ] fa[x][i]!=fa[y][i] fa[x][i]!=fa[y][i]时,我们选择往上跳。

  • i i i递减

为什么使得 i i i递减?

  • i i i递增,则我们 i = 0 , 1 , 2 , 3... i=0,1,2,3... i=0,1,2,3...的时候就很容易就跳,但是我们以后若还需要跳比较小的深度的时候, i i i却无法取到比较小的值了,因此我们使 i i i递减。

该循环运行完后的结果是图中红圈圈出的:
树2
显而易见,我们要求的LCA是图中红色节点的直接父亲,因此 f a [ x ] [ 0 ] fa[x][0] fa[x][0]或者 f a [ y ] [ 0 ] fa[y][0] fa[y][0]就是LCA。

找到对应的节点后,我们就可以套公式求出 d d d

完整代码:

#include<cstdio>
#include<iostream>
#define maxn 1010
using namespace std;
int n,m,num,head[maxn],c[maxn],fa[maxn][25],dis[maxn];
struct node{
    int t,v,pre;
}e[maxn*2];
void Add(int from,int to,int dis){
    num++;e[num].v=to;
    e[num].t=dis;
    e[num].pre=head[from];
    head[from]=num;
}
void Dfs(int now,int from,int dep,int S){
    c[now]=dep;fa[now][0]=from;dis[now]=S;
    for(int i=head[now];i!=0;i=e[i].pre){
        int v=e[i].v;
        if(v==from)continue;
        Dfs(v,now,dep+1,S+e[i].t);
    }
}
void Get(){
    for(int j=1;j<=23;j++)
        for(int i=1;i<=n;i++)
            fa[i][j]=fa[fa[i][j-1]][j-1];
}
int LCA(int a,int b){
    if(c[a]<c[b])swap(a,b);
    int t=c[a]-c[b];
    for(int i=0;i<=23;i++)
        if(t&(1<<i))a=fa[a][i];
    if(a==b)return a;
    for(int i=23;i>=0;i--)
        if(fa[a][i]!=fa[b][i]){
            a=fa[a][i];b=fa[b][i];
        }
    return fa[a][0];
}
int main()
{
    cin>>n>>m;//n个点m次询问 
	//建树 
    int u,v,t;//起点,终点,权值 
    for(int i=1;i<n;i++){//n-1条边 
        cin>>u>>v>>t;
        Add(u,v,t);Add(v,u,t);//建双向边
    }
    Dfs(1,0,0,0);Get();
    while(m--){
    	cin>>u>>v; 
        int anc=LCA(u,v);
        int re=dis[u]+dis[v]-2*dis[anc];
        printf("%d\n",re);
    }
    return 0;
}

修改后的python版本

class Tree:
    def __init__(self, n):
        self.n = n
        self.Graph = [[] for i in range(n + 1)]

    def insert(self, x, y):
        self.Graph[x].append(y)
        self.Graph[y].append(x)

#
# class Node:
#     def __init__(self, id):
#         self.id = id


def dfs(now, father, deep):
    fa[now][0] = father
    dep[now] = deep
    for v in tree.Graph[now]:
        if v != father:
            dfs(v, now, deep + 1)


def get_fa():
    for j in range(1, S + 1):
        for i in range(1, n + 1):
            fa[i][j] = fa[fa[i][j - 1]][j - 1]


def get_same(a, t):
    for i in range(S + 1):
        if t & (1 << i):
            a = fa[a][i]
    return a


def LCA(a, b):
    if dep[a] < dep[b]:
        a, b = (b, a)
    a = get_same(a, dep[a] - dep[b])
    if a == b:
        return a
    for i in range(S, -1, -1):
        if fa[a][i] != fa[b][i]:
            a = fa[a][i]
            b = fa[b][i]
    return fa[a][0]


n, m, s = input().split()
n = int(n)
m = int(m)
s = int(s)
S = 25
tree = Tree(n)
fa = [[0 for _ in range(S + 1)] for __ in range(n + 1)]
dep = [0 for _ in range(n + 1)]

for _ in range(n - 1):
    x, y = input().split()
    x = int(x)
    y = int(y)
    tree.insert(x, y)
dfs(s, s, 0)
get_fa()
for _ in range(m):
    x, y = input().split()
    x = int(x)
    y = int(y)
    print(LCA(x, y))

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值