树的公共祖先问题LCA

在一棵树中,每个节点都有0个或者多个孩子节点,除了根节点以外每个节点都有一个双亲节点,从根节点到当前节点的路径上的节点都是当前节点的祖先节点, 现任意给定两个节点,要求它们的公共祖先,并且这个公共祖先离它们最近

这里有两种算法:在线法和离线法.

在线法:指每提出一次请求,便给出一次应答

离线法:收集所有的请求,然后统一进行处理

在线法 dfs+RMQ

在线法主要借助了RMQ的思想,先对树进行深度优先遍历,对于含有n个节点的树,深度优先遍历产生的序列长度为2n-1, 遍历的过程中,记录每个节点第一次出现的位置,并记录序列中每个节点的层次。当询问节点a和节点b之间的最近公共祖先时,首先找到节点a和节点b第一次在遍历序列中出现的位置,它们之间有一段遍历序列,要想找离a和b最近的祖先,只用找出这段序列中层次最小的节点即可,而借助于RMQ的知识可以经过O(nlogn)的预处理,然后在O(1)的时间内找出最值,深度优先遍历的时间复杂度是O(n),因此总的时间复杂度是O(nlogn)

  1 #include <stdio.h>

  2 #include <string.h>

  3 #include <math.h>

  4 #include <malloc.h>

  5 

  6 #define max(a,b) ((a)>(b)?(a):(b))

  7 #define min(a,b) ((a)>(b)?(b):(a))

  8 

  9 struct tree{

 10     int n;

 11     int left, right;

 12 }p[100];

 13 

 14 int first[9];

 15 int sequence[20];

 16 int dep[20];

 17 int point=0;

 18 int deep=0;

 19 

 20 /* 建树 */

 21 int createTree()

 22 {

 23     int temp[8]={1,2,7,3,4,8,5,6};

 24     for(int i=0;i<8;i++)

 25         p[i].n=temp[i];

 26     p[0].left=1;p[0].right=2;

 27     p[1].left=3;p[1].right=4;

 28     p[2].left=-1;p[2].right=5;

 29     p[3].left=-1;p[3].right=-1;

 30     p[4].left=6;p[4].right=7;

 31     p[5].left=-1;p[5].right=-1;

 32     p[6].left=-1;p[6].right=-1;

 33     p[7].left=-1;p[7].right=-1;

 34     return 0;

 35 }

 36 

 37 /* 结点访问顺序是: 1 2 3 2 4 5 4 6 4 2 1 7 8 7 1 共2n-1个值 */

 38 /* 结点对应深度是: 0 1 2 1 2 3 2 3 2 1 0 1 2 1 0 */

 39 void dfs(int root)

 40 {

 41     if(p[root].n < 0)

 42         return;

 43     sequence[point]=p[root].n;

 44     dep[point]=deep;

 45     if(first[p[root].n] < 0)

 46         first[p[root].n]=point;

 47     point++;

 48     if(p[root].left > 0){

 49         deep++;

 50         dfs(p[root].left);

 51         deep--;

 52         sequence[point]=p[root].n;

 53         dep[point]=deep;

 54         point++;

 55     }

 56     if(p[root].right > 0){

 57         deep++;

 58         dfs(p[root].right);

 59         deep--;

 60         sequence[point]=p[root].n;

 61         dep[point]=deep;

 62         point++;

 63     }

 64 }

 65 

 66 int map[100][100];

 67 /**

 68  * 花费O(nlogn)的时间进行预处理 递推公式

 69  * f[i,j]=max(f[i, j-1], f[i + 2^(j-1), j-1])

 70  * @param m 存储数值的数组

 71  * @param n 数组长度

 72  */

 73 void pre_handle(int * m , int n)

 74 {

 75     memset(map, 0, sizeof(map));

 76     for(int i=0;i<n;i++)

 77         map[i][0]=dep[i];

 78     int k = (int)(log(n) / log(2));

 79     for(int i=1;i<=k;i++)       /* i表示列,j表示行, 每一列的结果只依赖于前一列 */

 80         for(int j = 0; j + pow(2,i-1) < n; j++)

 81             /* 注意因为每列都限制j + pow(2,i-1) < n,而除了第一列以外

 82              * 其它都不到n-1,所有每一行的最后一个数有可能是不对的,但

 83              * 是不影响最后结果 */

 84             map[j][i]=min(map[j][i-1], map[j+(int)pow(2, i-1)][i-1]);

 85 }

 86 

 87 int RMQ(int a, int b)

 88 {

 89     int k = (int)(log(b-a+1)/log(2)); /* 区间长度为b-a+1 */

 90     /* 两个区间之间有重合 */

 91     return min(map[a][k], map[b+1-(int)pow(2, k)][k]);

 92 }

 93 

 94 

 95 int main()

 96 {

 97     int root = createTree();

 98     memset(first, -1, sizeof(first));

 99     dfs(root);

100     pre_handle(sequence, point);

101     printf("%d\n", RMQ(4, 11));

102     return 0;

103 }

