How far away ?
题意:给出一棵树,问两个节点x,y之间的最短距离是多少(边权值)。
思路:用
根
节
点
到
x
的
距
离
+
根
节
点
到
y
的
距
离
−
多
走
的
距
离
根节点到x的距离+根节点到y的距离-多走的距离
根节点到x的距离+根节点到y的距离−多走的距离
其中多走的距离要用LCA解决。
L C A LCA LCA
L
C
A
LCA
LCA只是一个概念,它的是一棵树上的两个节点的最近公共祖先。当然,它也是一个节点。
来源:百度百科-树形结构 。
拿图片举例:
E
E
E和
G
G
G的最近公共祖先就是
B
B
B,
I
I
I和
B
B
B的最近公共祖先就是
A
A
A。
结合例题:
E
到
G
的
距
离
=
根
节
点
到
E
的
距
离
+
根
节
点
到
G
的
距
离
−
2
∗
根
节
点
到
B
的
距
离
E到G的距离 = 根节点到E的距离 + 根节点到G的距离 - 2 * 根节点到B的距离
E到G的距离=根节点到E的距离+根节点到G的距离−2∗根节点到B的距离。其中B为E,G的最近公共祖先。同理其他两节点之间的距离就也是如此。
L C A LCA LCA求解方法
1.欧拉序 + + +线段树或RMQ
欧拉序
用一个数组记录某个节点在某个时间访问。
o
u
l
a
[
i
]
=
j
oula[i] = j
oula[i]=j表示在第
i
i
i个时刻访问了节点
j
j
j。
o
u
l
a
oula
oula数组在
d
f
s
dfs
dfs遍历后应该是
A
D
J
D
I
D
A
C
H
C
A
B
G
B
F
B
E
B
A
A D J D I D A C H C A B G B F B E B A
ADJDIDACHCABGBFBEBA
欧拉序不唯一,但每两个节点之间的较小的节点(按遍历顺序较小)总是这两个节点的公共祖先。
我们用
c
n
t
cnt
cnt数组存下每个节点第一次出现的位置,之后在这个区间中找到最小的哪个节点,就找到了最近公共祖先了。
void dfs(int u, int fa, int d) {
oula[++len] = u;
dep[u] += d;
if(!vis[u]) {
cnt[u] = len;
vis[u] = 1;
}
for(int i=head[u]; i; i=edge[i].next) {
if(edge[i].to != fa) {
dfs(edge[i].to, u, d+edge[i].we);
oula[++len] = u;
}
}
}
RMQ(线段树)
再说一说这个RMQ,这个是用来寻找某个区间的最小值的。
这个就得讲讲构建ST表,和如何查询了。
构建ST表
我们用
f
[
i
]
[
j
]
f[i][j]
f[i][j]数组表示从第
i
i
i个元素起后
2
j
2^j
2j个元素的最小值。那该怎么求出这个表呢。我们可以知道
f
[
i
]
[
0
]
=
o
u
l
a
[
i
]
f[i][0] = oula[i]
f[i][0]=oula[i],之后
f
[
i
]
[
1
]
=
m
i
n
(
f
[
i
]
[
0
]
,
f
[
i
+
1
]
[
0
]
)
f[i][1] = min(f[i][0], f[i+1][0])
f[i][1]=min(f[i][0],f[i+1][0]),由此得到递推式
f
[
i
]
[
j
]
=
m
i
n
(
f
[
i
]
[
j
−
1
]
,
f
[
i
+
(
1
<
<
(
j
−
1
)
)
]
[
j
−
1
]
)
f[i][j] = min(f[i][j-1], f[i+(1<<(j-1))][j-1])
f[i][j]=min(f[i][j−1],f[i+(1<<(j−1))][j−1])
这里主要是一个分治的思想:一个区间的最小值为左半边区间和右半边区间中的较小值。
查询操作:
我们要求区间
[
x
,
y
]
[x,y]
[x,y](保证
x
<
y
x<y
x<y)的最小值,我们可以得到区间的长度
y
−
x
+
1
y-x+1
y−x+1,而f[i][j]只能表示
1
,
2
,
4
,
8
,
…
…
,
2
n
1,2,4,8,……,2^n
1,2,4,8,……,2n的区间长度。所以我们计算int k = int(log(y-x+1)/log(2));
其中
2
k
2^k
2k为向下取整(
对
应
上
面
的
特
定
区
间
对应上面的特定区间
对应上面的特定区间)的区间大小。
再分两段取最小值。
设
x
=
3
,
y
=
9
,
得
k
=
2
。
x
+
(
1
<
<
k
)
=
7
,
y
−
(
1
<
<
k
)
=
5
设x = 3,y = 9,得k = 2。x + (1<<k) = 7,y - (1<<k) = 5
设x=3,y=9,得k=2。x+(1<<k)=7,y−(1<<k)=5。
求出
m
i
n
(
f
[
x
]
[
k
]
,
f
[
y
−
(
1
<
<
k
)
]
[
k
]
)
min(f[x][k],f[y-(1<<k)][k])
min(f[x][k],f[y−(1<<k)][k])就是整个区间
[
x
,
y
]
[x,y]
[x,y]的最小值了。
void init_ST() {
memset(f, 0, sizeof f);
for(int i=1; i<=len; i++) {
f[i][0] = oula[i];
}
for(int i=1; (1<<i) <=len; i++) {
for(int j=1; (j+(1<<i)) <=len; j++) {
f[j][i] = min(f[j][i-1], f[j+(1<<(i-1))][i-1]);
}
}
}
int query(int x, int y) {
if(x > y) swap(x, y);
int k = int(log(y-x+1)/log(2));
return min(int(f[x][k]), int(f[y-(1<<k)][k]));
}
总代码:
#include<bits/stdc++.h>
using namespace std;
// ST表维护欧拉序求lca
const int N = 1e5;
int n, m;
int f[N][32], pos[N][32], cnt[N];
int oula[N], dep[N], len = 0;
int head[N], vis[N], idx = 1;
struct E {
int to, next, we;
} edge[N<<1];
void add(int x, int y, int w) {
edge[idx].to = y, edge[idx].next = head[x], edge[idx].we = w, head[x] = idx++;
}
void init_ST() {
memset(f, 0, sizeof f);
for(int i=1; i<=len; i++) {
f[i][0] = oula[i];
}
for(int i=1; (1<<i) <=len; i++) {
for(int j=1; (j+(1<<i)) <=len; j++) {
f[j][i] = min(f[j][i-1], f[j+(1<<(i-1))][i-1]);
}
}
}
int query(int x, int y) {
if(x > y) swap(x, y);
int k = int(log(y-x+1)/log(2));
return min(int(f[x][k]), int(f[y-(1<<k)][k]));
}
void dfs(int u, int fa, int d) {
oula[++len] = u;//欧拉序
dep[u] += d;//深度,也就是到根节点的距离。
if(!vis[u]) {
cnt[u] = len;
vis[u] = 1;
}
for(int i=head[u]; i; i=edge[i].next) {
if(edge[i].to != fa) {
dfs(edge[i].to, u, d+edge[i].we);
oula[++len] = u;
}
}
}
void init() {//多组输入,初始化。
len = 0, idx = 1;
memset(head, 0, sizeof head);
memset(edge, 0, sizeof edge);
memset(dep, 0, sizeof dep);
memset(cnt, 0, sizeof cnt);
memset(f, 0, sizeof f);
}
int main() {
freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
int a, b, c, t;
cin >> t;
while(t--) {
init();
cin >> n >> m;
for(int i=1; i<n; i++) {
cin >> a >> b >> c;
add(a, b, c);
add(b, a, c);
}
dfs(1, 0, 0);
init_ST();
for(int i=0; i<m; i++) {
cin >> a >> b;
c = query(cnt[a], cnt[b]);
cout << dep[a] + dep[b] - 2*dep[c] << endl;
}
}
return 0;
}
同理:线段树也可以维护区间的最小值,求LCA。
说明:下面代码只是求LCA的板子,和题目没关系。
#include<bits/stdc++.h>
using namespace std;
//线段树维护欧拉序求lca
const int N = 1e5;
const int inf = 0x7fffffff;
int n, m;
int uola[N], cnt[N], len = 0;
int head[N], vis[N], idx = 1;
int T[N<<2];
struct E {
int to, next;
}edge[N<<1];
void add(int a, int b) {
edge[idx].to = b, edge[idx].next = head[a], head[a] = idx++;
}
void dfs(int u, int fa) {//可以用dep[u]计算权值。
uola[++len] = u;
if(!vis[u]) {
cnt[u] = len;
vis[u] = 1;
}
for(int i=head[u]; i; i=edge[i].next) {
if(edge[i].to != fa) {
dfs(edge[i].to, u);
uola[++len] = u;
}
}
}
void make_tree(int x, int y, int node) {//建树
if(x == y) {
T[node] = uola[x];
return ;
}
int mid = x + y >> 1;
make_tree(x, mid, node<<1);
make_tree(mid+1, y, node<<1|1);
T[node] = min(T[node<<1], T[node<<1|1]);
}
int query(int x, int y, int l, int r, int node) {//查询
if(x <= l && y >= r) return T[node];
if(x > r || y < l) return inf;
int mid = l + r >> 1;
int u = query(x, y, l, mid, node<<1);
int v = query(x, y, mid+1, r, node<<1|1);
return min(u, v);
}
int main() {
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
int a, b, c;
cin >> n >> m;
for(int i=1; i<n; i++) {
cin >> a >> b;
add(a, b);
add(b, a);
}
dfs(1, 0);
make_tree(1, len, 1);
for(int i=0; i<m; i++) {
cin >> a >> b;
if(cnt[a] > cnt[b]) swap(a, b);
cout << query(cnt[a], cnt[b], 1, len, 1) << endl;
}
}
2.Tarjan
这个方法主要是用
d
f
s
dfs
dfs和并查集实现。
从这篇大佬的博客学到的LCA 最近公共祖先
主要思路:对树进行 d f s dfs dfs,每到一个节点就看它是否在要查询的元素对中,当某个节点是要在查询的一对节点中时,且另一个节点已经访问过(被访问过后,节点会和它的父节点并在一起)。询问另一节点的父亲就是最近公共祖先。
还是这张图:
假如我们要求
E
,
C
E,C
E,C的最近公共祖先,在遍历完
B
,
E
,
F
,
G
B,E,F,G
B,E,F,G后,将吧
E
,
F
,
G
E,F,G
E,F,G直接与
A
A
A相连,且将
B
,
E
,
F
,
G
B,E,F,G
B,E,F,G都标记。当遍历到
C
C
C节点时,就可以直接找到
E
E
E的父亲
A
A
A,所以
E
,
C
E,C
E,C的最近公共祖先就是
A
A
A。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e4;
using namespace std;
int head[N], vis[N], idx = 1;
//链式前向星模板{
struct E{
int to, next;
} edge[N<<1];
void add(int x, int y) {
edge[idx].to = y, edge[idx].next = head[x], head[x] = idx++;
}
//}
vector<int> V[N];//存下m次查询。
vector<int> C[N];//存在每次查询的位置。
int f[N], ans[N];//存储父亲节点,和查询得到的结果。
//并查集模板 {
int finds(int x) {
return f[x] == x ? x : f[x] = finds(f[x]);
}
void unions(int x, int y) {
int r1 = finds(x);
int r2 = finds(y);
if(r1 != r2) {
f[r1] = r2;
}
}
//}
//dfs+unions操作将节点合并。
void dfs(int u, int fa) {
f[u] = u;
vis[u] = 1;
for(int i=head[u]; i; i=edge[i].next) {
if(edge[i].to != fa) {
dfs(edge[i].to, u);
//细节处理,必须是子节点将边加到父节点的祖先上,和f[r1] = r2对应。
unions(edge[i].to, u);
}
}
//验证节点是否要被查询。
for(int i=0; i<V[u].size(); i++) {
int t = V[u][i];
if(vis[t]) {//满足查询条件。
ans[C[u][i]] = finds(t);
}
}
}
int main() {
freopen("in.txt", "r", stdin);
int n, m, a, b;
cin >> n >> m;
for(int i=1; i<=n; i++) f[i] = i;
for(int i=0; i<n-1; i++) {
cin >> a >> b;
add(a, b); add(b, a);
}
for(int i=0; i<m; i++) {
cin >> a >> b;
V[a].push_back(b)
V[b].push_back(a);
C[a].push_back(i);
C[b].push_back(i);
}
dfs(1, 1);
for(int i=0; i<m; i++) {
cout << ans[i] << endl;
}
return 0;
}