POJ 1330 ---(线段树在线LCA 与 tarjan离线LCAs)

       说好是要开始做bzoj 然后第一题是原来一直想搞搞却没有结果的树链剖分,写的时候突然发现自己连LCA怎么写都有点忘了,于是就去找了LCA的模板题目,就当成练练手吧。做题这种东西也不是一朝一夕就可以完成的~继续巩固一下基础说不定也是一件好事!


      

Nearest Common Ancestors
Time Limit: 1000MS Memory Limit: 10000K
Total Submissions: 22030 Accepted: 11528

Description

A rooted tree is a well-known data structure in computer science and engineering. An example is shown below: 

 
In the figure, each node is labeled with an integer from {1, 2,...,16}. Node 8 is the root of the tree. Node x is an ancestor of node y if node x is in the path between the root and node y. For example, node 4 is an ancestor of node 16. Node 10 is also an ancestor of node 16. As a matter of fact, nodes 8, 4, 10, and 16 are the ancestors of node 16. Remember that a node is an ancestor of itself. Nodes 8, 4, 6, and 7 are the ancestors of node 7. A node x is called a common ancestor of two different nodes y and z if node x is an ancestor of node y and an ancestor of node z. Thus, nodes 8 and 4 are the common ancestors of nodes 16 and 7. A node x is called the nearest common ancestor of nodes y and z if x is a common ancestor of y and z and nearest to y and z among their common ancestors. Hence, the nearest common ancestor of nodes 16 and 7 is node 4. Node 4 is nearer to nodes 16 and 7 than node 8 is. 

For other examples, the nearest common ancestor of nodes 2 and 3 is node 10, the nearest common ancestor of nodes 6 and 13 is node 8, and the nearest common ancestor of nodes 4 and 12 is node 4. In the last example, if y is an ancestor of z, then the nearest common ancestor of y and z is y. 

Write a program that finds the nearest common ancestor of two distinct nodes in a tree. 

Input

The input consists of T test cases. The number of test cases (T) is given in the first line of the input file. Each test case starts with a line containing an integer N , the number of nodes in a tree, 2<=N<=10,000. The nodes are labeled with integers 1, 2,..., N. Each of the next N -1 lines contains a pair of integers that represent an edge --the first integer is the parent node of the second integer. Note that a tree with N nodes has exactly N - 1 edges. The last line of each test case contains two distinct integers whose nearest common ancestor is to be computed.

Output

Print exactly one line for each test case. The line should contain the integer that is the nearest common ancestor.

Sample Input

2
16
1 14
8 5
10 16
5 9
4 6
8 4
4 10
1 13
6 15
10 11
6 7
10 2
16 3
8 1
16 12
16 7
5
2 3
3 4
3 1
1 5
3 5

Sample Output

4
3

Source


  
        这题题意就是给出一棵树,然后给出两个节点,求他们的最近公共祖先的节点编号是多少?

         一、先说说离线的LCA 做法,也就是tarjan,所谓离线也就是需要将询问全部保存住,等输入结束之后统一计算。tarjan LCA我觉得更倾向于利用图本身的性质来进行求解。

        这种做法可以看成是在dfs的过程中一边对集合进行合并,一边进行求解。集合的划分是按照从根节点到当前节点这条活动路径上所有的点作为代表元进行的:
      
        

        红色点集合为{8,1,13,14}代表元为8,黄色点集合为{4,6,15,7}代表元为4,蓝色点集合为{10,2}代表元为10。我们假设此时3号点与12号点已经经过遍历,现在准备结束对以16节点为根的子树的dfs。若此时有关于16号节点的询问,我们可以知道的是:
        1.如果询问的另一个节点属于集合{8,1,13,14} 那么此时 16节点与另一节点的LCA为 红色节点集合的代表元 -- 8
        2.如果询问的另一个节点属于集合{4,6,15,7}那么此时16节点与另一节点的LCA为黄色节点的代表元 -- 4
        3.如果询问的另一个节点属于集合{10,2}那么此时16节点与另一节点的LCA为蓝色节点的代表元 -- 10
        4.如果询问的另一个节点不属于以上集合,我们尚且不知道16节点与另一节点的LCA是什么。

       并且需要注意的是,对以当前节点为根的子树dfs进行完毕之后需要把当前节点与当前节点父节点的集合进行合并,用以扩充当前活动活动路径上集合的划分。集合的操作我们利用并查集就能十分高效的完成。 以下给出代码:

       
