#2020寒假集训#最近公共祖先入门(Least Common Ancestors)代码笔记

倍增算法(在线:输入一个查询一个)

【存图】
  • 链式前向星(结构体存起始位置、边权等信息)
  • vector邻接表(结构体存终点、边权等信息,下标是起点)
【函数】(样例使用链式前向星存图)
  • 初始化函数(init函数)
    对链式前向星、输入数组、计数变量、标记数组、祖先结点数组初始化
  • 链式前向星函数(EDGE结构体及其内部赋值函数+addEdge函数)
    构造链式前向星,输入的时候直接用addEdge函数加边存储,后续用于遍历
  • 深度搜索函数(DFS函数)
    预处理出每个结点的深度 and 每个结点上一层结点(父结点)
  • LCA初始化函数(LCAinit函数)
    初始化储存每个结点向上 2 的 i 次方层祖先结点,为LCA查询备用
    思想:它的爷爷是它爹的爹
  • LCA查询函数(LCA函数)
    先将深度大的结点移到与另一结点同深度的位置,再同时向上移层,直到它们的父亲结点相同
**初始化函数**
void init()
{
	memset(head,-1,sizeof(head));
	memset(dis,0,sizeof(dis));
	edgecount=-1;
	for(int i=1;i<=n;++i) vis[i]=yes[i]=false,father[i][0]=i;
}
**链式前向星函数**
struct EDGE
{
	int from,to,next;
	long long w;
/*
	Edge(){}是个用来给变量初始化0的函数
	fzhead:fzbody{}为结构体赋初值
*/
	EDGE(){}//后面别加分号 
	fzhead:fzbody{}//后面别加分号 
}edge[maxn];
/*
	i:边的编号;从from到to;w:权值,此处已删除;next:下一条边的编号
	head[i]:由i出发的第一条边的编号
	仅插入一条从u1开始的边,则next是-1,head[u1]为该边的编号
	再读入u1开始的边,则其为第一条边,next是上一条边的编号 
*/
void addEdge(int from,int to,int w)//链序和读入顺序相反 
{
	edge[++edgecount]=EDGE(from,to,w,head[from]);
	head[from]=edgecount;
}
**深度搜索函数**
void DFS(int root, int d)//处理每个结点的深度 and 寻找每个点的父结点 
{//d是初始根结点的上一层深度,因为有deep[root]=++d;赋值前根结点深度也加了1,初始深度一般算作1 
	vis[root]=true;
	deep[root]=++d;//深度递增 
	for(int i=head[root];i!=-1;i=edge[i].next)//递归出口就是到达叶子结点 
	{//链式前向星遍历,遍历的是root的子结点们,子结点的子结点在递归中遍历 
		if(vis[edge[i].to]==false)
		{//无向图 1 到 2 一条边,还有 2 到 1 一条边,不能重复访问,有向图不会出现 
			father[edge[i].to][0]=root;//子孙节点往上2的0次方层(1层)的祖先为父结点 
			dis[edge[i].to]=dis[root]+edge[i].w;
			DFS(edge[i].to,d);
		}//上文的++d先自加后赋值可以使得到此的d就是这层深度,也是对下一层而言的上一层深度 
	}
}
**LCA初始化函数**
void LCAinit()
{
	for(int j=1;j<=18;++j)
	{
		for(int i=1;i<=n;++i)
		{//思想:它的爷爷是它爹的爹 
			father[i][j]=father[father[i][j-1]][j-1];
			/*
				i 往上 2 的 j 次方层的祖先
				就是 i 往上 2 的 j-1 次方层
				再往上 2 的 j-1 次方层的祖先 
				显然 2 的 j 次方 等于 2 的 j-1 次方 *2(两次 j-1 次方)
				每一个点往上 2 的 0 次方层的祖先已经在DFS函数内实现
				现在从 1 次方开始,每个结点的 1 次方先实现,再实现每个结点 2 次方
				以此类推,所以次方数的 j 在外层,内层是所有结点
				每一个次方数都得在所有结点实现后,再进行下一个次方级别的赋值 
			*/
		}
	}
}
**LCA查询函数**
int LCA(int x,int y)//传入询问的两个结点 
{
	if(deep[x]<deep[y]) swap(x,y);//保证询问时 x 的深度比 y 的深度大 
	for(int i=18;i>=0;--i)//循环的目的是让 x 和 y 处于同深度 
	{//18只是一个初始化的习惯性数值,2的18次方层的数结点已经够多了 
		if(deep[father[x][i]]>=deep[y]) x=father[x][i];
		/*
			从深层开始找 
			如果 x 往上 2 的 i 次方层的祖先的深度,大于等于 y 的深度的话
			x 就移动到它的 2 的 i 次方深度的祖先
			从满足深度的大幅度开始调整
			2 的 n 次方……16 8 4 2 1选择移动或不移动,可以实现 2 的 n 次方-1所有层数的移动 
		*/
	}
	//现在 x 和 y 处于同一层(同一深度)啦 
	if(x==y) return x;//令 x 与 y 同深度时,如果 x 和 y 相等,那么这就是LCA的答案,否则继续 
	for(int i=18;i>=0;--i)// x 和 y 一起往上移动 
	{
		if(father[x][i]!=father[y][i])
		{//此次向上 2 的 i 次方层,找到的祖先不一致,x 和 y 都移动到找到的祖先位置(赋值给 x y) 
			x=father[x][i];
			y=father[y][i];
		}
		/*
			如果发现往上移动 n 层祖先一致了,那就把 i 自减接着遍历,用更少的层数往上移动
			直到最后i==0的时候,只移动一层,father就相等,这个时候循环彻底结束,输出x和y的父结点就行
			此时的 x 和 y 已经都被移动到LCA答案结点的子结点位置 
		*/
	}
	return father[x][0] ;
}
图解算法(LCA查询部分:移到同层+移到同父)

