节点的最近公共祖先

4 篇文章 0 订阅
2 篇文章 0 订阅

树是一种很常见的数据结构。现在小明面临一个问题,在一个有 n个节点的树上,节点编号分别是
1…n。小明想知道一些节点之间的最近公共祖先是那些节点。  输入格式:第一行输入一个整数 n(2≤n≤10,000),表示树上有 n个节点。  接下来的 n−1 行,每行输入俩个整数 a,b(1≤a,b≤n)代表节点 a,b 之间有一条 a 到 b 边,a 是 b 的父亲。  接下来输入一个整数 q,代表小明的 q 次提问。(1≤q≤1,000)  接下来的 q 行,每行输入俩个整数 c,d(1≤c,d≤n)代表询问 c,d 俩个节点的最近公共祖先。  输出格式:对于每次询问,输出公共祖先的节点编号,占一行。
 样例输入
 5
 1 2
 2 3
 1 4
 2 5
 2
 3 4
 3 5
 样例输出
 1
 2

这道题属于图论里的LCA(least common ancestor)问题,如我我们暴力求解的话时间复杂度就是O(V);但是如果用倍增算法的话可以把一次的查询复杂度就会降到O(log V)

首先什么是倍增算法?
倍增解法的核心是分治思想。当已知两个点在树中的深度时,先让较深的结点
向上走,直到两个结点深度一样;再二分找出离他们最近的公共祖先。我们记
一个结点的父结点为它的 20=1 倍祖先,它的父结点的父结点为它的 2 倍祖先,
以此类推。
倍增算法的具体流程第一步:
一、在树上预处理每个结点的深度和 1倍祖先,也就是每个结点的父结点。用d数组来表示每个
结点的深度,p[v][h]表示结点 v的 h倍祖先的结点编号。d数组中的元素初始为 −1。初始化完
成后,p[a][0]保存的是a的第 20=1 倍祖先结点,即它的父结点。时间复杂度 O(V)。

const int MAX_N = 100000; 
const int MAX_M = 1000000; 
int d[MAX_N], p[MAX_N][20]; 
void init() {
     memset(d, -1, sizeof(d)); }
void dfs(int node) {
for (int i = f[node]; i != -1; i = e[i].next) { 
     if (d[e[i].v] == -1) {
     d[e[i].v] = d[node] + 1;
     p[e[i].v][0] = node;
     dfs(e[i].v); 
     }
  }
  }

倍增算法的具体流程第二步:
 二、倍增计算各个点的 ??祖先是谁,其中,1倍祖先就是父亲,2倍祖先是父亲
的父亲,以此类推。该点?? 祖先也就是该点 ??−?祖先的 ??−? 祖先。
 for (int level = 1; (1 << level) <= n; level++) {
 for (int i = 1; i <= n; i++) {
 p[i][level] = p[p[i][level - 1]][level - 1]; // i 的第 2^j 祖先就是 i 的
//第 2^(j-1) 祖先的第 2^(j-1) 祖先
 }
 }
 在这一步中,??????表示的是祖先的倍数。很显然,i 的 ??倍祖先就是i 的 ??−?
倍祖先的 ??−?倍祖先。时间复杂度为O(VlogV)。
倍增算法的具体流程第三步:
在这里插入图片描述
这是我们老师的代码,在许多地方都用了移位来优化。
下面贴我的代码

#include <iostream>
#include <string.h>
using namespace std;
const int MAX_N=1000;
const int MAX_M=10000;
int head[MAX_N];
bool visit[MAX_N];
int depth[MAX_N];//记录深度
int fa[MAX_N][21];//记录i节点的2^j倍祖先节点的编号
int ans=0;
struct edge{
    int to;
    int next;
}eid[MAX_M];

void insert(int u,int v){
    eid[ans].to=v;
    eid[ans].next=head[u];
    head[u]=ans++;
}

inline int read(){
    int s=0,w=1;
    char ch=getchar();
    while (ch<'0' || ch> '9'){
        if(ch=='-')
            w=-1;
        ch=getchar();
    }
    while (ch>='0' && ch<='9'){
        s=10*s+ch-'0';
        ch=getchar();
    }
    return s*w;
}//快读,等会再介绍一下快读

void swap(int& a,int& b){
    int temp=a;
    a=b;
    b=temp;
}

//这里用深度搜索来预处理深度和fa[i][0]的信息。这里用了visit[]数组来记录有没有搜索过。因为我们的边是无向边,
//但是这道题没有必要因为他给的信息都是小的点作为父节点的。
void dfs(int now,int dep,int fat){
    if(visit[now]) return;//1
    visit[now]=true;//2 这两句可要可不要
    depth[now]=dep;
    fa[now][0]=fat;//now节点的父亲节点是fat
    for(int i=head[now];i!=-1;i=eid[i].next){
        dfs(eid[i].to,dep+1,now);
    }
}