离线法 dfs+并查集

算法的思想比较复杂,这里不做详细介绍,基本操作是在深度优先遍历过程中,利用并查集不断归并节点,并处理相应节点上的请求

  1 /**

  2  * @file   code.c

  3  * @author  <kong@KONG-PC>

  4  * @date   Mon Dec 03 20:52:03 2012

  5  *

  6  * @brief  Tarjan算法,基本思想是深度优先遍历+并查集,利用并查集可以将

  7  * 查询两个数字是否在一个集合中的操作变为O(1)

  8  */

  9 

 10 #include <stdio.h>

 11 #include <vector>

 12 using namespace std;

 13 #define N 100

 14 

 15 vector<int> tree[N],request[N]; /* 用来记录子节点和对应节点的询问信息 */

 16 int ancestor[N];                /* 记录节点的祖先节点 */

 17 

 18 int visit[N];                   /* 记录节点的访问情况 */

 19 int p[N];                  /* 临时记录并查集中的父亲节点,在遍历的过程

 20                             * 中它的值会不断变化 */

 21 int rank[N];

 22 

 23 void init(int n)

 24 {

 25     for(int i=0;i<n;i++)

 26     {

 27         rank[i]=1;              /* 初始化所在集合的节点个数 */

 28         p[i]=i;

 29         visit[i]=0;

 30         ancestor[i]=-1;          /* 把所有节点的祖先节点先初始化为-1 */

 31         tree[i].clear();

 32         request[i].clear();

 33     }

 34 }

 35 

 36 /* 返回并查集的根节点 */

 37 int find(int t)

 38 {

 39     if(p[t]==t)

 40         return t;

 41     return p[t]=find(p[t]);     /* 在查询的过程中对并查集进行优化,使

 42                                  * 用路径压缩 */

 43 }

 44 /* 合并两个集合,使用启发算法,将深度较小的树指到深度较大的树的根上,

 45  * 可以防止树的退化 */

 46 int unionSet(int a, int b)

 47 {

 48     int m=find(a);              /* 首先获取m,n所在集合的根元素 */

 49     int n=find(b);

 50     if(m==n)

 51         return 0;

 52     if(rank[m]>=rank[n])        /* 判断两个集合的rank值,并把值小的并

 53                                  * 到值大的集合中 */

 54     {

 55         p[n]=m;

 56         rank[m]+=rank[n];

 57     }

 58     else

 59     {

 60         p[m]=n;

 61         rank[n]+=rank[m];

 62     }

 63     return 1;

 64 }

 65 /* 深度优先遍历+并查集 */

 66 void LCS(int t)

 67 {

 68     ancestor[t]=t;

 69     for(int i=0;i<(int)tree[t].size();i++)

 70     {

 71         LCS(tree[t][i]);

 72         unionSet(tree[t][i],t);

 73         ancestor[p[t]]=t;

 74     }

 75     visit[t]=1;

 76     for(int i=0;i<(int)request[t].size();i++)

 77         if(visit[request[t][i]]==1)

 78             printf("%d %d  %d\n", t, request[t][i],ancestor[p[request[t][i]]]); /* 这里需要知道request[t][i]所在集合的祖先节点 */

 79 }

 80 

 81 int main()

 82 {

 83     freopen("in","r",stdin);

 84     int n,t,r;                  /* 分别表示节点个数、边的个数和询问个

 85                                  * 数 */

 86     int a,b;

 87     scanf("%d%d%d",&n,&t,&r);

 88     init(n);                    /* 初始化结构 */

 89     while(t--)                  /* 读入树结构 */

 90     {

 91         scanf("%d%d",&a,&b);

 92         tree[a].push_back(b);

 93     }

 94     while(r--)                  /* 读入询问并存入对应的列表中 */

 95     {

 96         scanf("%d%d",&a,&b);

 97         request[a].push_back(b);

 98         request[b].push_back(a);

 99     }

100     LCS(0);

101     return 0;

102 }

 

你可能感兴趣的:(问题)