距离---

距离


题目描述

image-20210722165410424


前置知识

tarjan算法

由于tarjan发明了很多算法,特别是在图论领域,因此很多算法都直接被称为了tarjan算法,需要注意区分的是,这里的tarjan算法是用来解决LCA问题的离线算法。

所谓的在线做法离线做法都是针对有询问这种类型的题目。

  • 在线算法:每读入一个询问,都需要运行一次程序立即得到本次查询的答案。若一次查询需要 O ( l o g n ) O(logn) O(logn),则 m m m次查询需要 O ( m l o g n ) O(mlogn) O(mlogn)
  • 离线算法:读入所有询问,然后运行一次程序就可以得到所有查询的答案。

tarjan算法利用并查集优越的时空复杂性,可以在 O ( n + m ) O(n+m) O(n+m)时间内解决LCA问题。

问题:为什么tarjan算法利用并查集就可以求出LCA呢?

如下图:

image-20210722170818667

从图中可以看出,节点 u u u与子树 A A A中的节点,它俩的最近公共祖先是根节点 r o o t root root,而且我们发现一个很强的性质就是:子树 A A A中的每个节点的祖宗节点刚好就是 r o o t root root;节点 u u u与子树 B B B中的每个节点,它俩最近公共祖先是那个被涂色的节点,而这个节点LCA刚好就是节点子树 B B B中每个节点的祖宗节点,也就是说 v v v节点的祖宗节点是那个被涂色节点,而 从图中易知 u u u v v v的最近公共祖先恰好就是那个被涂色的节点。

因此,当我们遍历完 u u u时,查询有关 u u u的询问,比如 v v v,然后使用并查集找到节点 v v v的集合号,那么就是 u u u v v v的最近公共祖先LCA了。

下面是tarjan算法求解LCA的过程:

  • 初始化集合号数组和访问数组,即fa[i]=ivis[i]=0,这里 v i s [ i ] = 0 vis[i]=0 vis[i]=0表示 i i i这个节点还没有被访问过, v i s [ i ] = 1 vis[i]=1 vis[i]=1表示 i i i这个节点已经被访问过了
  • 从节点 u u u出发进行深度优先遍历,标记 v i s [ u ] = 1 vis[u]=1 vis[u]=1,深搜 u u u所有还未被访问的邻接点,在遍历的过程中更新距离,回退时更新节点的集合号(祖宗节点)
  • 当节点 u u u的所有邻接点全部遍历完毕时,检查关于 u u u的所有询问,若存在一个查询 u , v u,v u,v,并且 v i s [ v ] = 1 vis[v]=1 vis[v]=1,那么就利用并查集查找节点 v v v的集合号,找到它的祖宗节点,那么这个祖宗节点就是 u u u v v v的最近公共祖先节点。

举个栗子:

在树中求 5 、 6 5、6 56的最近公共祖先,求解过程如下:

【1】初始化所有节点的集合号等于自己,访问数组都设置为还没有被访问过,即 f a [ i ] = i fa[i]=i fa[i]=i v i s [ i ] = 0 vis[i]=0 vis[i]=0

如下图所示:

image-20210722180942823

【2】随意选择一个节点进行深搜,这里选择从根节点开始深度优先遍历,在遍历的过程中,将访问过的节点都设置为 v i s [ i ] = 1 vis[i]=1 vis[i]=1

如下图所示:

image-20210722181051956

【3】 8 8 8号节点的所有邻接点都已经访问完毕了,没有与 8 8 8相关的查询,则回退到 6 6 6,更新 f a [ 8 ] = 6 fa[8]=6 fa[8]=6

如下图所示:

image-20210722181120440

【4】遍历 6 6 6号节点的下一个邻接点 9 9 9,标记 v i s [ 9 ] = 1 vis[9]=1 vis[9]=1 9 9 9号节点的所有邻接点都已经访问完毕了,没有与 9 9 9相关的查询,则回退到 6 6 6,更新 f a [ 9 ] = 6 fa[9]=6 fa[9]=6

如下图所示:
在这里插入图片描述

