算法学习:最近公共祖先
最近公共祖先
1、暴力算法
復雜度:O(m * logn)(最大O(n * m));
先記錄每個層數,再對層數高的依次向上移動,直到找到相同祖先,此處不寫代碼
2、倍增思想
復雜度:O(nlog(n)+mlog(n));
原理:
記錄其第2^i個父節點的內容,按二進制進位行運算
首先初始化sic[x] [0]為其上第一個父節點,隨后從i到所需1<<n,依次根據上一層父節點的父節點進行記錄:
sic[x][i] = sic[sic[x][i - 1]][i - 1];查找最近公共祖先時,首先將兩結點放在同一層
for (int i = 15; i > -1; i--){if (len[sic[a][i]] >= len[b])a = sic[a][i]; }因為父節點層數一定比a與b的最小層數小或相等,因此不會出現有跳過父節點的情況。
之后,同時將a與b進行跳躍操作,跳躍到最近公共父節點的下一層(因每次跳躍的判定條件是)sic[a] [i] != sic[b] [i],因此只能找到最近公共父節點下一層,最后返回sic[a] [0]即可得到最近公共祖先。
代碼如下:
#include<iostream> #include<algorithm> #include<stdio.h> #include<string> #include<string.h> #include<vector> #include<queue>using namespace std;int n, m; int sic[1005][16], len[1005]; vector<int>v[1005];void lca() {queue<int>q;q.emplace(1);while (!q.empty()) {int k = q.front();q.pop();for (auto x : v[k]) {len[x] = len[k] + 1;for (int i = 1; i < 15; i++){if (sic[sic[x][i - 1]][i - 1] == 0)break;sic[x][i] = sic[sic[x][i - 1]][i - 1];}q.emplace(x);}} }int solve(int a,int b) {if (len[a] < len[b])swap(a, b);for (int i = 15; i > -1; i--){if (len[sic[a][i]] >= len[b])a = sic[a][i];}if (a == b)return a;for (int i = 15; i > -1; i--) {if (sic[a][i] != sic[b][i]) {a = sic[a][i]; b = sic[b][i];}}return sic[a][0]; }int main() {int a, b;cin >> n >> m;memset(sic, -1, sizeof sic);sic[1][0] = -1;len[1] = 1;for (int i = 0; i < n; i++){cin >> a >> b;v[a].push_back(b);sic[b][0] = a;}lca();for (int i = 0; i < m; i++){cin >> a >> b;cout << solve(a, b) << endl;}return 0; }樣例:
7 6
1 2 2 3 3 4 2 5 5 6 6 7 1 8
4 7
3 2
5 2
1 8
7 8
4 6
預期輸出:
2
2
2
1
1
2
3、Tarjan
復雜度:O(n+m);
原理:
此算法可算作是暴力求解算法的優化,是離線查詢,其原理如下:
將樹上的點分為三個部分:
【1】已經遍歷且回溯過的點,如藍色部分
【2】正在遍歷且未回溯的點,如紅色分支
【3】還未遍歷的點
如下圖所示:
此時,與并查集的思想相結合,已經回溯過的點,其根結點記錄在目前正在遍歷分支上。當要查詢的結點中的一個被遍歷到,且另一個點已經被回溯過,則直接找到另一個點的根節點,就是這兩個點的最近公共祖先。
那么,如何記錄已經回溯過的點在目前正在遍歷的點的根節點?
首先初始化每個結點的根節點是其本身,在每次回溯過后,再更改其根節點位置,如此,其已回溯過的分支的根節點在其回溯之前的根節點一定是此結點本身,在遍歷此節點的子節點分支時,這個狀態不會改變。
如上圖所示,當回溯到3時,3的根節點更新為其父節點。
根據如上原理,就可有如下操作:
void tarjan(int u) {sic[u] = 1;for (auto x:v[u]){//st[x] = st[u] + 1;tarjan(x);len[x] = u;}for (auto x : vis[u]) {int y = x.first, p = x.second;if (sic[y] == 2) {ans[p] = fa(y);}}sic[u] = 2; }sic數組為標記數組,標記點是否被遍歷/已回溯/未遍歷;
len數組為記錄數組,記錄其根節點,在每次回溯后更新當前回溯點的根節點。
注:此代碼建立的是有向圖,因此不須剪枝。
代碼如下:
#include<iostream> #include<algorithm> #include<stdio.h> #include<string> #include<string.h> #include<vector> #include<queue>using namespace std;int n, m; int sic[1005], len[1005]; int st[1005]; int ans[1005], o; vector<int>v[1005]; vector<pair<int,int>>vis[1005];int fa(int x) {if (len[x] != x)len[x] = fa(len[x]);return len[x]; }void tarjan(int u) {sic[u] = 1;for (auto x:v[u]){//st[x] = st[u] + 1;tarjan(x);len[x] = u;}for (auto x : vis[u]) {int y = x.first, p = x.second;if (sic[y] == 2) {ans[p] = fa(y);}}sic[u] = 2; }int main() {int a, b;cin >> n >> m;len[1] = 1;for (int i = 0; i < 1005; i++){len[i] = i;}for (int i = 0; i < n; i++){cin >> a >> b;v[a].push_back(b);}for (int i = 0; i < m; i++){cin >> a >> b;vis[a].push_back({ b,i });vis[b].push_back({ a,i });}//st[1] = 0;tarjan(1);for (int i = 0; i < m; i++) {cout << ans[i] << endl;}return 0; }總結
以上是生活随笔為你收集整理的算法学习:最近公共祖先的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 软件测试工程师是做什么的?
- 下一篇: 单源最短路SPFA