HDU2586——How far away ?(LCA模板)

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的距离 EG=E+G2。其中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][j1],f[i+(1<<(j1))][j1])
这里主要是一个分治的思想:一个区间的最小值为左半边区间和右半边区间中的较小值。

查询操作:
我们要求区间 [ x , y ] [x,y] [x,y](保证 x < y x<y x<y)的最小值,我们可以得到区间的长度 y − x + 1 y-x+1 yx+1,而f[i][j]只能表示 1 , 2 , 4 , 8 , … … , 2 n 1,2,4,8,……,2^n 12482n的区间长度。所以我们计算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=3y=9k=2x+(1<<k)=7y(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 EC的最近公共祖先,在遍历完 B , E , F , G B,E,F,G BEFG后,将吧 E , F , G E,F,G EFG直接与 A A A相连,且将 B , E , F , G B,E,F,G BEFG都标记。当遍历到 C C C节点时,就可以直接找到 E E E的父亲 A A A,所以 E , C E,C EC的最近公共祖先就是 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;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值