一、向上标记法

比如求x,y的lca,我们先从x向上走到根节点,中途标记每个被走过的点,然后从y向上标记,直到遇到第一个被标记的点就是他们的lca了。(不适用于多次查询操作
时间复杂度:O(n)

二、树上倍增法

这种方法主要是利用了倍增的思想,不断地向上试探,同时利用dp不断转移到上面,然后即可求出lca,

/*
            树上倍增法求最近公共祖先
    时间复杂度: O ((n+m)log(n))
 1、d[x]表示x的深度,不妨设d[x]>=d[y](否则x,y交换)
 2、用二进制拆分的思想,把x向上调整到和y同一深度
 3、若此时x=y,则说明找到了LCA,LCA就是y
 4、用二进制拆分的思想,把x,y同时向上调整,变更且保持两者深度一致且不相会
 5、此时x,y必定只差一步就会相会了,此时他们的父结点f[x,0]就是LCA 
 f[x,k]表示x的2^k辈祖先,即从x向根结点走了2^k步到达的结点 
*/
#include<bits/stdc++.h>
using namespace std;
const int size=50010;
int T,n,m,tot,t;
int f[size][20],d[size],dist[size];
int ver[size*2],nxt[size*2],edge[size*2],head[size*2];
queue<int> q;
void add(int x,int y,int z){
    ver[++tot]=y,edge[tot]=z,nxt[tot]=head[x],head[x]=tot;
}
void bfs(){  //预处理 
    q.push(1),d[1]=1;
    while(q.size()) {
        int x=q.front();
        q.pop();
        for(int i=head[x];i;i=nxt[i]){
            int y=ver[i];
            if(d[y]) continue;
            d[y]=d[x]+1;  //y的结点深度加1 
            dist[y]=dist[x]+edge[i];
            f[y][0]=x;
            for(int j=1;j<=t;j++){
                f[y][j]=f[f[y][j-1]][j-1];  //不断更新各祖先 
            }
            q.push(y);
        }
    }
}
int lca(int x,int y){  //计算树上任意两点之间的距离 
    if(d[x]>d[y]) swap(x,y);  //令x的深度小于y的深度,即x更靠经公共祖先 
    for(int i=t;i>=0;i--){
        if(d[f[y][i]]>=d[x])  y=f[y][i];  //y不断向上进行调整 
    }
    if(x==y) return x;
    for(int i=t;i>=0;i--){
        if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];  //x不算进行向上调整 
    }
    return f[x][0];  //x向上调整1步即可得到lca 
}
int main(){
    cin>>T;
    while(T--){
        cin>>n>>m;
        t=(int)(log(n)/log(2))+1;
        for(int i=1;i<=n;i++) head[i]=d[i]=0;
        tot=0;
        for(int i=1;i<n;i++){
            int x,y,z;
            cin>>x>>y>>z;
            add(x,y,z);
            add(y,x,z);
        }
        bfs();
        //进行m次查询,查询任意两点之间的距离 
        for(int i=1;i<=m;i++){
            int x,y;
            cin>>x>>y;
            cout<<dist[x]+dist[y]-2*dist[lca(x,y)];
        }
    }
    return 0;
}

三、tarjan算法

该算法就是一个对向上标记法的一个优化,采用的是离线查询的操作,先记录下每次询问的操作,然后利用tarjan算法进行一个标记,同时开并查集进行一个优化。

//时间复杂度O(n+m)
#include<bits/stdc++.h> 
using namespace std;
const int size=5010;
int ver[size*2],nxt[size*2],head[size*2],edge[size*2];
int fa[size],d[size],v[size],lca[size],ans[size];
vector<int> query[size],query_id[size];
int T,n,m,tot,t;
void add(int x,int y,int z){
    ver[++tot]=y,edge[tot]=z;
    nxt[tot]=head[x],head[x]=tot;
}
void add_query(int x,int y,int id){  //记录查询情况 
    query[x].push_back(y),query_id[x].push_back(id);
    query[y].push_back(x),query_id[y].push_back(id);
}
int get(int x){  //寻找父结点 
    if(x==fa[x])  return x;
    return fa[x]=get(fa[x]);
}
void tarjan(int x){
    v[x]=1;  //开始递归,但是还没有回溯 
    for(int i=head[x];i;i=nxt[i]){
        int y=ver[i];
        if(v[y])  continue;
        d[y]=d[x]+edge[i];
        tarjan(y);
        fa[y]=x;   //标记父结点 
    }
    for(int i=0;i<query[x].size();i++){  //查询关于x的所有询问 
        int y=query[x][i],id=query_id[x][i];
        if(v[y]==2){  //这个结点是访问完毕并且回溯的结点 
            int lca=get(y);  //直接找到父结点 
            ans[id]=min(ans[id],d[x]+d[y]-2*d[lca]);
        }
    }
    v[x]=2;  //标记这个结点 
}
int main(){
    cin>>T;
    while(T--){
        cin>>n>>m;
        for(int i=1;i<=n;i++){  //清空 
            head[i]=0,fa[i]=i,v[i]=0;
            query[i].clear(),query_id[i].clear();
        }
        tot=0;
        for(int i=1;i<n;i++){
            int x,y,z;
            scanf("%d%d%d",&x,&y,&z);
            add(x,y,z),add(y,x,z);
        }
        for(int i=1;i<=m;i++){  //进行m次查询 
            int x,y;
            scanf("%d%d",&x,&y);
            if(x==y) ans[i]=0;
            else{
                add_query(x,y,i);
                ans[i]=1<<30;
            }
        }
        tarjan(1);
        for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
    }
    return 0;
}