题意:求树上两个节点的最近公共祖先
算法一:tarjan
LCA(u) {
Make-Set(u)
ancestor[Find-Set(u)]=u //设置u所在集合的祖先
对于u的每一个孩子v {
LCA(v)
Union(v,u) //把v生成的子集并入u中
ancestor[Find-Set(u)]=u //防止采用树形启发式合并使u的集合根(代表)变化
}
checked[u]=true
对于每个(u,v)属于P {
if checked[v]=true
then 回答u和v的最近公共祖先为 ancestor[Find-Set(v)]
}
}
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
/*
时间复杂度O(N+Q)
离线算法,必须先记录询问
对于每一对询问lac(u,v),在u,v的查询队列里各加一次
*/
const int MAXN = 10010;
class LCA_Tarjan
{
public:
int n, father[MAXN];
bool vis[MAXN];
vector<int> edge[MAXN];
vector<int> query[MAXN];
void init(int n)
{
for(int i = 1; i <= n; i++)
{
vis[i] = false;
edge[i].clear();
query[i].clear();
}
make_set(n);
}
void make_set(int n) //下标从1开始
{
for(int i = 1; i <= n; i++)
father[i] = i;
}
int find(int u)
{
if(u == father[u])
return u;
return father[u] = find(father[u]);
}
void Union(int u, int v) //可以优化
{
int fu = find(u);
int fv = find(v);
if(fv != fu)
father[fv] = fu;
}
void tarjan(int u)
{
father[u] = u;
int sz = edge[u].size();
for(int i = 0; i < sz; i++)
{
tarjan(edge[u][i]);
Union(u, edge[u][i]);
father[find(u)] = u;
}
vis[u] = true;
sz = query[u].size();
for(int i = 0; i < sz; i++)
{
if(vis[query[u][i]] == true)
cout << father[find(query[u][i])] << endl;
}
}
};
int main()
{
LCA_Tarjan t;
int T;
scanf("%d",&T);
while(T--)
{
scanf("%d", &t.n);
t.init(t.n);
int u, v;
bool notRoot[MAXN] = {0};
for(int i = 1; i < t.n; i++)
{
scanf("%d %d", &u, &v);
t.edge[u].push_back(v);
notRoot[v] = true;
}
scanf("%d %d", &u, &v);
t.query[u].push_back(v);
t.query[v].push_back(u);
for(int i = 1; i <= t.n; i++)
if(notRoot[i] == false) {t.tarjan(i); break;}
}
return 0;
}
算法二LCA向RMQ的转化:
对有根树T进行DFS,将遍历到的结点按照顺序记下,我们将得到一个长度为2N – 1的序列,称之为T的欧拉序列F
每个结点都在欧拉序列中出现,我们记录结点u在欧拉序列中第一次出现的位置为pos(u)
根据DFS的性质,对于两结点u、v,从pos(u)遍历到pos(v)的过程中经过LCA(u, v)有且仅有一次,且深度是深度序列B[pos(u)…pos(v)]中最小的
即LCA(T, u, v) = RMQ(B, pos(u), pos(v)),并且问题规模仍然是O(N)的
这就证明了LCA问题可以转化成RMQ问题
LCA与RMQ问题可以互相转化,并且可以在O(N)的时间内完成!
/*
1.时间复杂度O(N*logN+Q)
2.在线算法
3.搜索之后n个节点得到2*n-1个编号
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<queue>
#include<vector>
using namespace std;
const int MAXN = 10010;
const int PP = 25;
class LCA_RMQ
{
public:
int n;
int pow2[PP]; //2^k
int tim; //时间戳,从1开始
int first[MAXN]; //节点第一次访问的时间戳
int nodeId[MAXN*2]; //与相应时间戳对应的节点编号
int dep[MAXN*2]; //深度
int dp[MAXN*2][PP]; //搜索之后得到2*n-1个编号
bool vis[MAXN]; //记录节点是否被访问过
vector<int> edge[MAXN];
void init(int n)
{
for(int i = 0; i < PP; i++)
pow2[i] = (1<<i);
for(int i = 1; i <= n; i++)
{
edge[i].clear();
vis[i] = false;
}
}
void addedge(int u ,int v)
{
edge[u].push_back(v);
}
void dfs(int u ,int d) //u是节点, d是深度
{
tim++; //时间戳+1
vis[u] = true;
nodeId[tim] = u; //记录与时间戳相对应的节点
first[u] = tim; //记录下节点u首次访问的时间戳
dep[tim] = d; //根节点的深度为1
int sz = edge[u].size();
for(int i = 0; i < sz; i++)
{
int v = edge[u][i];
if(vis[v] == false)
{
dfs(v, d + 1);
tim++;
nodeId[tim] = u; //只要访问的不是叶子节点,tim都要重复增加
dep[tim] = d;
}
}
}
void ST(int len)
{
int k = (int)(log(len+1.0) / log(2.0));
for(int i = 1; i <= len; i++)
dp[i][0] = i;
for(int j = 1; j <= k; j++)
for(int i = 1; i + pow2[j] - 1 <= len; i++)
{
int a = dp[i][j-1]; //a, b均为下标
int b = dp[i+pow2[j-1]][j-1];
if(dep[a] < dep[b]) dp[i][j] = a;
else dp[i][j] = b;
}
}
int RMQ(int x ,int y)
{
int k = (int)(log(y-x+1.0) / log(2.0));
int a = dp[x][k];
int b = dp[y-pow2[k]+1][k];
if(dep[a] < dep[b]) return a;
else return b;
}
int LCA(int u ,int v)
{
int x = first[u];
int y = first[v];
if(x > y) swap(x,y);
int index = RMQ(x,y);
return nodeId[index];
}
};
LCA_RMQ t;
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
int u, v, rt;
bool notRoot[MAXN] = {0};
scanf("%d",&t.n);
t.init(t.n);
for(int i = 1; i < t.n; i++)
{
scanf("%d %d",&u,&v);
t.addedge(u, v);
notRoot[v] = true;
}
scanf("%d %d", &u, &v);
for(int i = 1; i <= t.n; i++)
if(notRoot[i] == false) {rt = i; break;}
t.tim = 0;
t.dfs(rt, 1); //根节点深度为1
t.ST(2 * t.n - 1);
int ans = t.LCA(u, v);
printf("%d\n", ans);
}
return 0;
}