#include<iostream>
#include<string>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
struct node{
	int nex,vi;
};
int head_tree[10005];
int cnt_tree;
node edge_tree[20005];

int head_quer[10005];
int cnt_quer;
node edge_quer[3];

int fah[10005];
bool vised[10005];
int n;
bool root[10005];
int root_fin;
int ans;

void init(){
	cnt_tree=0;
	memset(head_tree,0,sizeof(head_tree));
	cnt_quer=0;
	memset(head_quer,0,sizeof(head_quer));
	memset(fah,-1,sizeof(fah));
	memset(root,0,sizeof(root));
	memset(vised,0,sizeof(vised));
}

void addedge(int ui,int vi,int head[],node edge[],int &cnt){
	cnt++;edge[cnt].vi=vi;edge[cnt].nex=head[ui];head[ui]=cnt;
}

int getfath(int now){
	if (fah[now]!=now)
		fah[now]=getfath(fah[now]);
	return (fah[now]);
}

int uni(int sec,int rot){
	int fah_sec=getfath(sec);
	int fah_rot=getfath(rot);
	if (fah_sec==fah_rot)
		return (0);
	fah[fah_sec]=fah_rot;
	return (1);
}

void tarjan_lca(int now){
	fah[now]=now;
	for (int i=head_tree[now];i!=0;i=edge_tree[i].nex)
		if (!vised[edge_tree[i].vi]){
			tarjan_lca(edge_tree[i].vi);
			uni(edge_tree[i].vi,now);
		}
	for (int i=head_quer[now];i!=0;i=edge_tree[i].nex)
		if (vised[edge_quer[i].vi])
			ans=getfath(edge_quer[i].vi);
	vised[now]=true;
}

int main(){
	int t;
	scanf("%d",&t);
	for (int cas=1;cas<=t;cas++){
		init();
		scanf("%d",&n);
		for (int i=0;i<n-1;i++){
			int ui,vi;
			scanf("%d%d",&ui,&vi);
			root[vi]=true;
			addedge(ui,vi,head_tree,edge_tree,cnt_tree);
		}
		int ui,vi;
		scanf("%d%d",&ui,&vi);
		addedge(ui,vi,head_quer,edge_quer,cnt_quer);
		addedge(vi,ui,head_quer,edge_quer,cnt_quer);
		root_fin=-1;
		for (int i=1;i<=n;i++)
			if (!root[i]){
				root_fin=i;
				break;
			}
		ans=-1;
		tarjan_lca(root_fin);
		printf("%d\n",ans);
	}
}

       虽然写的丑,但是各位看官就姑且看看呗~233333.


       二、现在说说在线的LCA解法,在线的LCA比较常用的是RMQ的做法,但是因为写线段树写的比较经常所以还是决定用线段树来写,无论是RMQ还是线段树,我们只需要知道在线LCA的原理就是利用两个数组:1.深度数组 2.标记数组来完成的。

       很常规的想法,我们如果采用线段树的写法我们需要一个有效的序列,如何将一棵树转化成一个有效的序列呢?比较经常采用的办法无非就是通过遍历这棵树,遍历过程所产生的轨迹作为这个序列。而我们将要介绍一种序列,这种序列也是通过遍历得到的,但是和先序或者后序不同。我们将每经过一次某个节点就先序列中进行一次体现。如样例中的序列就应该是:

      ID: 8 1 13 1 14 1 8 4 10 2 10 11 10 16 12 16 3 16 10 4 6 7 6 15 6 4 8 5 9 5 8 4

   DEP: 1 2 3   2 3   2 1 2 3   4 3   4   3   4   5   4   5 4   3   2 3 4 3 4   3 2 1 2 3 2 1 4

      我们如果要查询的是11号节点与6号节点的LCA,我们只要查询11编号出现最后一次与6编号出现最后一次之间的深度数组中最小深度所对应的结点编号,这个编号即是他们的LCA。我们可以通过反证法证明该结论。因此这个线段树的模型也就呼之欲出了,下面给出在线LCA的代码:


      