在这里插入图片描述

例题 HDU-2586-How far away ?

Description
There are n houses in the village and some bidirectional roads connecting them. Every day peole always like to ask like this “How far is it if I want to go from house A to house B”? Usually it hard to answer. But luckily int this village the answer is always unique, since the roads are built in the way that there is a unique simple path(“simple” means you can’t visit a place twice) between every two houses. Yout task is to answer all these curious people.

Input
First line is a single integer T(T<=10), indicating the number of test cases.
For each test case,in the first line there are two numbers n(2<=n<=40000) and m (1<=m<=200),the number of houses and the number of queries. The following n-1 lines each consisting three numbers i,j,k, separated bu a single space, meaning that there is a road connecting house i and house j,with length k(0<k<=40000).The houses are labeled from 1 to n.
Next m lines each has distinct integers i and j, you areato answer the distance between house i and house j.

Output
For each test case,output m lines. Each line represents the answer of the query. Output a bland line after each test case.

Sample Input
2
3 2
1 2 10
3 1 15
1 2
2 3
2 2
1 2 100
1 2
2 1

Sample Output
10
25
100
100

AC代码(模板详见代码行注释)
#include<stdio.h>
#include<string.h>
#include <set>
#include <map>
#include<vector>
#include<algorithm>
#define fzhead EDGE(int _from,int _to,int _w,int _next)
#define fzbody from(_from),to(_to),w(_w),next(_next)
using namespace std;
const int maxn=1e5+10;
int T,n,m,x,y;
int head[maxn],edgecount,father[maxn][20],deep[maxn];
//father数组表示每个点往上2的次方层的祖先 
long long ans,dis[maxn];//dis是从根结点到i结点的权值总路程 
bool vis[maxn],yes[maxn];//vis标记结点是否已被访问,yes用于寻找根结点 
void init()
{
	memset(head,-1,sizeof(head));
	memset(dis,0,sizeof(dis));
	edgecount=-1;
	for(int i=1;i<=n;++i) vis[i]=yes[i]=false,father[i][0]=i;
}
struct EDGE
{
	int from,to,next;
	long long w;
/*
	Edge(){}是个用来给变量初始化0的函数
	fzhead:fzbody{}为结构体赋初值
*/
	EDGE(){}//后面别加分号 
	fzhead:fzbody{}//后面别加分号 
}edge[maxn];
/*
	i:边的编号;从from到to;w:权值,此处已删除;next:下一条边的编号
	head[i]:由i出发的第一条边的编号
	仅插入一条从u1开始的边,则next是-1,head[u1]为该边的编号
	再读入u1开始的边,则其为第一条边,next是上一条边的编号 
*/
void addEdge(int from,int to,int w)//链序和读入顺序相反 
{
	edge[++edgecount]=EDGE(from,to,w,head[from]);
	head[from]=edgecount;
}
void DFS(int root, int d)//处理每个结点的深度 and 寻找每个点的父结点 
{//d是初始根结点的上一层深度,因为有deep[root]=++d;赋值前根结点深度也加了1,初始深度一般算作1 
	vis[root]=true;
	deep[root]=++d;//深度递增 
	for(int i=head[root];i!=-1;i=edge[i].next)//递归出口就是到达叶子结点 
	{//链式前向星遍历,遍历的是root的子结点们,子结点的子结点在递归中遍历 
		if(vis[edge[i].to]==false)
		{//无向图 1 到 2 一条边,还有 2 到 1 一条边,不能重复访问,有向图不会出现 
			father[edge[i].to][0]=root;//子孙节点往上2的0次方层(1层)的祖先为父结点 
			dis[edge[i].to]=dis[root]+edge[i].w;
			DFS(edge[i].to,d);
		}//上文的++d先自加后赋值可以使得到此的d就是这层深度,也是对下一层而言的上一层深度 
	}
}
void LCAinit()
{
	for(int j=1;j<=18;++j)
	{
		for(int i=1;i<=n;++i)
		{//思想:它的爷爷是它爹的爹 
			father[i][j]=father[father[i][j-1]][j-1];
			/*
				i 往上 2 的 j 次方层的祖先
				就是 i 往上 2 的 j-1 次方层
				再往上 2 的 j-1 次方层的祖先 
				显然 2 的 j 次方 等于 2 的 j-1 次方 *2(两次 j-1 次方)
				每一个点往上 2 的 0 次方层的祖先已经在DFS函数内实现
				现在从 1 次方开始,每个结点的 1 次方先实现,再实现每个结点 2 次方
				以此类推,所以次方数的 j 在外层,内层是所有结点
				每一个次方数都得在所有结点实现后,再进行下一个次方级别的赋值 
			*/
		}
	}
}
int LCA(int x,int y)//传入询问的两个结点 
{
	if(deep[x]<deep[y]) swap(x,y);//保证询问时 x 的深度比 y 的深度大 
	for(int i=18;i>=0;--i)//循环的目的是让 x 和 y 处于同深度 
	{//18只是一个初始化的习惯性数值,2的18次方层的数结点已经够多了 
		if(deep[father[x][i]]>=deep[y]) x=father[x][i];
		/*
			从深层开始找 
			如果 x 往上 2 的 i 次方层的祖先的深度,大于等于 y 的深度的话
			x 就移动到它的 2 的 i 次方深度的祖先
			从满足深度的大幅度开始调整
			2 的 n 次方……16 8 4 2 1选择移动或不移动,可以实现 2 的 n 次方-1所有层数的移动 
		*/
	}
	//现在 x 和 y 处于同一层(同一深度)啦 
	if(x==y) return x;//令 x 与 y 同深度时,如果 x 和 y 相等,那么这就是LCA的答案,否则继续 
	for(int i=18;i>=0;--i)// x 和 y 一起往上移动 
	{
		if(father[x][i]!=father[y][i])
		{//此次向上 2 的 i 次方层,找到的祖先不一致,x 和 y 都移动到找到的祖先位置(赋值给 x y) 
			x=father[x][i];
			y=father[y][i];
		}
		/*
			如果发现往上移动 n 层祖先一致了,那就把 i 自减接着遍历,用更少的层数往上移动
			直到最后i==0的时候,只移动一层,father就相等,这个时候循环彻底结束,输出x和y的父结点就行
			此时的 x 和 y 已经都被移动到LCA答案结点的子结点位置 
		*/
	}
	return father[x][0] ;
}
int main()
{
	scanf("%d",&T) ;
	while(T--)
	{
		scanf("%d %d",&n,&m);
		init();
		int from,to;
		long long w;
		for(int i=1;i<=n-1;++i)
		{
			scanf("%d %d %lld",&from,&to,&w);
			addEdge(from,to,w); 
			addEdge(to,from,w);
			yes[to]=true;
		}//初始化edgecount为-1,所以从0号边开始存入,但链式前向星遍历不受影响 
		DFS(1,0);//根结点深度从1开始,先给所有结点找好自己的父结点,然后进行下文循环 
		LCAinit();
		for(int i=1;i<=m;++i)
		{
			scanf("%d %d",&x,&y);//输入需查询的两个结点
			ans=dis[x]+dis[y]-dis[LCA(x,y)]*2;
			printf("%lld\n",ans);
		}
	}
	return 0;
}

