首先,众所周知,求LCA共有3种算法(树剖就不说了,太高级,以后再学。。)。

1、树上倍增(ST表优化)

2、RMQ&时间戳(ST表优化)

3、tarjan(离线算法)不讲。。(后面补坑啦!)

一、树上倍增

这种方法原理是这样的:

我们可以知道,最朴素的算法就是一步一步的并查集往上找,知道找到2个点并查集相同,即为LCA

但这种算法的时间效率为O(NM)看到0<n,m<5*10^5我们就知道一定会炸。

但是,我们可以发现给出树后,每个点的LCA及走到LCA的路径一定是固定的。

所以可以ST表优化。

首先先BFS出每个点在树上的深度。。(记为depth[i])

接着我们要先让2个点的深度相同,之后2个点一起走,可以加快效率。

最后我们直接倍增上去。(if(fa[i][u]!=fa[i][v])u=fa[i][u];v=fa[i][v];)

最后只要往上走一步,就是LCA了。

重点!倍增时倍增循环在外!不能在内!否则会挂!

下面贴代码(debug了三小时,才调好。膜拜zxyer)

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
struct data{
int next,to;
}g[];
int depth[];
int que[];
bool visit[];
int head[];
int fa[][];
int n,q,root,num=;
int lca(int u,int v){
if(depth[u]<depth[v])swap(u,v);
int dc=depth[u]-depth[v];
for(int i=;i<=;i++)
if((<<i)&dc&&fa[i][u]){
u=fa[i][u];
}
if(u==v)return u;
for(int i=;i>=;i--)
if(fa[i][u]!=fa[i][v]&&fa[i][u]){
u=fa[i][u];
v=fa[i][v];
}
return fa[][u];
}
void bfs(int u)
{
memset(que,,sizeof(que));
memset(visit,,sizeof(visit));
visit[u]=true;
que[]=u;
int h=,l=;
while(h<=l)
{
int rt=que[h];
for (int i=head[rt];i;i=g[i].next)
{
int v=g[i].to;
if ( !visit[v] )
{
visit[v]=true;
depth[v]=depth[rt]+;
que[++l]=v;
}
}
h++;
}
}
int main(){
scanf("%d%d",&n,&q);
memset(g,,sizeof(g));
memset(fa,,sizeof(fa));
memset(depth,,sizeof(depth));
for(int i=;i<n;i++)
{
int f,t;
scanf("%d%d",&f,&t);
g[++num].next=head[f];
head[f]=num;
g[num].to=t;
fa[][t]=f;
if(fa[][f]==)root=f;
}
for(int j=;j<=;j++)
for(int i=;i<=n;i++)
{ fa[j][i]=fa[j-][fa[j-][i]];
}
depth[root]=;
bfs(root);
for(int i=;i<=q;i++){
{
int num1,num2;
scanf("%d%d",&num1,&num2);
printf("%d\n",lca(num1,num2));
}
}
}

二、RMQ+时间戳

这个算法理解了很久才懂

首先我们都知道如果把树看成一个无向图,那么LCA一定在u->v的最短路上。而且,LCA的点就是最短路上depth最小的点。

换句话说,就是LCA是2个点到根节点的路径的第一个交汇处。

接下来用dfs为点标号,用id[i]表示这个点第一次出现在顶点序列的标号。

接下来就是求id[u]<i<id[v]中depth的最小值啦!

这个过程可以用RMQ高速解决。

所以LCA=RMQ(depth)(u,v)

注意!这个算法比较难懂,可以先看看RMQ,理解之后再画画图,恩,就差不多了。

下面贴代码

#include <cstdio>

