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)−2∗d(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(q∗logn+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][j−1]的含义是节点i向root方向走
2
j
−
1
2^{j-1}
2j−1个边到达的父节点。已知
2
j
2^j
2j=
2
j
−
1
+
2
j
−
1
2^{j-1}+2^{j-1}
2j−1+2j−1,因此我们想要得到
f
a
[
i
]
[
j
]
fa[i][j]
fa[i][j],我们可以先得到
f
a
[
i
]
[
j
−
1
]
fa[i][j-1]
fa[i][j−1],然后再得到它的
f
a
[
i
]
[
j
−
1
]
fa[i][j-1]
fa[i][j−1],也就是关键代码
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][j−1]][j−1]
由于我们已知
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的位置。因此,我们通过KaTeX parse error: Expected 'EOF', got '&' at position 3: P &̲ (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递减。
该循环运行完后的结果是图中红圈圈出的:
显而易见,我们要求的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。
完整代码(C++版):
#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))