Tarjan算法(离线:输入一堆统一查询)

【存图】
  • 链式前向星(结构体存起始位置、边权等信息)
  • vector邻接表(结构体存终点、边权等信息,下标是起点)
【函数】(样例使用链式前向星存图)
  • 初始化函数(init函数)
    对链式前向星、输入数组、计数变量、标记数组、祖先结点数组初始化以及询问vector清空
  • 并查集函数(find函数+baba函数)
    后续用于查找祖先+合并实现祖先传递
  • 链式前向星函数(EDGE结构体及其内部赋值函数+addEdge函数)
    构造链式前向星,输入的时候直接用addEdge函数加边存储,后续用于遍历
  • Tarjan实现LCA查询函数(tarjan函数)
    深度搜索(DFS)遍历访问 and 递归逆向返回进行baba函数合并传递祖先
**初始化函数**
void init()
{
	memset(head,-1,sizeof(head));
	memset(ans,0,sizeof(ans));
	edgecount=-1;
	for(int i=1;i<=n;++i) vis[i]=yes[i]=false,father[i]=i,qst[i].clear();
}
**并查集函数**
int find(int x) {return x==father[x]?x:father[x]=find(father[x]);}
void baba(int x,int y) {int fx=find(x);int fy=find(y);father[fx]=fy;}
**链式前向星函数**
struct EDGE
{
	int from,to,next;
/*
	Edge(){}是个用来给变量初始化0的函数
	fzhead:fzbody{}为结构体赋初值
*/
	EDGE(){}//后面别加分号 
	fzhead:fzbody{}//后面别加分号 
}edge[maxn];
/*
	i:边的编号;从from到to;w:权值,此处已删除;next:下一条边的编号
	head[i]:由i出发的第一条边的编号
	仅插入一条从u1开始的边,则next是-1,head[u1]为该边的编号
	再读入u1开始的边,则其为第一条边,next是上一条边的编号 
*/
void addEdge(int from,int to)//链序和读入顺序相反 
{
	edge[++edgecount]=EDGE(from,to,head[from]);
	head[from]=edgecount;
}
**Tarjan实现LCA查询函数**
void tarjan(int root)
{
	vis[root]=true;//遍历到的根结点标记已被访问 
	for(int i=head[root];i!=-1;i=edge[i].next)//递归出口就是到达叶子结点 
	{//链式前向星遍历,遍历的是root的子结点们,子结点的子结点在递归中遍历 
		if(vis[edge[i].to]==false)
		{//无向图 1 到 2 一条边,还有 2 到 1 一条边,不能重复访问,有向图不会出现 
			tarjan(edge[i].to);//递归中第二个循环也会经历,把当前结点看做根结点 
			baba(edge[i].to,root);//合并到祖宗 
		}
		/*
			合并前提是所有子结点都已返回
			不然退出if还会继续进行i=edge[i].next遍历 
			不足以彻底让子结点们的父结点if内的tarjan结束 
		*/
	}
	for(int i=0;i<qst[root].size();++i)
	{//遍历询问,与root相关的有qst[root].size()个询问 
		if(vis[qst[root][i].first]==true)//注意vector第一维确定,第二维可变长 
		{//qst[root][i]是与root相关的第i个询问,first是相关结点,second是询问序号 
			ans[qst[root][i].second]=find(qst[root][i].first);
		}//若相关结点已经被遍历到,就用相关结点的祖先,作为这个second序号LCA询问的answer 
	}
}
图解算法(LCA查询部分:顺搜逆回)