#include <cstring>
#include <queue>
#include <algorithm>
#define MAXN 1010
#define MAXM 100000
using namespace std;
struct Edge
{
int from, to, next;
};
Edge edge[MAXM];
int head[MAXN], edgenum;
int vs[MAXN<<];//第i次DFS访问节点的编号
int depth[MAXN<<];//第i次DFS访问节点的深度
int id[MAXN];//id[i] 记录在vs数组里面 i节点第一次出现的下标
int dfs_clock;//时间戳
int N, M, Q;//点数 边数 查询数
int dp[MAXN<<][];//dp[i][j]存储depth数组 以下标i开始的,长度为2^j的区间里 最小值所对应的下标
void init()
{
edgenum = ;
memset(head, -, sizeof(head));
}
void addEdge(int u, int v)
{
Edge E = {u, v, head[u]};
edge[edgenum] = E;
head[u] = edgenum++;
}
void getMap()
{
int a, b;
while(M--)
scanf("%d%d", &a, &b),
addEdge(a, b), addEdge(b, a);
}
void DFS(int u, int fa, int d)//当前遍历点以及它的父节点 遍历点深度
{
id[u] = dfs_clock;
vs[dfs_clock] = u;
depth[dfs_clock++] = d;
for(int i = head[u]; i != -; i = edge[i].next)
{
int v = edge[i].to;
if(v == fa) continue;
DFS(v, u, d+);
vs[dfs_clock] = u;//类似 回溯
depth[dfs_clock++] = d;
}
}
void find_depth()
{
dfs_clock = ;
memset(vs, , sizeof(vs));
memset(id, , sizeof(id));
memset(depth, , sizeof(depth));
DFS(, -, );//遍历
}
void RMQ_init(int NN)//预处理 区间最小值
{
for(int i = ; i <= NN; i++)
dp[i][] = i;
for(int j = ; (<<j) <= NN; j++)
{
for(int i = ; i + (<<j) - <= NN; i++)
{
int a = dp[i][j-];
int b = dp[i + (<<(j-))][j-];
if(depth[a] <= depth[b])
dp[i][j] = a;
else
dp[i][j] = b;
}
}
}
int query(int L, int R)
{
//查询L <= i <= R 里面使得depth[i]最小的值 返回对应下标
int k = ;
while((<<(k+)) <= R-L+) k++;
int a = dp[L][k];
int b = dp[R - (<<k) + ][k];
if(depth[a] <= depth[b])
return a;
else
return b;
}
int LCA(int u, int v)
{
int x = id[u];//比较大小 小的当作左区间 大的当作右区间
int y = id[v];
if(x > y)
return vs[query(y, x)];
else
return vs[query(x, y)];
}
void solve()
{
int a, b;
while(Q--)
{
scanf("%d%d", &a, &b);
printf("LCA(%d %d) = %d\n", a, b, LCA(a, b));
}
}
int main()
{
while(scanf("%d%d%d", &N, &M, &Q) != EOF)
{
init();
getMap();
find_depth();//DFS遍历整个树 求出所需要的信息
RMQ_init(dfs_clock - );
solve();
}
return ;
} 重点同上哈!倍增写外面!

三、tarjan算法求LCA

这个算法和之前的tarjan求强联通分量有一些差别(只是名字一样而已)

这个算法非常妙啊,其实就是从根节点dfs标记父节点,但是厉害的地方在于,在它标记完根节点后,恰好能够更新答案。

这个算法比较绕,网上有很多的图解,我就不贴了,我们来具体算法理解。

下面是tarjan的主体,h就是链表的头。。这个不讲,这里讲一下q数组,它用于存储询问,g[i].to表示他要查询的第二个节点。

void tarjan(int x){
for(int i=h[x];i;i=g[i].next)
tarjan(g[i].to),f[g[i].to]=x;
for(int i=q[x];i;i=g[i].next)
ans[g[i].to]=ans[g[i].to]>?getfa(ans[g[i].to]):x;
}

很显然,在回溯后,我们计算答案,如果没有出现g[i].to的答案,我们就将他标记为x,这是什么意思呢?其实如果标记为X,就说明在dfs时先搜到了x,再搜g[i].to,显然x即为这2者的公共祖先。

但是如果我们已经更新过ans[g[i].to],其实这说明了两个点不在同一子树上,所以我们要找到后一个节点的父亲。(重点来了!)

我们在做getfather的操作的时候,由于是先序遍历,所以我们做到这个dfs时,寻找父亲只会找到他们的公共祖先然后停止。是不是很妙?

嗯对,所以是不是很简单?

#include<iostream>
#include<cstdio>
using namespace std;
int h[],q[];
bool fa[];
int f[];
int ans[];
int n,m,num=;
struct edge{
int to,next;
}g[];
int getfa(int x){return f[x]?f[x]=getfa(f[x]):x;}
void ins(int *h,int u,int v){g[++num].next=h[u];h[u]=num;g[num].to=v;}
void tarjan(int x){
for(int i=h[x];i;i=g[i].next)
tarjan(g[i].to),f[g[i].to]=x;
for(int i=q[x];i;i=g[i].next)
ans[g[i].to]=ans[g[i].to]>?getfa(ans[g[i].to]):x;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=;i<n;i++){
int x,y;
scanf("%d%d",&x,&y);
ins(h,x,y);
fa[y]=;
}
for(int i=;i<m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
ins(q,x,i);ins(q,y,i);
}
for(int i=;i<=n;i++)if(!fa[i]){tarjan(i);break;}
for(int i=;i<m;i++)printf("%d\n",ans[i]);
return ;
}

