点的距离 (倍增求LCA模板)

问题:

给定一棵 n 个点的树,Q 个询问,每次询问点 x 到点 y 两点之间的距离。

输入格式

第一行一个正整数 n(1<= n <=10^5),表示这棵树有 n 个节点;

接下来 n-1 行,每行两个整数 x, y (1<= x , y<=n )表示 x, y 之间有一条连边;

然后一个整数 Q,表示有 Q 个询问;

接下来 Q 行每行两个整数 x, y 表示询问 x 到 y 的距离。

输出格式

输出 Q 行,每行表示每个询问的答案。

样例

样例输入

6
1 2
1 3
2 4
2 5
3 6
2
2 6
5 6

样例输出

3
4

LCA求公共祖先:DFS+ST(RMQ)

两个关键理论:
任意一个数都可以用二进制去表示,拆分方法也很简单:例如20这个数,先找最接近但不大于20的一个2的幂次数,即16,然后20减去16等于4。再找最接近但不大于4的一个2的幂次数,即4。然后4-4=0,拆分结束,得到了16和4。如果c是a和b的LCA,那么c的所有祖先同样也是a和b的公共祖先,但不是最近的。

在ST算法中,我们维护了一个数组 dp[i][j],表示的是以下标为i为起点的长度为2^j的序列的信息。然后用动态规划的思想求出整个数组。刚才在上面说我们求LCA时一次要跳2的幂次方层,这就与dp数组中下标 j 的定义不谋而合了。所以我们定义倍增法中的dp[i][j]为:结点 i 的向上 2^j 层的祖先。


  dp[4][1]=1;结点4的向上2^1=2层的祖先是结点1。
  dp[10][1]=2;结点10的向上2^1=2层的祖先是结点2。
  注意dp[6][0]=3,结点6的向上2^0=1层的祖先是3,即6的父节点。而这一现象正好可以当做DP的初始条件。dp[i][0]为i的父节点。下面写出递推式:dp[i][j] = dp[ dp[i][j-1] ] [j-1]。        如何理解这个递推式呢?dp[i][j-1]是结点i往上跳2^(j-1)层的祖先,那我们就在跳到这个结点的基础上,再向上跳2^(j-1)层,这样就相当于从结点i,先跳2^(j-1)层,再跳2^(j-1)层,最后还是到达了2^j层。

代码(模板):

#include<stdio.h>
#include<string.h>
#include<math.h>
#define N 100005
#define M 25
#include<algorithm>
using namespace std;
int n,x,y,cot,m;//需要用到的变量
int dp[N][M],fa[N],book[N],deep[N];//算法用到的数组
int first[N],net[N*2],to[N*2];//邻接表数组
void init()//初始化
{
    cot=1;
    memset(deep,0,sizeof(deep));
    memset(first,-1,sizeof(first));
    memset(dp,0,sizeof(dp));
    memset(book,0,sizeof(book));
}
void build_bian(int x,int y)//构建邻接表
{
    to[cot]=y;
    net[cot]=first[x];
    first[x]=cot++;
}
void dfs(int x,int father)//DFS找到边与边之间的关系
{
    book[x]=1;
    fa[x]=father;//主要就是要这个东西father表示x的父亲节点
    for(int i=first[x]; i!=-1; i=net[i])
    {
        int k=to[i];
        if(k!=father)//防止回到父亲节点
            dfs(k,x);
    }
}
int LCA(int x,int y)
{
    if(deep[x]<deep[y])//确保x的深度大于y的深度,方便后面操作。 
        swap(x,y);
    for(int i=17; i>=0; i--)//若不能确保x的深度大于y,这一步中就无法确定往上跳的是x还是y
        if(deep[x]-(1<<i)>=deep[y])
            x=dp[x][i];
    if(x==y)//若二者处于同一深度后,正好相遇,则这个点就是LCA
        return x;
    for(int i=17; i>=0; i--)//x和y同时往上跳,从大到小遍历步长,遇到合适的就跳上去,不合适就减少步长
    {
        if(dp[x][i]!=dp[y][i]) //若二者没相遇则跳上去
        {
            x=dp[x][i];
            y=dp[y][i];
        }
    }
    return dp[x][0];//最后x和y跳到了LCA的下一层,LCA就是x和y的父节点
}
int main()
{
    init();
    scanf("%d",&n);
    for(int i=1; i<n; i++)
    {
        scanf("%d%d",&x,&y);
        build_bian(x,y);//双向建边
        build_bian(y,x);
    }
    dfs(1,0);
    for(int i=1; i<=n; i++)//处理deep数组和dp数组
    {
        deep[i]=deep[fa[i]]+1;
        dp[i][0]=fa[i];
    }
    for(int i=1; i<=n; i++)
        for(int j=1; j<=log2(n); j++)
            dp[i][j]=dp[dp[i][j-1]][j-1];//求出dp数组
    scanf("%d",&m);
    while(m--)
    {
        scanf("%d%d",&x,&y);
        int L=LCA(x,y);
        int sum=(deep[x]+deep[y])-2*deep[L];
        printf("%d\n",sum);
    }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Tarjan算法是一种用于解决最近公共祖先(LCA)问题的离线算法。离线算法指的是在读取所有查询之后一次性计算所有查询的答案,而不是每读取一个查询就计算一次。\[1\] 在Tarjan算法中,需要使用并查集来实现。并查集是一种数据结构,用于维护元素之间的集合关系。下面是一个并查集的模板代码: ```cpp int fa\[100000\]; void reset(){ for (int i=1;i<=100000;i++){ fa\[i\]=i; } } int getfa(int x){ return fa\[x\]==x?x:getfa(fa\[x\]); } void merge(int x,int y){ fa\[getfa(y)\]=getfa(x); } ``` 在Tarjan算法的伪代码中,首先标记当前节为已访问状态。然后遍历当前节的子节,递归调用Tarjan函数并合并子节。接下来,遍历与当前节有查询关系的节,如果该节已经访问过,则输出当前节和该节的LCA(通过并查集的查找函数getfa获取)。\[3\] 以上是关于Tarjan算法解LCA的相关内容。 #### 引用[.reference_title] - *1* [Tarjan 算法解决 LCA 问题](https://blog.csdn.net/chengqiuming/article/details/126878817)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [详解使用 Tarjan LCA 问题(图解)](https://blog.csdn.net/weixin_34315485/article/details/93801193)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值