在这里插入图片描述

例题 POJ-1330-Nearest Common Ancestors

Description
A rooted tree is a well-known data structure in computer science and engineering. An example is shown below:
在这里插入图片描述
In the figure, each node is labeled with an integer from {1, 2,…,16}. Node 8 is the root of the tree. Node x is an ancestor of node y if node x is in the path between the root and node y. For example, node 4 is an ancestor of node 16. Node 10 is also an ancestor of node 16. As a matter of fact, nodes 8, 4, 10, and 16 are the ancestors of node 16. Remember that a node is an ancestor of itself. Nodes 8, 4, 6, and 7 are the ancestors of node 7. A node x is called a common ancestor of two different nodes y and z if node x is an ancestor of node y and an ancestor of node z. Thus, nodes 8 and 4 are the common ancestors of nodes 16 and 7. A node x is called the nearest common ancestor of nodes y and z if x is a common ancestor of y and z and nearest to y and z among their common ancestors. Hence, the nearest common ancestor of nodes 16 and 7 is node 4. Node 4 is nearer to nodes 16 and 7 than node 8 is.
For other examples, the nearest common ancestor of nodes 2 and 3 is node 10, the nearest common ancestor of nodes 6 and 13 is node 8, and the nearest common ancestor of nodes 4 and 12 is node 4. In the last example, if y is an ancestor of z, then the nearest common ancestor of y and z is y.
Write a program that finds the nearest common ancestor of two distinct nodes in a tree.