算法详解(LCA&RMQ&tarjan)补坑啦!完结撒花(。◕ˇ∀ˇ◕)的更多相关文章

  1. Tarjan算法详解

    Tarjan算法详解 今天偶然发现了这个算法,看了好久,终于明白了一些表层的知识....在这里和大家分享一下... Tarjan算法是一个求解极大强联通子图的算法,相信这些东西大家都在网络上百度过了, ...

  2. 安全体系(三)——SHA1算法详解

    本文主要讲述使用SHA1算法计算信息摘要的过程. 安全体系(零)—— 加解密算法.消息摘要.消息认证技术.数字签名与公钥证书 安全体系(一)—— DES算法详解 安全体系(二)——RSA算法详解 为保 ...

  3. 算法进阶面试题01——KMP算法详解、输出含两次原子串的最短串、判断T1是否包含T2子树、Manacher算法详解、使字符串成为最短回文串

    1.KMP算法详解与应用 子序列:可以连续可以不连续. 子数组/串:要连续 暴力方法:逐个位置比对. KMP:让前面的,指导后面. 概念建设: d的最长前缀与最长后缀的匹配长度为3.(前缀不能到最后一 ...

  4. BM算法  Boyer-Moore高质量实现代码详解与算法详解

    Boyer-Moore高质量实现代码详解与算法详解 鉴于我见到对算法本身分析非常透彻的文章以及实现的非常精巧的文章,所以就转载了,本文的贡献在于将两者结合起来,方便大家了解代码实现! 算法详解转自:h ...

  5. kmp算法详解

    转自:http://blog.csdn.net/ddupd/article/details/19899263 KMP算法详解 KMP算法简介: KMP算法是一种高效的字符串匹配算法,关于字符串匹配最简 ...

  6. 机器学习经典算法详解及Python实现--基于SMO的SVM分类器

    原文:http://blog.csdn.net/suipingsp/article/details/41645779 支持向量机基本上是最好的有监督学习算法,因其英文名为support vector  ...

  7. [转] KMP算法详解

    转载自:http://www.matrix67.com/blog/archives/115 KMP算法详解 如果机房马上要关门了,或者你急着要和MM约会,请直接跳到第六个自然段.    我们这里说的K ...

  8. 【转】AC算法详解

    原文转自:http://blog.csdn.net/joylnwang/article/details/6793192 AC算法是Alfred V.Aho(<编译原理>(龙书)的作者),和 ...

  9. KMP算法详解(转自中学生OI写的。。ORZ!)

    KMP算法详解 如果机房马上要关门了,或者你急着要和MM约会,请直接跳到第六个自然段. 我们这里说的KMP不是拿来放电影的(虽然我很喜欢这个软件),而是一种算法.KMP算法是拿来处理字符串匹配的.换句 ...

随机推荐

  1. [CodeForces948D]Perfect Security(01字典树)

    Description 题目链接 Solution 01字典树模板题,删除操作用个数组记录下就行了 Code #include <cstdio> #include <algorith ...

  2. 正则表达式,regular expression, regex, RE

    正则表达式是用来简洁表达一组字符串的表达式 正则表达式可以用来判断某字符串的特征归属

  3. TouTiao开源项目 分析笔记15 新闻详情之两种类型的实现

    1.预览效果 1.1.首先看一下需要实现的效果. 第一种,文字类型新闻. 第二种,图片类型新闻. 1.2.在NewsArticleTextViewBinder中设置了点击事件 RxView.click ...

  4. 课后题2.87&2.86

    课后题2.86&2.87 单纯就是想加点分第十章的题目都被做过了就做下第二章的,正好复习一下前面学的知识,第二章给我剩下的题目也不多了,我就挑了这个题目. 2.86 考虑一个基于IEEE浮点格 ...

  5. Dapper.Extension的基本使用

    前言    上一篇随笔写了Dapper的简单的使用,这次写一下Dapper.Extension的使用,它是Dapper的简单的封装扩展,可以通过实例化的对象赋值后进行增删改的操作以及分页,但是却不能进 ...

  6. P1800 software_NOI导刊2010提高(06)(二分答案)

    P1800 software_NOI导刊2010提高(06) 题目描述 一个软件开发公司同时要开发两个软件,并且要同时交付给用户,现在公司为了尽快完成这一任务,将每个软件划分成m个模块,由公司里的技术 ...

  7. Django基本使用

    目录 1 安装 1.1 安装pip 1.2 安装django 2 创建项目 2.1 使用 管理工具 django-admin.py 来创建 PyLearn 项目: 2.2 启动服务 本文章以下所有列子 ...

  8. 无限小数转分数POJ1930分析

    将无限小数化为分数,有一套简单的公式.使其轻松表示出来. 循环节 例如:0.121212…… 循循环节为12.   公式 这个公式必须将循环节的开头放在十分位.若不是可将原数乘10^x(x为正整数) ...

  9. Pascal小游戏 井字棋

    一个很经典的井字棋游戏 Pascal源码Chaobs奉上 注意:1.有的FP版本不支持汉语,将会出现乱码.2.别想赢电脑了,平手不错了. 井字过三关: program TicTacToe; uses ...

  10. USACO刷题之路,开始了

    几天前,重新开始刷题了. 重新刷题有几个原因: 1.曾经的OI经历,如今除了悟性高些.知识多些,大多已经遗忘.不希望真的让之前的OI水平就这么丢了. 2.越来越觉得,刷题真的是一件很开心的事情.大学中 ...