【树上倍增】【USACO MAR11银组】聚会地点

题目描述

Bessie和Jon每天都要去他们所居住的小镇的某些地方游玩。有趣的是,他们居住的小镇是一个树的结构,也就意味是,小镇的每个地方之间有且仅有一条通路(不是指一条边,而是指一条通路),每个地方都会有且仅有一个父亲地点(除了小镇的城镇中心,它没有祖先)。
小镇共有N个地点(1 <= N <= 1,000),编号1~N。点1是镇的中心。
Bessie和Jon决定每天都要在游玩后见面,他们见面的地点总是在他们游玩的两个地方之间的那条通路中,离城镇中心最近的地方,下面给出他们的旅行日程,你需要帮他们每天的见面地点。
你可以理解为城镇中心就是成为在这个树结构上的根。

                              地点          它的父亲地点
             [1]           ---------     ----------------
            / | \              1       ---(城镇中心没有父亲)
           /  |  \             2                1 
         [2] [3] [6]           3                1
         /        | \          4                2
        /         |  \         5                8
      [4]        [8]  [9]      6                1
                /   \          7                8
               /     \         8                6
             [5]     [7]       9                6

以下为他们某次见面的安排:

       Bessie      Jon        Meeting Place
      --------   --------    ---------------
         2           7              1
         4           2              2
         1           1              1
         4           1              1
         7           5              8
         9           5              6

输入

第1行:两个数N,M代表一共有N个地方,B和J已经进行了M次见面
第2..N-1行,每行一个数X,代表第i个地点的父亲为X
再接下来M行,每行两个数,分别代表B和J当天准备去游玩的地方
1<=N<=1000,1<=M<=1000

输出

一共M行,每行一个数代表B和J当天见面的地方。

样例输入

9 6
1
1
2
8
1
8
6
6
2 7
4 2
3 3
4 1
7 5
9 5

样例输出

1
2
3
1
8
6

分析

这道题有两种方法:暴力*and倍增*。
先来看看倍增吧。。

树上倍增

建一个二维数组ff[i][j]表示节点i的第 2j 个祖先。
可知i的枚举范围为 O(n) ,j的枚举范围为 O(logn)

可得出:

  • f[i][0]=fa[i]
  • f[i][j]=f[f[i][j1]][j1]

for(int i=1;i<=n;i++) f[i][0]=fa[i];
for(int j=1;j<=LOG;j++)
    for(int i=1;i<=n;i++)
        f[i][j]=f[f[i][j-1]][j-1];

getk操作

定位节点x的第k个祖先(父亲为第一个祖先):
根据倍增数组的性质,一次只能向上跳2的次幂个节点。
将k拆成二进制数,即可通过2的次幂凑出k。
觉得有点像线段树。。。

int getk(int x,int k){
    for(int i=0;iif(k&(1<return x;
}

getd操作

定位节点x的深度为d的祖先:

int getd(int x,int d){
    return getk(x,dep[x]-d);
}

最近公共祖先

在一棵有根树上找到两个点u,v,它们的共有祖先中离跟越远(深度越大)的节点就叫做最近公共祖先(LCA)。
【树上倍增】【USACO MAR11银组】聚会地点_第1张图片
举个栗子,如上图中,8和9的最近公共祖先是5,3和7的最近公共祖先是1。通常,两个一样的点的最近公共祖先是它自己,所以3和3的最近公共祖先是3。(这不是废话吗。。。)

用倍增数组求两个节点的LCA

首先用getd操作,统一两个节点的深度,这样子并不会影响答案。
当两个节点深度相同时,我们既可以通过二分深度+getd操作判定来确定LCA(时间复杂度 O(log2n) ),也可以直接用倍增数组查找LCA。
首先 u=v ,答案就是u。(如果你不特判,很坑的。。)
如果 uv ,就可以用下列代码:

for(int i=LOG-1;i>=0;i--)
    if(f[u][i]!=f[v][i]){
        u=f[u][i];
        v=f[v][i];
    }

其实它就是从下往上找,找到最后的两个不一样的节点。然后这些点的父亲就是答案了。
附:
二分LCA代码:

int LCA(int u,int v){
    if(dep[u]if(u==v) return u;
    int l=1,r=dep[v];
    while(l<=r){
        int mid=(l+r)>>1;
        if(getd(u,mid)==getd(v,mid)) l=mid+1;
        else r=mid-1;
    }
    return f[getd(u,l)][0];
}

数组LCA代码:

int LCA(int u,int v){
    if(dep[u]if(u==v) return u;
    for(int i=LOG-1;i>=0;i--)
        if(f[u][i]!=f[v][i]){
            u=f[u][i];
            v=f[v][i];
        }
    return f[u][0];
}

代码

#include
#include
using namespace std;
const int N=1000;
const int LOG=20;
int n,m,dep[N+1],fa[N+1],f[N+1][LOG+1];
vector<int> son[N+1];
int read(){
    char ch=getchar();int x=0;
    while(ch<'0'||ch>'9') ch=getchar();
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    return x;
}
int getk(int x,int k){
    for(int i=0;iif(k&(1<return x;
}
int getd(int x,int d){
    return getk(x,dep[x]-d);
}
void dfs(int x,int deep){
    dep[x]=deep;
    int len=son[x].size();
    for(int i=0;i1);
}
int LCA(int u,int v){
    if(dep[u]if(u==v) return u;
    for(int i=LOG-1;i>=0;i--)
        if(f[u][i]!=f[v][i]){
            u=f[u][i];
            v=f[v][i];
        }
    return f[u][0];
}
int main()
{
    n=read();m=read();
    for(int i=2;i<=n;i++){
        int x=read();
        f[i][0]=x;
        son[x].push_back(i);
    }
    for(int j=1;j<=LOG;j++)
        for(int i=2;i<=n;i++)
            f[i][j]=f[f[i][j-1]][j-1];
    dfs(1,1);
    while(m--)
        printf("%d\n",LCA(read(),read()));
}

暴力

暴力就很简单了,以下图为例。
【树上倍增】【USACO MAR11银组】聚会地点_第2张图片
我们让u,v两个点走到根节点,并把他们的路径记录下来。这里以7和9为例。

  7  6  5  1
     9  5  1

从路径中可以看出5是他们第一个共同的祖先,所以答案就是5啦。。。暴力出奇迹
代码就不打啦。。。

你可能感兴趣的:(#,C++,#,School,OJ,#,LCA)