#include<iostream>
#include<string>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
struct node{
	int nex,vi;
};
int head[10005];
int cnt;
node edge[20005];

void addedge(int ui,int vi){
	cnt++;edge[cnt].vi=vi;edge[cnt].nex=head[ui];head[ui]=cnt;
}

struct intp{
	int dep,id;
};
intp nows[20005];
int cnt_nows;
int pos[10005][2];
bool root[10005];
int n;

void init(){
	cnt_nows=cnt=0;
	memset(head,0,sizeof(head));
	memset(root,0,sizeof(root));
}
struct segment_tree{
	int tr_node[80005];
	void creat_tree(int id,int li,int ri){
		int mid=(li+ri)>>1;
		if (li==ri){
			tr_node[id]=mid;
			return;
		}
		creat_tree(id<<1,li,mid);
		creat_tree(id<<1|1,mid+1,ri);
		if (nows[tr_node[id<<1]].dep<nows[tr_node[id<<1|1]].dep)
			tr_node[id]=tr_node[id<<1];
		else
			tr_node[id]=tr_node[id<<1|1];
		return;
	}
	int find_min(int id,int ldes,int rdes,int lnow,int rnow){
		int mid=(lnow+rnow)>>1;
		intp ret;
		if (ldes==lnow && rdes==rnow){
			return (tr_node[id]);
		}
		if (rdes<=mid)
			return (find_min(id<<1,ldes,rdes,lnow,mid));
		else if (mid<ldes)
			return (find_min(id<<1|1,ldes,rdes,mid+1,rnow));
		else{
			int ret1=find_min(id<<1,ldes,mid,lnow,mid);
			int ret2=find_min(id<<1|1,mid+1,rdes,mid+1,rnow);
			if (nows[ret1].dep<nows[ret2].dep)
				return (ret1);
			else
				return (ret2);
		}
	}
};
void dfs(int now,int dep){
	cnt_nows++;
	nows[cnt_nows].dep=dep;
	nows[cnt_nows].id=now;
	pos[now][0]=pos[now][1]=cnt_nows;
	for (int i=head[now];i!=0;i=edge[i].nex){
		dfs(edge[i].vi,dep+1);
		cnt_nows++;
		nows[cnt_nows].dep=dep;
		nows[cnt_nows].id=now;
		pos[now][1]=cnt_nows;
	}
}
int main(){
	int t;
	segment_tree seg;
	scanf("%d",&t);
	for (int cas=1;cas<=t;cas++){
		scanf("%d",&n);
		init();
		for (int i=0;i<n-1;i++){
			int ui,vi;
			scanf("%d%d",&ui,&vi);
			addedge(ui,vi);
			root[vi]=true;
		}
		memset(pos,0,sizeof(pos));
		int root_fin;
		for (int i=1;i<=n;i++)
			if (root[i]==false){
				root_fin=i;
				break;
			}
		dfs(root_fin,1);
		seg.creat_tree(1,1,cnt_nows);
		int ui,vi;
		scanf("%d%d",&ui,&vi);
		int li,ri;
		li=min(pos[ui][1],pos[vi][1]);
		ri=max(pos[ui][1],pos[vi][1]);
		/*for (int i=1;i<=cnt_nows;i++)
			printf("%d ",nows[i].dep);*/
		int fah=seg.find_min(1,li,ri,1,cnt_nows);
		printf("%d\n",nows[fah].id);
	}
}

       好啦 今天搞搞居然也就10点半了!后面的话慢慢把准备工作做完~就继续朝着bzoj前进~  噗对了我应该先把10.漆子超《分治算法在树的路径问题中的应用》里面的题目先写完才对 【捂脸~  以上!
       
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值