int lca(int x,int y){
    if(depth[x]<depth[y])
        swap(x,y);//让深的节点为X
    for(int i=20;i>=0;--i){
        if(fa[x][i]!=0 && depth[fa[x][i]]>=depth[y]){
            x=fa[x][i];
        }
    }//这里相当于二进制的位选择,从第20位( 2^20)一直到第0位(2^0)进行选择跳跃,【哪些位被选择了呢?就是Y与X的深度差值的二进制中的那些为1的位】最后的结果就是X与Y的深度变成了相同
    
    if(x==y) return x;
    for(int i=20;i>=0;--i){
        if(fa[x][i]!=0 && fa[x][i]!=0 && fa[x][i]!=fa[y][i]){
            x=fa[x][i];
            y=fa[y][i];
        }
    }
    return fa[x][0];
}
//当他们的点相同时说明刚好都到达了最近公共祖先的点,如果没有到则再进行二分跳跃。如果其中一个点跳到了根节点或者以上为0的时候,或者两个相同(意味着跳过了)则选择跳过当次迭代。

int main() {
    memset(head,-1,sizeof(head));
    int n=read();
    for(int i=0;i<n-1;++i){
        int x,y;
        x=read();
        y=read();
        insert(x,y);
        insert(y,x);
    }
    dfs(1,1,0);
    for(int j=1;j<=20;++j){
        for(int i=1;i<=n;++i){
            fa[i][j]=fa[fa[i][j-1]][j-1];
        }
    }
    int q;
    q=read();
    for(int i=0;i<q;++i){
        int x,y;
        x=read();
        y=read();
        printf("%d\n",lca(x,y));
    }
    return 0;
}

这讲一下快读和快写。
首先呢 无非就是cin 因为比较好用 简单
cin>>x;
1
然后听老师 说要用scanf() 读入更快 在省赛里面啥的很有用
scanf()
1
然后在某一篇题解里面看见 ios::sync_with_stdio(false) 可以加速(也就是关闭同步流)
cin慢是有原因的,其实默认的时候,cin与stdin总是保持同步的,也就是说这两种方法可以混用,而不必担心文件指针混乱,同时cout和stdout也一样,两者混用不会输出顺序错乱。正因为这个兼容性的特性,导致cin有许多额外的开销,如何禁用这个特性呢?只需一个语句std::ios::sync_with_stdio(false);,这样就可以取消cin于stdin的同步了。
所以在主函数里面(当然是在输出前面)加了一句
ios::sync_with_stdio(false);
1
再然后就是平时做题 容易TLE 就发现了一种高端的东西叫做快读 也就是read
什么是快读
我们知道,scanf比cin快,在读入量大的时候便可以优化时间复杂快.
快读就是一种读入很快的读入方法,可以优化时间复杂度.
快读很快,比cin与scanf都快,可以极大优化时间复杂度
为什么要用快读
1.我们知道cin和scanf不能混用,但在保留小数的时候不得不用printf,我们知道cin和printf不能混用,这时候便可以用快读read,再用printf
2.很快…….过会儿在举例中可以得到
快读为什么很快
int读是很慢的,我们需要用更快的字符的读入方法getchar进行读入
快读原理
用字符读入,再转换成数字,最后输出.
templateinline void read(t&x) {
x=0;
int f=1;
char ch=getchar();
while(!isdigit(ch)) {
if(ch=’-’) f=-1;
ch=getchar();
}
while(isdigit(ch)) x=(x<<1)+(x<<3)+(ch-48),ch=getchar();
x*=f;
}

然后献上模板
#include <bits/stdc++.h>
#define f(i,j,n) for(register int i=j;i<=n;i++)
#define openfile freopen(".in",“r”,stdin),freopen(".out",“w”,stdout);
#define closefile fclose(stdin),fclose(stdout);
using namespace std;
typedef long long ll;
templateinline void read(t&x) {
x=0;
int f=1;
char ch=getchar();
while(!isdigit(ch)) {
if(ch=’-’) f=-1;
ch=getchar();
}
while(isdigit(ch)) x=(x<<1)+(x<<3)+(ch-48),ch=getchar();
x*=f;
}
signed main() {
//openfile

//closefile
return 0;

}

作者:清风ღ
来源:CSDN
原文:https://blog.csdn.net/qq_42628055/article/details/85177064
版权声明:本文为博主原创文章,转载请附上博文链接!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值