【蓝桥杯】版本分支 (图论——Tarjan求LCA)

本文详细介绍了Tarjan算法在解决离线最近公共祖先(LCA)问题上的应用,阐述了LCA的基本概念和Tarjan算法的原理。通过实例展示了如何利用Tarjan算法判断版本间的祖先关系,并给出了相关代码实现。此外,还提供了一个具体的版本分支问题,解释了如何利用算法解答此类问题。
摘要由CSDN通过智能技术生成

一、算法介绍(Tarjan算法-离线求LCA )

1. 什么是最近共先祖 ?

首先是最近公共祖先的概念(什么是最近公共祖先?):

在一棵没有环的树上,每个节点肯定有其父亲节点和祖先节点,而最近公共祖先,就是两个节点在这棵树上深度最大的公共的祖先节点。

换句话说,就是两个点在这棵树上距离最近的公共祖先节点。

所以LCA主要是用来处理当两个点仅有唯一一条确定的最短路径时的路径。

有人可能会问:那他本身或者其父亲节点是否可以作为祖先节点呢?

答案是肯定的,很简单,按照人的亲戚观念来说,你的父亲也是你的祖先,而LCA还可以将自己视为祖先节点。
    
举个例子吧,如下图所示4和5的最近公共祖先是2,5和3的最近公共祖先是1,2和1的最近公共祖先是1。 
在这里插入图片描述

2.Tarjan算法

时间复杂度: O(n + m) ,n是结点的数量,m为询问查询的数量。Tarjan是离线算法,所谓离线就是不能边查询两个结点的公共祖先,只能把结点全部存起来,再来求。

算法思路:
tarjan算法其实就是我们在深度遍历是对结点进行分类处理,我们在对整颗树深搜时,会将所以点分为三类:这里我们用数字0,1,2来代替。(这个用一个st数组维护即可)
第0类,就是还没遍历过的结点。
第1类,就是在目前所遍历到的分支上的结点
第2类,就是已经遍历过的结点并且已经回溯完成。(他和第一类的区别在于结点已经回溯完成)。

那tarjan是如何来处理公共祖先问题的呢?这里需要借助一个数据结构——并查集。在遍历时我们需要记录下第2类结点的最近祖先。这样我们在遍历到每一个结点的时候,只需要来枚举一下当前结点存在哪些询问,如果存在一个询问的话,那么我们找到询问中对应的另一个结点,如果此时这个结点为第2类,那么并查集存的就是他们的祖先结点。
举个例子:假设我们现在遍历到u点,那么此时我们应该把u归为第一类(令st[u] = 1),接着我们枚举u的相邻结点,等我们回溯回来后,就可以来处理询问了,那么假设现在我们查找到一个询问,问u和v的最近公共祖先是谁。如果v结点为第二类,那么此时并查集中v的祖先结点就是他们两个的最近公共祖先。

上面例子理解起来不难,但是为什么这样就能确定并查集中v的祖先结点就是他们两个的最近公共祖先?

因为我们在深搜时,最近公共祖先必定是先被遍历到的,而并查集中的祖先结点也是离v最近的祖先结点,那么此时的话就会出现两种情况,第一种,如果在这个祖先结点下存在一对询问,最近公共祖先必然就是并查集中v的祖先结点。否则的话就是第二种情况:如果不存在,那么在随着回溯的进行,v的祖先结点也会因此被更新的更高一层。所以这个过程是并查集和回溯互补的过程,需要细细体会。

废话不多说,看下面例题吧。

二、例题

版本分支

题目描述
小明负责维护公司一个奇怪的项目。这个项目的代码一直在不断分支(branch)但是从未发生过合并(merge)。

现在这个项目的代码一共有 N 个版本,编号 1 ~ N,其中 1 号版本是最初的版本。

除了 1 号版本之外,其他版本的代码都恰好有一个直接的父版本;即这 N 个版本形成了一棵以 1 为根的树形结构。

如下图就是一个可能的版本树:

1

/ \

2 3

\ / \

5 4 6

现在小明需要经常检查版本 x 是不是版本 y 的祖先版本。你能帮助小明吗?

输入描述
第一行包含两个整数 N, QN,Q,代表版本总数和查询总数。

以下 NN - 1 行,每行包含 2 个整数 u 和 v ,代表版本 u 是版本 v 的直接父版本。

再之后 QQ 行,每行包含 2 个整数 x 和 y,代表询问版本 x 是不是版本 y 的祖先版本。

输出描述
对于每个询问,输出 YES 或 NO 代表 xx 是否是 yy 的祖先。

输入输出样例
输入
6 5
1 2
1 3
2 5
3 6
3 4
1 1
1 4
2 6
5 2
6 4
输出

YES
YES
NO
NO
NO
运行限制
最大运行时间:1s
最大运行内存: 256M
题目来源: 第九届蓝桥杯国赛
题目链接:版本分支

思路分析:
题意很简单,就是给我们两个结点x,y。问x是不是y的祖先结点。注意这里问的只是祖先结点,不是公共祖先。这个题目就是考察我们tarjan的回溯更新祖宗结点这个特性,这个做法其实很简单,首先我们存下询问,询问内容是,x结点是否是y的祖先结点,直接一遍tarjan算法,并且遍历到每个结点,在询问处理是,如果y的并查集中的父节点出现过x,那么则x就是y的祖先结点。

最后附上代码,输入内容很大,最好用scanf读入。

#include <iostream>
#include <vector>
using namespace std;

typedef pair<int,int> PII;

const int N = 100010;

int p[N],st[N];

vector<PII> query[N];
vector<int> v[N];
bool res[N];

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

void tarjan(int u)
{
    st[u] = 1;
    for (int i = 0; i < v[u].size(); i ++ )
    {
      int t = v[u][i];
      if(!st[t])
      {
          tarjan(t);
          p[t] = u;
      }
    }

    for (auto item : query[u])
    {
       int y = item.first,id = item.second;
       int anc = Find(y);
       if(anc == u) res[id] = true;
    }

    st[u] = 2;
}

int main()
{
  int n,m;
  scanf("%d%d",&n,&m);
  for (int i = 0; i < n - 1; i ++ )
  {
      int a,b;
      scanf("%d%d",&a,&b);
      v[a].push_back(b);
      v[b].push_back(a);
  }
  for (int i = 0; i < m; i ++ )
  {
      int a,b;
      scanf("%d%d",&a,&b);
      query[a].push_back({b,i});
  }
  for (int i = 1; i <= n; i ++ ) p[i] = i;
  tarjan(1);
  for (int i = 0; i < m; i ++ )
      if(res[i]) printf("YES\n");
      else printf("NO\n");
  return 0;
}

更多例题推荐:acwing距离

总结

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

老帅比阿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值