【5】 6 6 6号节点的所有邻接点都已经访问完毕了,有与 6 6 6相关的查询(查询 5 5 5 6 6 6),但是由于此时 v i s [ 5 ] ≠ 1 vis[5]\neq1 vis[5]=1,并不满足 v i s [ u ] = 1 vis[u]=1 vis[u]=1并且 v i s [ v ] = 1 vis[v]=1 vis[v]=1(这里 u = 6 , v = 5 u=6,v=5 u=6,v=5),所以啥也不做,然后回退到 4 4 4,更新 f a [ 6 ] = 4 fa[6]=4 fa[6]=4

如下图所示:

image-20210722181334949

【6】 4 4 4号节点的所有邻接点都已经访问完毕了,没有与 4 4 4相关的查询,回退到 2 2 2,更新 f a [ 4 ] = 2 fa[4]=2 fa[4]=2

如下图所示:

image-20210722181412378

【7】遍历 2 2 2号节点的下一个邻接点 5 5 5,标记 v i s [ 5 ] = 1 vis[5]=1 vis[5]=1,继续深度优先遍历到 7 7 7,标记 v i s [ 7 ] = 1 vis[7]=1 vis[7]=1 7 7 7号节点的所有邻接点都已经访问完毕了,没有与 7 7 7相关的查询,回退到 5 5 5,更新 f a [ 7 ] = 5 fa[7]=5 fa[7]=5

如下图所示:

image-20210722181421905

【8】 5 5 5号节点的所有邻接点都已经访问完毕了,有与 5 5 5相关的查询(查询 5 5 5 6 6 6),并且 v i s [ 6 ] = 1 vis[6]=1 vis[6]=1,满足 v i s [ u ] = 1 vis[u]=1 vis[u]=1并且 v i s [ v ] = 1 vis[v]=1 vis[v]=1(这里 u = 5 , v = 6 u=5,v=6 u=5,v=6),那么就需要执行并查集,找到节点 v v v的集合号。回退到 2 2 2,更新 f a [ 5 ] = 2 fa[5]=2 fa[5]=2。设找到节点 v v v的祖宗为 x x x,那么 x x x就是节点 u u u和节点 v v v的最近公共祖先

如下图所示:

image-20210722181530072

【9】从 6 6 6号节点开始利用并查集查找祖宗的过程如下:首先判断 6 6 6的集合号 f a [ 6 ] = 4 fa[6]=4 fa[6]=4,找到 4 4 4的集合号 f a [ 4 ] = 2 fa[4]=2 fa[4]=2,找到 2 2 2的集合号 f a [ 2 ] = 2 fa[2]=2 fa[2]=2,所以找到祖宗(集合号为其自身)后返回,在回溯过程中更新祖宗到当前节点路径上所有节点的集合号,即更新 6 、 4 6、4 64的父节点为 f a [ 4 ] = 2 fa[4]=2 fa[4]=2 f a [ 6 ] = 2 fa[6]=2 fa[6]=2。那么此时 f a [ 6 ] = 2 fa[6]=2 fa[6]=2,即 2 2 2号节点就是 5 5 5号节点和 6 6 6号节点的最近公共祖先

如下图所示:

image-20210722181540652

总结:

在当前节点 u u u的所有邻接点都已经访问完毕时,检查与 u u u相关的查询 v v v,注意此时 v i s [ u ] vis[u] vis[u]已经是1了,如果 v i s [ v ] ≠ 1 vis[v]\neq1 vis[v]=1,那么什么也不做;如果 v i s [ v ] = 1 vis[v]=1 vis[v]=1,则利用并查集查找 v v v的祖宗节点。最终 L C A ( u , v ) = f a [ v ] LCA(u,v)=fa[v] LCA(u,v)=fa[v]。实际上, u u u的祖宗就是 u u u向上查找第1个邻接点未访问完的节点,因为它的 f a [ ] fa[] fa[]还没有更新,仍满足 f a [ i ] = i fa[i]=i fa[i]=i,它就是 v v v的祖宗。

算法实现:

并查集算法:

int find(int x)
{
    if(x!=p[x])
        p[x]=find(p[x]);
    return p[x];
}

tarjan算法:

void tarjan(int u)
{
    vis[u]=1;
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        if(vis[j])
            continue;
        dist[j]=dist[u]+w[i];
        tarjan(j);
        fa[j]=u;
    }
    for(int i=0;i<query[u].size();i++)
    {
        //query[u][i]存储的是第i次询问时,与u相关的查询号  将其赋值给v
        int v=query[u][i];
        //query_id[u][i]存储的是第i次查询
        int id=query_id[u][i];
        if(vis[v])
        {
            int lca=find(v);
            ans[id]=dist[u]+dist[v]-2*dist[lca];
        }
    }
}

