- 求最近公共祖先的算法
一.向上標記法
從x向上走到根節點, 並標記路徑上經過的點
從y向上走到根節點, 當遇到第一個被標記的點就找到了LCA(x, y)
二.樹上倍增發
時間複雜度: O(mlogn)
用 f[x, k] 表示x向上走2^k到達的節點, 很容易知道 , f[x, 0] 就是x的父節點, 遞推關係就是, f[x, k] = f[ f[x, k - 1], k - 1]
用depth[x], 存x的深度
步驟
1.預處理 f 數組和 depth 數組
2.實現目標函數: 保證 depth[x] > depth[y], 如果不是, 就想換x, y的值
3.用二進制拆分的思想, 把 x 調到和 y 同一個深度
4.如果此時 x == y, 那麼y就是他們的公共祖先
5. 如果不相等,就把x 和 y 同時往上調, 並且保證他們不會相遇
6. 這樣我們到達的就是他們的LCA的子節點, 最後結果就是f[x, 0]
對應步驟的代碼
1.預處理部分
用bfs實現, 深度就是當前節點的父節點深度+1, f 數組就是利用那個遞推式實現, 這裏的15是取值範圍的最大值的位數, 由題目而定
void bfs() //步驟1 預處理
{
memset(depth, 0x3f, sizeof depth);
depth[root] = 1;
depth[0] = 0;
queue<int> q;
q.push(root);
while(q.size())
{
int x = q.front();
q.pop();
for(int i = h[x]; ~i; i = ne[i])
{
int y = e[i];
if(depth[y] > depth[x] + 1)
{
depth[y] = depth[x] + 1;
q.push(y);
f[y][0] = x;
for(int j = 1; j <= 15; j++)
f[y][j] = f[f[y][j - 1]][j - 1];
}
}
}
}
2.lca函數部分
這裏是求 x, y 的LCA
int lca(int x, int y)
{
if(depth[x] > depth[y]) //步驟2
swap(x, y);
for(int i = 15; i >= 0; i--) // 步驟3
{
if(depth[f[y][i]] >= depth[x])
y = f[y][i];
}
if(x == y) return x; //步驟4
for(int i = 15; i >= 0; i--) //步驟5
{
if(depth[f[x][i]] != depth[f[y][i]])
{
x = f[x][i];
y = f[y][i];
}
}
return f[y][0]; //步驟6 返回最後結果
}
三:Tarjan 算法
時間複雜度:O(n + m)
該算法的本質是用並查集對向上標記法進行優化, 達到線性的時間複雜度
在實現算法的過程中, 我們把樹中的節點分爲3類:
- 正在訪問的節點, 以及它的到根節點這條路徑上的節點, 標記爲1
- 已經訪問完畢, 並且已經回溯的節點嗎,標記爲2
- 還沒有訪問的節點, 不標記
由於這是一種離線算法, 我們需要把詢問一次性全部讀入, 然後再執行算法
3中節點解釋
代碼實現
void tarjan(int x)
{
vis[x] = 1;
for(int i = h[x]; ~i; i = ne[i])
{
int y = e[i];
if(!vis[y])
{
tarjan(y);
f[y] = x;
}
}
for(int i = 0; i < q[x].size(); i++)
{
PII t = q[x][i];
int y = t.first;
int id = t.second;
if(vis[y] == 2)
{
int father = find(y);
//此時這個father就是LCA答案
}
}
vis[x] = 2;
}