LCA转RMQ

#include <bits/stdc++.h>
using namespace std;
const int N = 40010;
const int M = 25;
int dp[2*N][M];  //这个数组记得开到2*N,因为遍历后序列长度为2*n-1
bool vis[N];
struct edge{int u,v,w,next;}e[2*N];
int tot,head[N];
void add(int u ,int v ,int w ,int &k){
    e[k].u = u; e[k].v = v; e[k].w = w;//存边
    e[k].next = head[u]; head[u] = k++;//U点射出的第一条边是K
    u = u^v; v = u^v; u = u^v;//U,V交换
    e[k].u = u; e[k].v = v; e[k].w = w;//存边
    e[k].next = head[u]; head[u] = k++;//V点射出的第一条边是K
}
int ver[2*N],R[2*N],first[N],dir[N];//VER与R是I是按DFS顺序,FIRST与DIR是结点编呈号
//ver:节点编号 R:深度 first:点编号位置 dir:距离
//VER是第I个那些搜到的节点DFS序(含右界)编号,在算法中用于在用DP求最所求区间深度最小编号是第几位后求出结点编号,再让DIS求距离(三)
//R是深度是第I个扫到的编号的深度,在算法中用来把树打平成一个区间,生成DP维护左右界的区间中的最小深度点(二)
//FIRST是I号结点第一次出现的位置(即左界),在算法中用于求两点LCA时两点在R中作为左右界的位置,(一)
//DIS是I号结点与根结点的距离(即边权和),在算法中用于最后答案的获得,知两点与其LCA,求距离(四)
void dfs(int u ,int dep){//当前结点及当前深度
    vis[u] = true; //当前U结点访问标记打上
    ver[++tot] = u; //TOT是总节点数也表示当前搜到的结点是第TOT个
    first[u] = tot;//U号结点第一次出位的位置记录在FIRST
    R[tot] = dep;//当前结点深度记录
    for(int k=head[u]; k!=-1; k=e[k].next)//遍历指向的边
        if( !vis[e[k].v] ){//非老豆
            int v = e[k].v , w = e[k].w;//读出此边指向的结点编号及边权
            dir[v] = dir[u] + w;//边权更新
            dfs(v,dep+1);//继续深搜
            ver[++tot] = u;//一个儿子子树搜完回来又又记录一下(这样一个点可以有多次记录)
            R[tot] = dep;//第TOT个扫到的结点(即U)深度是DEP
        }
}
void ST(int n){//构建dp:dp[i][j]指ver数组中第i位(从1起)的2^j区间出现的结点编号中深度最小的结点是第几个扫到(R也不是以结点编号为下标)
    for(int i=1;i<=n;i++)dp[i][0] = i;//每位起往右控制的2^0长的区间就只有他自己,注意这里说的是VER数组,不是真正的树
    for(int j=1;(1<<j)<=n;j++){//枚举长度,2^j要小于等于N
        for(int i=1;i+(1<<j)-1<=n;i++){//枚举左界起点
            int a = dp[i][j-1] , b = dp[i+(1<<(j-1))][j-1];//得到左右子区间中在R中深度最小的那个结点的排位
            dp[i][j] = R[a]<R[b]?a:b;//左右子区间得到的结点排位所在深度比较,较小的作为本区间排位
        }
    }
}
int RMQ(int l,int r){//中间部分可能会有交叉
    int k=0;while((1<<(k+1))<=r-l+1)k++;//找出最小和K使1<<(k+1)大于r-l+1
    int a = dp[l][k], b = dp[r-(1<<k)+1][k]; //得到左右子区间中在R中深度最小的那个结点的排位
    return R[a]<R[b]?a:b;//左右子区间得到的结点排位所在深度比较,较小的返回
}
int LCA(int u ,int v){//找U与V的LCA
    int x = first[u] , y = first[v];//找出两个点的第一个出现的位置
    if(x > y) swap(x,y);//要求X比Y要先出现,实即在VER中X左界Y右界
    int res = RMQ(x,y);//读出X,Y这个区间中深度最小的点的排位(即搜索时是第几个搜到)
    return ver[res];//第RES个扫到的结点就是LCA
}
int main(){
    int cas;scanf("%d",&cas);
    while(cas--){//逐个样例判
        int n,q,num = 0;
        scanf("%d%d",&n,&q);
        memset(head,-1,sizeof(head));
        memset(vis,false,sizeof(vis));
        for(int i=1; i<n; i++){
            int u,v,w;scanf("%d%d%d",&u,&v,&w);//U-V无向边,W边权
            add(u,v,w,num);//建边,num是引用传用,代表当前的边数
        }
        tot = 0; dir[1] = 0;
        dfs(1,1);//深搜,一号点是根,深度是一
        /*printf("节点ver "); for(int i=1; i<=2*n-1; i++) printf("%d ",ver[i]); cout << endl;
        printf("深度R "); for(int i=1; i<=2*n-1; i++) printf("%d ",R[i]);   cout << endl;
        printf("首位first "); for(int i=1; i<=n; i++) printf("%d ",first[i]);    cout << endl;
        printf("距离dir "); for(int i=1; i<=n; i++) printf("%d ",dir[i]);      cout << endl;*/
        ST(2*n-1);//构建ST表
        while(q--){
            int u,v;scanf("%d%d",&u,&v);
            int lca = LCA(u,v);//求出两点的LCA
            printf("%d\n",dir[u] + dir[v] - 2*dir[lca]);//dist存储节点到根的距离
        }//思路:<u, v>距离 = dist[ u ] + dist[ v ] - 2 * dist[ LCA(u,v)]
    }
    return 0;
}
LCA转RMQ
hdoj 2586 How far away ? 
题意:给你一个N个点的(有边权)和Q次查询,每次查询问你任意两点间距离。
 
IN
2 
3 2 
1 2 10 
3 1 15 
1 2 
2 3 
2 2 
1 2 100 
1 2 
2 1

OUT
10
25
100
10 

我们通过深搜可以得到这样一个序列:
节点ver 1 3 1 2 5 7 5 6 5 2 4 2 1 (先右后左)
深度R 1 2 1 2 3 4 3 4 3 2 3 2 1 
首位first 1 4 2 11 5 8 6
搜索得到序列之后假如我们想求4 和 7的 LCA,那么我们找4和7在序列中的位置通过first 数组查找发现在6---11,即7 5 6 5 2 4。在上面图上找发现正好是以2为根的子树。而我们只要找到其中一个深度最小的编号就可以了。这时候我们就用到了RMQ算法。
维护一个dp数组保存其区间深度最小的下标,查找的时候返回就可以了。比如上面我们找到深度最小的为2点,返回其编号10即可。
反思:本题的DP是用R深度数组来做的,另一种思路可以是R数组不用搜索的顺序标记作为下标,也是以结点编号作为下标,然后在DP表构建时就用VER来构,DP[I][J]表示以I为左界长为2^j的区间中深度最小的结点编号

猜你喜欢

转载自blog.csdn.net/cj1064789374/article/details/85381762