Input
The input consists of T test cases. The number of test cases (T) is given in the first line of the input file. Each test case starts with a line containing an integer N , the number of nodes in a tree, 2<=N<=10,000. The nodes are labeled with integers 1, 2,…, N. Each of the next N -1 lines contains a pair of integers that represent an edge --the first integer is the parent node of the second integer. Note that a tree with N nodes has exactly N - 1 edges. The last line of each test case contains two distinct integers whose nearest common ancestor is to be computed.

Output
Print exactly one line for each test case. The line should contain the integer that is the nearest common ancestor.

Sample Input
2
16
1 14
8 5
10 16
5 9
4 6
8 4
4 10
1 13
6 15
10 11
6 7
10 2
16 3
8 1
16 12
16 7
5
2 3
3 4
3 1
1 5
3 5

Sample Output
4
3

AC代码(模板详见代码行注释)
#include<stdio.h>
#include<string.h>
#include <set>
#include <map>
#include<vector>
#include<algorithm>
#define fzhead EDGE(int _from,int _to,int _next)
#define fzbody from(_from),to(_to),next(_next)
using namespace std;
const int maxn=1e4+10;
int head[maxn],edgecount,father[maxn],ans[maxn];
bool vis[maxn],yes[maxn];//vis标记结点是否已被访问,yes用于寻找根结点 
int T,n,m,s,cnt=0;
vector<pair<int,int> > qst[maxn];
//下标是询问结点,first是相关结点,second是询问序号 
void init()
{
	memset(head,-1,sizeof(head));
	memset(ans,0,sizeof(ans));
	edgecount=-1;
	for(int i=1;i<=n;++i) vis[i]=yes[i]=false,father[i]=i,qst[i].clear();
}
int find(int x) {return x==father[x]?x:father[x]=find(father[x]);}
void baba(int x,int y) {int fx=find(x);int fy=find(y);father[fx]=fy;}
struct EDGE
{
	int from,to,next;
/*
	Edge(){}是个用来给变量初始化0的函数
	fzhead:fzbody{}为结构体赋初值
*/
	EDGE(){}//后面别加分号 
	fzhead:fzbody{}//后面别加分号 
}edge[maxn];
/*
	i:边的编号;从from到to;w:权值,此处已删除;next:下一条边的编号
	head[i]:由i出发的第一条边的编号
	仅插入一条从u1开始的边,则next是-1,head[u1]为该边的编号
	再读入u1开始的边,则其为第一条边,next是上一条边的编号 
*/
void addEdge(int from,int to)//链序和读入顺序相反 
{
	edge[++edgecount]=EDGE(from,to,head[from]);
	head[from]=edgecount;
}
void tarjan(int root)
{
	vis[root]=true;//遍历到的根结点标记已被访问 
	for(int i=head[root];i!=-1;i=edge[i].next)//递归出口就是到达叶子结点 
	{//链式前向星遍历,遍历的是root的子结点们,子结点的子结点在递归中遍历 
		if(vis[edge[i].to]==false)
		{//无向图 1 到 2 一条边,还有 2 到 1 一条边,不能重复访问,有向图不会出现 
			tarjan(edge[i].to);//递归中第二个循环也会经历,把当前结点看做根结点 
			baba(edge[i].to,root);//合并到祖宗 
		}
		/*
			合并前提是所有子结点都已返回
			不然退出if还会继续进行i=edge[i].next遍历 
			不足以彻底让子结点们的父结点if内的tarjan结束 
		*/
	}
	for(int i=0;i<qst[root].size();++i)
	{//遍历询问,与root相关的有qst[root].size()个询问 
		if(vis[qst[root][i].first]==true)//注意vector第一维确定,第二维可变长 
		{//qst[root][i]是与root相关的第i个询问,first是相关结点,second是询问序号 
			ans[qst[root][i].second]=find(qst[root][i].first);
		}//若相关结点已经被遍历到,就用相关结点的祖先,作为这个second序号LCA询问的answer 
	}
}
int main()
{
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d",&n);//一共n个结点,n-1条边 
		init();//初始化用到了n,一定要在输入后再初始化鸭!!! 
		for(int i=1,from,to;i<=n-1;++i)
		{
			scanf("%d %d",&from,&to);
			addEdge(from,to); 
			yes[to]=true;
		}//初始化edgecount为-1,所以从0号边开始存入,但链式前向星遍历不受影响 
		int root;
		for(int i=1;i<=n;++i)
		{//所有的to都成了true,最后是false的只有唯一一个from 
			if(yes[i]==false)
			{
				root=i;
				break;
			}
		}
		m=1;//询问个数,此处仅出入一例 
		int putfrom,putto;
		scanf("%d %d",&putfrom,&putto);
		//对于1号询问,putfrom和putto都可能是询问结点,另一个为相关结点 
		qst[putfrom].push_back(make_pair(putto,1)); 
		qst[putto].push_back(make_pair(putfrom,1)); 
		//vector第二维下标自动从0开始存,所以函数内部第二个遍历i=0开始 
		tarjan(root);//传入根结点序号
		//for(int i=1;i<=m;++i) printf("%d\n",ans[i]);此处仅1例
		printf("%d\n",ans[1]);
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Tarjan算法是一种用于求解最近公共祖(Least Common Ancestors,LCA)问题的离线算法算法的核心思想是利用深度优先搜索(DFS)和并查集(Union Find)来解决问题。 首先,我们从根节点开始遍历每一个节点,并将节点分为三类,用st[]数组表示。0代表还未被遍历,1代表正在遍历这个点,2代表已经遍历完这个点并且回溯回来了。这样的划分有助于确定节点的最近公共祖先。 在Tarjan算法中,我们一边遍历一边回应查询。每当遍历到一个节点时,我们查找与该节点相关的所有查询。如果查询中的节点已经被遍历完(即st[]值为2),我们可以利用已经计算好的信息来计算它们的最近公共祖先最近公共祖先的距离可以通过两个节点到根节点的距离之和减去最近公共祖先节点到根节点的距离来计算。 在Tarjan算法中,我们可以通过深度优先搜索来计算dist[]数组,该数组表示每个节点到根节点的距离。我们可以利用父节点到根节点的距离加上边的权值来计算每个节点到根节点的距离。 最后,我们可以通过并查集来操作st[]数组。当遍历完一个节点的所有子树后,将子树中的节点放入该节点所在的集合。这样,每个子树的节点的最近公共祖先都是该节点。 综上所述,Tarjan算法利用DFS和并查集来求解最近公共祖先问题。它的时间复杂度为O(n+m),其中n是节点数,m是查询次数。通过该算法,我们可以高效地解决最近公共祖先的问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [最近公共祖先之tarjan](https://blog.csdn.net/qq_63092029/article/details/127737575)[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^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [【模版】Tarjan离线算法最近公共祖先(LCA)](https://blog.csdn.net/weixin_43359312/article/details/100823178)[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^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [Tarjan算法求解最近公共祖先问题](https://blog.csdn.net/Yeluorag/article/details/48223375)[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^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值