核心思路

这道题就直接利用上面所讲述的方法,使用tarjan+并查集算法来解决LCA的离线算法。

树上任意两点之间的距离公式: L = d i s t [ u ] + d i v t [ v ] − 2 × d i s t [ l c a ] L=dist[u]+divt[v]-2\times dist[lca] L=dist[u]+divt[v]2×dist[lca]

d i s t [ u ] dist[u] dist[u]表示节点 u u u到整棵树的根节点的距离;

d i s t [ v ] dist[v] dist[v]表示节点 v v v到整棵树的根节点的距离;

d i s t [ l c a ] dist[lca] dist[lca],其中 l c a lca lca是节点 u u u和节点 v v v的最近公共祖先,表示节点 l c a lca lca到整棵树的根节点的距离;


代码

#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
//这是一棵树  有N个节点 则有N-1条边 由于是无向边  所以要开2倍
const int N=10010,M=2*N;
typedef pair<int,int>PII;
int n,m;    //n是点数  m是询问次数
int h[N],e[M],ne[M],w[M],idx;
int dist[N];    //距离
int p[N];       //并查集的集合号数组
int res[M];     //存储询问的结果
int st[N];      //标记当前节点是否已经被访问过  st[i]=1表示i节点被访问过了
//query[u]是存放关于节点u的查询信息
//query[u].first存放的是与u相关的那个查询是啥(比如说查询5 6  那么u就是5  query[5].first就是6)
//query[u].second存放的就是这次询问是第几次
vector<PII>query[N]; //存放查询数据

//并查集查找x的祖宗节点
int find(int x)
{
    if(x!=p[x])
        p[x]=find(p[x]);
    return p[x];
}

//建边
void add(int a,int b,int c)
{
    e[idx]=b;
    w[idx]=c;
    ne[idx]=h[a];
    h[a]=idx++;
}

//tarjan算法解决LCA的离线查询
void tarjan(int u)
{
    //标记节点u已经被访问过了
    st[u]=1;
    //遍历节点u的所有邻接点
    for(int i=h[u];~i;i=ne[i])
    {
        //j是u的邻接节点
        int j=e[i];
        //如果节点j还没有被访问过
        if(!st[j])
        {
            //更新顶点j的距离
            dist[j]=dist[u]+w[i];
            //以下两句不能调换  因为调换后 经过find函数会进行路径压缩 更新了节点  导致错误
            //然后对顶点j进行深度优先遍历
            tarjan(j);
            //回退时更新顶点j的父节点为u
            p[j]=u;
        }
    }
    //如果有查询的话
    for(auto item:query[u])
    {
        int v=item.first;   //取出与u相关的查询v
        int id=item.second; //这是第几次查询
        //如果节点v之前已经遍历过了  
        //那么满足st[u]=1并且st[v]=1
        if(st[v])
        {
            //对节点v进行并查集操作  找到它的祖宗节点
            //那么这个祖宗节点就是节点u和节点v的最近公共祖先
            int lca=find(v);
            //记录这次查询的结果 也就是树上节点u和节点v之间的距离
            res[id]=dist[u]+dist[v]-2*dist[lca];
        }
    }
}
int main()
{
    memset(h,-1,sizeof h);
    scanf("%d%d",&n,&m);
    //读入n-1个数据
    for(int i=0;i<n-1;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        //建立无向边
        add(a,b,c);
        add(b,a,c);
    }
    //读入m次询问
    for(int i=0;i<m;i++)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        //如果a==b 即查询自身到自身的距离 那肯定是0  那就直接使用全局res数组的0就好了
        //因此当a!=b时  才记录
        if(a!=b)
        {
            query[a].push_back({b,i});
            query[b].push_back({a,i});
        }
    }
    //初始化每个点都是独立的集合
    for(int i=1;i<=n;i++)
        p[i]=i;
    //这里选择从根节点1进行深度优先遍历,进行tarjan算法
    tarjan(1);
    //输出这m次查询的结果
    for(int i=0;i<m;i++)
        printf("%d\n",res[i]);
    return 0;    
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值