最近公共祖先(Least Common Ancestors)
2016-03-24 22:33
351 查看
题意:
给定一棵有根树T,给出若干个查询lca(u, v)(通常查询数量较大),每次求树T中两个顶点u和v的最近公共祖先,即找一个节点,同时是u和v的祖先,并且深度尽可能大(尽可能远离树根)。通常有以下几种算法:在线算法,每次读入一个查询,处理这个查询,给出答案。
离线算法,一次性读入所有查询,统一进行处理,给出所有答案。
在线:
倍增(基于二分搜索):
基本思想就是让u和v同时走到同一高度,然后再一起一步步往上走。将父亲结点的父亲结点利用起来,依次计算,便可以得到从当前结点向上走2k步所到达的顶点,这样便有了k以内的点的所有信息,进行二分查找答案即可~
预处理时间复杂度O(nlogn),查询时间复杂度O(logn)。
关键代码:
首先预处理阶段//DFS预处理所有结点的深度和父节点 void dfs(int v, int p, int d) { pa[0][v] = p; dept[v] = d; for(int i = head[v]; i != -1; i = edge[i].next){ int u = edge[i].to; if(u == p) continue; dfs(u, v, d + 1); } } void init() { dfs(root, -1, 0); //预处理祖先,向上走2^i所到的结点 for(int i = 0; i < maxm - 1; i++){ for(int j = 1; j <= V; j++){ if(pa[i][j] < 0) pa[i + 1][j] = -1; else pa[i + 1][j] = pa[i][pa[i][j]]; } } }
计算u和v的lca
int lca(int u, int v) { //让u和v 向上走到同一高度 if(dept[u] > dept[v]) swap(u, v); for(int i = 0; i < maxm; i++){ if((dept[v] - dept[u]) >>i &1) v = pa[i][v]; } if(u == v) return u; //二分搜索计算lca for(int i = maxm - 1; i >= 0; i--){ if(pa[i][u] != pa[i][v]){ u = pa[i][u]; v = pa[i][v]; } } return pa[0][u]; }
基于RMQ的算法:
初始化过程O(nlogn),查询过程O(1)。有根树处理的一个技巧就是将树转化为从根DFS标号后得到的序列。而这种算法的基本思想就是将树看成一个无向图,u和v的公共祖先一定在u和v之间的最短路上。
算法分三步:
首先DFS对结点从跟开始标号,用数组vs保存访问顺序,height记录深度。每条边恰好经过两次,因此一共记录了2n−1个结点
计算对于每个顶点首次出现子的下标,保存在id中。
获取LCA(u,v):LCA(u,v)=vs[id[u]≤i≤id[v]中深度最小的i]
预处理:
void dfs(int u, int pre, int dept) { vs[cnt] = u; height[cnt] = dept; id[u] = cnt++; for(int i = head[u]; i != -1; i = edge[i].next){ dfs(edge[i].to, u, dept + 1); vs[cnt] = u; height[cnt++] = dept; } } void init() { cnt = 1; //vs数组下标从1开始 dfs(root, root, 0); st.init(2 * V - 1); }
而最后一步属于RMQ(Range Minimum/Maximum Query),即区间最值查询问题,我们可以用线段树解决,也可以使用ST(Sparse Table)算法,在O(nlogn)时间内进行预处理,然后在O(1)时间内回答每个查询。
预处理使用动态规划,设dp[i][j]是从i开始的2j个数中的深度最小的值的下标。则有状态转移方程:
if(height[dp[i][j - 1]] < height[dp[i + (1<<(j - 1))][j - 1]]) dp[i][j] = dp[i][j - 1]; else dp[i][j] = dp[i + (1<<(j - 1))][j - 1];
初始化:
for(int i = 1; i <= n; i++) dp[i][0] = i;
查询:
int query(int a, int b) { if(a > b) swap(a, b); int k = lg[b - a + 1] ; if(height[dp[a][k]] <= height[dp[b - (1<<k) + 1][k]]) return dp[a][k]; else return dp[b - (1<<k) + 1][k]; }
离线Tarjan算法:
讲的很好Tarjan算法是离线算法,基于后序DFS和并查集。
算法从根节点root开始搜索,每次递归搜索所有的子树,然后处理跟当前根节点相关的所有查询。
算法用集合表示一类节点,这些节点跟集合外的点的LCA都一样,并把这个LCA设为这个集合的祖先。当搜索到节点x时,创建一个由x本身组成的集合,这个集合的祖先为x自己。然后递归搜索x的所有儿子节点。
所有子树处理完毕之后,处理当前根节点x相关的查询。遍历x的所有查询,如果查询的另一个节点v已经访问过了,那么x和v的LCA即为v所在集合的祖先。
建树可以用数组写链表也可以用vector保存,而查询可以用矩阵保存,这样可以减少重复,也可以用链表的形式,将一个结点的查询连在一起。
Tarjan关键代码:
void LCA(int u) { ance[u] = u; vis[u] = 1; for(int i = head[u]; i != -1; i = edge[i].next){ int v = edge[i].to; if(vis[v]) continue; LCA(v);//访问子树 unite(u, v);//子树与当前结点合并 ance[_find(u)] = u;//祖先为u } for(int i = h[u]; i != -1; i = query[i].next){ int v = query[i].q; if(vis[v]) ans[query[i].index] = ance[_find(v)]; } }
//感觉这个ance数组完全可以不用~~
相关文章推荐
- POJ1679(次小生成树)
- 爬虫(Spider),反爬虫(Anti-Spider)
- Hadoop HA高可用集群搭建(2.7.2)
- struts2原理
- 大数据风控指标----查准率与查全率
- 直接插入排序
- Codeforces 301D Yaroslav and Divisors 【树状数组 + 思维】
- 冒泡排序
- Upgrading Nagios Core 4. "How to Upgrade Nagios Core 4."
- 第三方的库epel安装
- linux之shell 条件测试
- J2EE轻量级框架--3.24学习笔记
- 个人简历
- MFC 以及多线程初探
- Android视频推流直播学习【四】
- iOS 封装UITabBarController(一)
- 一起talk C栗子吧(第一百二十九回:C语言实例--C程序内存布局一)
- Command模式实现撤销重做(Undo/Redo)
- WPF入门
- 关于i++和++i理解