CSS分类(需要构建一棵树)(全局的特征)
要求:输入两篇论文的名字(或其他标识符)就可以获得这两篇论文的分类所在节点到公共父节点的最短距离。
首先我们研究树中求得两个节点X,Y到最近公共父节点的距离。
方法一:暴力求解
思路:利用BFS或者DFS,从一个节点出发,沿着树的结构一层层查找,知道到达另一个节点,此时的距离为最短距离。
分析:时间复杂度O(n)O(n)O(n)
方法二:简单LCA
思路:
d=d(x,root)+d(y,root)−2∗d(z,root)d=d(x,root)+d(y,root)-2*d(z,root)d=d(x,root)+d(y,root)−2∗d(z,root)
其中,x,y为两个待求节点,z为最近公共祖先。
我们需要提前预处理的是求得所有的d(∗,root)d(*,root)d(∗,root) ,这样,根据公式,我们只需要知道那个节点是z就可以根据公式求出d。
LCA假设永远满足d(x,root)<d(y,root)d(x,root)<d(y,root)d(x,root)<d(y,root) ,若不满足,则将x,y互换即可。
步骤一:预处理深度数组。
维护数组 dep[∗]dep[*]dep[∗] ,使得 dep[∗]=d(∗,root)dep[*]=d(*,root)dep[∗]=d(∗,root) ,做查表用。
步骤二:跳到相同高度。
当 d(x,root)<d(y,root)d(x,root)<d(y,root)d(x,root)<d(y,root) 时,y=fa[y]y=fa[y]y=fa[y]( fa[y]fa[y]fa[y] 数组存储的是y的直接父亲节点),即当x,y不“同级”时, 将fa[y]fa[y]fa[y]赋值给y,y“向上跳了一级”。
步骤三:xy同时跳。
当 d(x,root)=d(y,root)d(x,root)=d(y,root)d(x,root)=d(y,root) 后,我们使x和y同时“向上跳”。即,当 x!=yx!=yx!=y 时:x=fa[x]x=fa[x]x=fa[x],y=fa[y]y=fa[y]y=fa[y]。
找到z之后,就可以套公式求出距离。当然,我们也可以直接在每次跳的时候都d=d+1d=d+1d=d+1(假设边权重为1)。
分析:步骤一时间复杂度为O(n)O(n)O(n),执行一次步骤二三的时间复杂度为 lognlognlogn ,因此,对于需求为“树结构基本不变,需要多次查询”的任务来说,时间复杂度为O(q∗logn+n)O(q*logn+n)O(q∗logn+n)(q为查询次数)
时间复杂度的分析是很有必要的,因为我们最终是以网页的形式呈现的,我们需要点击后的快速响应,而对于较为庞大的数据量来说,时间复杂度非常重要。
- 简单LCA对于结构较为“平衡”的树来说比较友好,但是对于比较“偏”的树来说步骤二三的时间复杂度可能上升为O(n)O(n)O(n),因此,我们需要进一步加快“跳”的过程。我们利用“倍增”的思想来实现算法的改进。
方法三:改进LCA
思路:在简单LCA的基础上改进“跳”的过程。
步骤一:预处理深度数组。
同上
步骤二:跳到相同高度。
已知任何一个十进制的自然数都能拆分成若干个2的幂的和。比如:17=24+2017=2^4+2^017=24+20,那么我们是不是可以利用这种性质进行优化?
已知fa[i]fa[i]fa[i]表示的是节点i的直接父亲节点,我们假设fa[i][j]fa[i][j]fa[i][j]表示节点i向root方向走2j2^j2j个边到达的父节点。我们需要做的预处理还有维护数组fa[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];
}
}
}
我们利用以上函数求得数组fa[i][j]fa[i][j]fa[i][j]的所有值。
解析:已知fa[i][j]fa[i][j]fa[i][j]的含义是节点i向root方向走2j2^j2j个边到达的父节点,那么fa[i][j−1]fa[i][j-1]fa[i][j−1]的含义是节点i向root方向走2j−12^{j-1}2j−1个边到达的父节点。已知2j2^j2j=2j−1+2j−12^{j-1}+2^{j-1}2j−1+2j−1,因此我们想要得到fa[i][j]fa[i][j]fa[i][j],我们可以先得到fa[i][j−1]fa[i][j-1]fa[i][j−1],然后再得到它的fa[i][j−1]fa[i][j-1]fa[i][j−1],也就是关键代码fa[i][j]=fa[fa[i][j−1]][j−1]fa[i][j]=fa[fa[i][j-1]][j-1]fa[i][j]=fa[fa[i][j−1]][j−1]
由于我们已知 dep[x]dep[x]dep[x]和 dep[y]dep[y]dep[y],因此我们可以得到 dep[y]−dep[x]dep[y]-dep[x]dep[y]−dep[x],设为PPP(已保证d(x,root)<d(y,root)d(x,root)<d(y,root)d(x,root)<d(y,root))。
为了优化“跳”的过程,我们可以把PPP分解,例如,假设P=17P=17P=17,而17=24+2017=2^4+2^017=24+20,因此,我们yyy向上“跳”P(17)P(17)P(17)的过程可以分解为“跳”242^424和202^020的过程。那么关键的问题就变成如何寻找PPP的分解。
for(int i=0;i<23;i++){
if(P & (1<<i)){
y=fa[y][i];
}
}
已知1<<i1<<i1<<i表示111左移iii位,例如,当i=5i=5i=5时,1<<i=(100000)21<<i=(100000)_21<<i=(100000)2,而KaTeX parse error: Expected 'EOF', got '&' at position 3: P &̲ (1<<i)其实就是检测了PPP的哪一位为111。观察二进制表示与分解的关系可知,当P=17P=17P=17时,PPP的二进制表示为(10001)2(10001)_2(10001)2而17=24+2017=2^4+2^017=24+20,因此可以看出,分解出来的各个指数的幂次就是原来数的二进制表示的111的位置。因此,我们通过 Pand(1<<i)P and (1<<i)Pand(1<<i)找到PPP二进制表示为111的位置,我们就找到了分解。
步骤三:xy同时跳。
假设树结构如下图:

则执行完步骤二时,x,yx,yx,y已经同深度了。此时我们需要做的是“同时跳”。
for(int i=23;i>=0;i--){
if(fa[x][i]!=fa[y][i]){
x=fa[x][i];
y=fa[y][i];
}
}
一旦fa[x][i]=fa[y][i]fa[x][i]=fa[y][i]fa[x][i]=fa[y][i],那么我们有两种情况:
- 恰好是LCA
- 跳过了,是LCA的父节点
如果是第一种情况,那么非常好,这是我们想要的,但是我们没法判断是否出现了第二种情况,因此我们我们先 “尝试跳”,即我们使iii递减。这样我们找到的第一个大概率是跳过了,因此当fa[x][i]=fa[y][i]fa[x][i]=fa[y][i]fa[x][i]=fa[y][i]时,我们不做改变,反而当fa[x][i]!=fa[y][i]fa[x][i]!=fa[y][i]fa[x][i]!=fa[y][i]时,我们选择往上跳。
- iii递减
为什么使得iii递减?
- 若iii递增,则我们i=0,1,2,3...i=0,1,2,3...i=0,1,2,3...的时候就很容易就跳,但是我们以后若还需要跳比较小的深度的时候,iii却无法取到比较小的值了,因此我们使iii递减。
该循环运行完后的结果是图中红圈圈出的:

显而易见,我们要求的LCA是图中红色节点的直接父亲,因此fa[x][0]fa[x][0]fa[x][0]或者fa[y][0]fa[y][0]fa[y][0]就是LCA。
找到对应的节点后,我们就可以套公式求出ddd。
完整代码:
#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))
本文深入讲解了求解树中两个节点最近公共祖先(LCA)问题的三种算法:暴力求解、简单LCA及改进LCA。详细介绍了每种算法的思路、预处理步骤及查询流程,并提供了Python代码实现。
1073

被折叠的 条评论
为什么被折叠?



