LCA 最近公共父祖先 POJ3379模板题
LCA(Least Common Ancestors),即最近公共祖先,是指在有根树中,找出某两个结点u和v最近的公共祖先。 ———来自百度百科
- 暴力算法
以 1717 和 1818 为例,既然要求LCA,那么我们就让他们一个一个向上爬~~(我要一步一步往上爬 —— 《蜗牛》)~~,直到相遇为止。第一次相遇即是他们的LCA。 模拟一下就是:
17->14->10->7->317−>14−>10−>7−>3
18->16->12->8->5->318−>16−>12−>8−>5−>3
最终结果就是 33
当然这个算法妥妥的会T飞掉,那么我们就要进行优化,于是就有了用倍增来加速的倍增LCA,这也是我们今天介绍的重点。
- 倍增算法
所谓倍增,就是按22的倍数来增大,也就是跳 1,2,4,8,16,321,2,4,8,16,32 …… 不过在这我们不是按从小到大跳,而是从大向小跳,即按……32,16,8,4,2,132,16,8,4,2,1来跳,如果大的跳不过去,再把它调小。这是因为从小开始跳,可能会出现“悔棋”的现象。拿 55 为例,从小向大跳,5≠1+2+45=1+2+4,所以我们还要回溯一步,然后才能得出5=1+45=1+4;而从大向小跳,直接可以得出5=4+5=4+1。这也可以拿二进制为例,5(101)5(101),从高位向低位填很简单,如果填了这位之后比原数大了,那我就不填,这个过程是很好操作的。
还是以 1717 和 1818 为例,如果分别从1717和1818跳到33的话,它们的路径分别是*(此例只演示倍增,并不是倍增LCA算法的真正路径)*:
17->317−>3
18->5->318−>5−>3
可以看出向上跳的次数大大减小。这个算法的时间复杂度为O(nlogn)O(nlogn),已经可以满足大部分的需求。
package pro.proclass; import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.StringTokenizer; public class P3379two { private static int N, M, S, level, cnt; // private static ArrayList<Integer>[] arr; private static int[][] parent; private static int[] depth, head; private static Edge[] edges; public static void main(String[] args) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer st = new StringTokenizer(br.readLine()); N = Integer.parseInt(st.nextToken()); M = Integer.parseInt(st.nextToken()); S = Integer.parseInt(st.nextToken()); // arr = new ArrayList[N+1]; for (int i = 0; i <= N; i++) { // arr[i] = new ArrayList<>(); } //1.parent 2. depth 3.lca // 求倍增法建立的树的最大跳的2^次方的值 int num = 1; level = 0; while (num < N) { num+=num; level++; } edges = new Edge[2*(N-1)]; parent = new int[level+1][N+1]; depth = new int[N+1]; cnt = 0; head = new int[N+1]; Arrays.fill(head, -1); for (int i = 1; i < N; i++) { st = new StringTokenizer(br.readLine()); int a = Integer.parseInt(st.nextToken()); int b = Integer.parseInt(st.nextToken()); // arr[a].add(b); // arr[b].add(a); add(a, b); add(b, a); } // 求深度和初始化parent[0][i]; bfs(); // 构建parent init(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < M; i++) { st = new StringTokenizer(br.readLine()); int a = Integer.parseInt(st.nextToken()); int b = Integer.parseInt(st.nextToken()); int ans = lca(a, b); sb.append(ans+"\n"); } System.out.println(sb); } private static int lca(int a, int b) { if(depth[a] < depth[b]){ a = a ^ b; b = a ^ b; a = a ^ b; } // 先进行跳跃深度大的点 // 逆序的原因是,保证进行最大跨步的跳跃 for (int i = level; i >= 0; i--) { // 如果跳跃了某个步子,深度依然大于b的的深度,那么继续跳 if(depth[parent[i][a]] >= depth[b]) { a = parent[i][a]; } } // 到这里两个点的深度必然一样,如果是同一个点 则返回 if(a == b) return a; // 两个点同时往上跳,当跳跃的步子大,绝对是一个父节点, // 当刚刚不是一个父节点的时候进行跳跃 for (int i = level; i >= 0; i--) { if(parent[i][a] != parent[i][b]) { a = parent[i][a]; b = parent[i][b]; } } return parent[0][a]; } private static void init() { // 知道了0层的值,可以推导除其他层的值 for (int i = 1; i <= level; i++) { for (int j = 1; j <= N; j++) { parent[i][j] = parent[i-1][parent[i-1][j]]; } } // 推导方法的由来原因 2^n = (2 ^ n-1) + (2 ^ n-1) } private static void bfs() { LinkedList<Integer> que = new LinkedList<>(); que.add(S); depth[S] = 1; int size = 0, step = 1; while (!que.isEmpty()) { size = que.size(); step++; for (int i = 0; i < size; i++) { Integer cur = que.poll(); for (int j = head[cur]; j != -1 ; j=edges[j].next) { int next = edges[j].to; // for (int j = 0; j < arr[cur].size(); j++) { // Integer next = arr[cur].get(j); if(depth[next] == 0) { depth[next] = step; parent[0][next] = cur; que.add(next); } } } } } private static void add(int u, int v) { edges[cnt] = new Edge(v, head[u]); head[u] = cnt++; } static class Edge{ int id; int to; int next; public Edge(int to, int next) { this.to = to; this.next = next; } public Edge(int to, int next, int id) { this.id = id; this.to = to; this.next = next; } } }

浙公网安备 33010602011771号