hdu2586(LCA 用并查集+DFS实现)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <iostream>
#define MAXN 40010
using namespace std ;
struct Graph
{
int vex , next , dis ;
};
Graph g[MAXN * 2] , Q[400] ;
int first[MAXN] , head[MAXN] , set[MAXN] , away[MAXN] , n , m ;
bool visited[MAXN] ;
//first[v]用来查找节点所在边,即g[first[v]],同时, g[i].next记录与节点v连接的节点,这俩个记录十分的关键,也就是这俩记录来实现深搜的
//head[v]用来查找该节点所在的询问的路径,和first[v]的作用类似,主要是为了在访问该节点的时候,将所有与该节点相邻或有关的已访问过的节点的询问路径的距离求出来
//away[v]用来记录节点v到根节点的距离
void Add (int v , int w , int d , int &j)
{
g[j].dis = d ;
g[j].vex = w ;
g[j].next = first[v] ; //记录与v相邻的路径
first[v] = j ++ ;
}
void Add2 (int v , int w , int &j)
{
Q[j].dis = -1 ; //未计算过,初始化为-1
Q[j].vex = w ;
Q[j].next = head[v] ;
head[v] = j ++ ;
}
void Read ()
{
int i , j , v , w , d ;
memset (first , -1 , sizeof (first)) ;
memset (head , -1 , sizeof (head)) ;
memset (visited , false , sizeof (visited)) ;
//初始化是肯定要的
scanf ("%d%d" , &n , &m) ;
for (j = 0 , i = 1 ; i < n ; i ++)
{
scanf ("%d%d%d" , &v , &w , &d) ;
Add (v , w , d , j) ;
Add (w , v , d , j) ;
//这里进行俩次记录,主要是为下面记录的俩次询问路径做准备
}
for (i = j = 0 ; i < m ; i ++)
{
scanf ("%d%d" , &v , &w) ;
Add2 (v , w , j) ;
Add2 (w , v , j) ;
//这里用进行俩次记录,首先,v到w和w到v的距离是一样的,其次,
//因为在访问到v的时候,w可能还没访问到,如果没进行俩次记录的话,当访问到w时,则也不会计算v和w的距离
//当然,记录俩次,但只会计算一次
}
}
int Find (int x)
{
if (x != set[x])
set[x] = Find (set[x]) ;
return set[x] ;
} //查找父节点,同时路径压缩
void DFS (int v , int dis)
{
int i ;
set[v] = v ;
visited[v] = true ;
away[v] = dis ;
printf("访问节点%d\n",v);
for (i = head[v] ; i != -1 ; i = Q[i].next)
{
// printf("q[i].next为%d\n",Q[i].next);
if (visited[Q[i].vex])
{
//printf("计算节点%d和%d的距离\n",v,Q[i].vex);
Q[i].dis = away[v] + away[Q[i].vex] - 2 * away[Find (set[Q[i].vex])] ;
//这步计算很好理解,关键是确定了Find(set[Q[i].vex)就是v和Q[i].vex的最近公共祖先
//printf("此时%d 的父节点为%d ,%d的父节点为%d\n",v,set[v],Q[i].vex,Find(set[Q[i].vex]));
//printf("away[v] :%d ,away[q[i].vex]: %d ,away[Find(set[q[i].vex])]:%d \n",away[v],away[Q[i].vex],away[Find(set[Q[i].vex])]);
}
}
for (i = first[v] ; i != -1 ; i = g[i].next)
{
//printf("i为:%d g[i].vex为: %d v为: %d first[v]为: %d g[i].next为: %d 节点%d 是否已查: %d\n",i,g[i].vex,v,first[v],g[i].next,g[i].vex,visited[g[i].vex]);
if (!visited[g[i].vex])
{
//printf("进入递归\n");
DFS (g[i].vex , dis + g[i].dis) ; //注意这里的距离累加 dis + g[i].dis,因为这一步的累加,所以,away[g[i].vex]就记录了该节点到根节点的距离
set[g[i].vex] = v ; //这一步十分重要,当v的某一子树访问完了,才将子树连接到节点v上
// printf("退出一个递归,此时%d的父节点为%d\n",g[i].vex,set[g[i].vex]);
}
}
}
void Print ()
{
int i , j ;
for (i = j = 0 ; i < m ; i ++ , j += 2)
if (Q[j].dis != -1)
printf ("%d\n" , Q[j].dis) ;
else
printf ("%d\n" , Q[j + 1].dis) ; //这里的话,因为这些边都重复的插入,都一条边只会被访问一次,所以俩者必有一个
}
int main ()
{
int t ;
scanf ("%d" , &t) ;
while (t --)
{
Read () ;
DFS (1 , 0) ;
Print () ;
}
return 0 ;
}
这题目郁闷了好久呀,主要是在做并查集的题目pku1986的时候,碰到了运用最近公共祖先LCA Tarjan算法,所以就跳到这道题目
可是,自学这块内容倒是蛮抽象的,都是自己找资料看的,可是,网上的介绍还真的是好抽象啊,看了一遍又一遍,嘿嘿,主要还是看代码+测试数据,搞清楚里面的迭代过程,还有深搜的过程,再加上集合的合并过程
这里有一个很关键的地方,这里为什么要用这种离线算法呢?又是如何保证这种离线算法(先记录询问的路径,在查找过程中计算,再全部输出)的正确性的呢?
这里,先看看大牛的解释:http://www.cnblogs.com/ylfdrib/archive/2010/11/03/1867901.html
对于一棵有根树,就会有父亲结点,祖先结点,当然最近公共祖先就是这两个点所有的祖先结点中深度最大的一个结点。
0
|
1
/ \
2 3
比如说在这里,如果0为根的话,那么1是2和3的父亲结点,0是1的父亲结点,0和1都是2和3的公共祖先结点,但是1才是最近的公共祖先结点,或者说1是2和3的所有祖先结点中距离根结点最远的祖先结点。
在求解最近公共祖先为问题上,用到的是Tarjan的思想,从根结点开始形成一棵深搜树,非常好的处理技巧就是在回溯到结点u的时候,u的子树已经遍历,这时候才把u结点放入合并集合中,这样u结点和所有u的子树中的结点的最近公共祖先就是u了,u和还未遍历的所有u的兄弟结点及子树中的最近公共祖先就是u的父亲结点。以此类推。。这样我们在对树深度遍历的时候就很自然的将树中的结点分成若干的集合,两个集合中的所属不同集合的任意一对顶点的公共祖先都是相同的,也就是说这两个集合的最近公共最先只有一个。对于每个集合而言可以用并查集来优化,时间复杂度就大大降低了,为O(n + q),n为总结点数,q为询问结点对数。
以下为个人看法:
建议还不是很理解的朋友,可以自己写一些测试数据,具体的看一下访问路径以及每一个节点的父节点的变化,什么时候变化的,搞清这俩个问题的话,这个算法也没多大问题了,当然还有具体实现深搜的过程,
也是很重要的(在代码中有个人的详细说明),还有就是,关于Tarjan的思想,个人的理解,利用深搜,搜到底再回溯,回溯过程中,逐渐将子树合并,还有那个计算的式子,
Q[i].dis = away[v] + away[Q[i].vex] - 2 * away[Find (set[Q[i].vex])] ; 也很好理解,关键是保证Find (set[Q[i].vex])是他们的最近公共祖先,这个在上面已经说了
额,想尽量写的详细些,因为个人算是吃了苦头,很多博客都是直接贴代码,很无语的说