最近公共祖先问题:给定一颗有根树,求其两个节点最近的公共祖先;节点的祖先即从节点至根的路径上的节点的集合。
朴素算法:从u的父亲开始顺着树往上枚举u的一个祖先并保存在一个列表L中,同样枚举v的祖先,当发现v的祖先第一次出现在u的祖先中,输出。
在线LCA算法:令L(u)为u的深度,设L(u)<L(v),若u是v的父亲,LCA(u,v)=u,否则:LCA(u,v)=LCA(u,father(v));
LCA的离线(Tarjan)算法:利用DFS+并查集,算法流程:对于新搜索到的一个结点,首先创建由这个结点构成的集合,再对当前结点的每一个子树进行搜索,每搜索完一棵子树,则可确定子树内的LCA询问都已解决。其他的LCA询问的结果必然在这个子树之外,这时把子树所形成的集合与当前结点的集合合并,并将当前结点设为这个集合的祖先。之后继续搜索下一棵子树,直到当前结点的所有子树搜索完。这时把当前结点也设为已被检查过的,同时可以处理有关当前结点的LCA询问,如果有一个从当前结点到结点v的询问,且v已被检查过,则由于进行的是深度优先搜索,当前结点与v的最近公共祖先一定还没有被检查,而这个最近公共祖先的包涵v的子树一定已经搜索过了,那么这个最近公共祖先一定是v所在集合的祖先。算法伪代码如下:
void LCA(u) { MAKE-SET(u);//建议一个集合,只包含元素u,且u为元素的代表元 ancestor[Find-SET(u)]=u; for(u的每一个孩子v) { LCA(v); Union(u,v); ancestor[Find-SET(u)]=u; } color[u]=black; for(Q(u)中的所有元素v) // (u,v)是被询问的点对 { if(color[v]==black) { ancestor[Find-SET(v)](u,v的最近公共祖先); } } }
POJ 1330题目链接:http://poj.org/problem?id=1330现在的问题是建立一个包含元素u的集合,可以使用邻接表存储。至于什么是邻接表,数据结构上有,建议用向量作邻接表:
向量做法:
#include<iostream> #include<cstring> #include<vector> #include<cstdio> using namespace std; const int MAX=10010; #define CLR(arr,val) memset(arr,val,sizeof(arr)) int n,father[MAX],rank[MAX],ancestors[MAX],Indegree[MAX]; vector<int> Adj[MAX],temp[MAX]; bool visited[MAX]; void Init() { for(int i=0;i<MAX;i++) { father[i]=i; Adj[i].clear(); temp[i].clear(); } fill(rank,rank+MAX,1); CLR(visited,false); } int Find(int u) { return father[u]==u?u:Find(father[u]); } void Union(int u,int v) { u=Find(u); v=Find(v); if(u==v) return ; if(rank[u]>rank[v]) { rank[u]+=rank[v]; father[v]=u; } else { rank[v]+=rank[u]; father[u]=v; } } void LCA(int u) { ancestors[Find(u)]=u;//设定自己为祖先 for(vector<int>::size_type i=0;i<Adj[u].size();i++) { LCA(Adj[u][i]);//DFS~~~ Union(u,Adj[u][i]); ancestors[Find(u)]=u; } visited[u]=true; for(vector<int>::size_type i=0;i<temp[u].size();i++) { if(visited[temp[u][i]]) cout<<ancestors[Find(temp[u][i])]<<endl; } } int main() { int k,u,v; scanf("%d",&k); while(k--) { scanf("%d",&n); Init(); for(int i=0;i<n-1;i++) { scanf("%d%d",&u,&v); Adj[u].push_back(v); Indegree[v]++; } scanf("%d%d",&u,&v); temp[u].push_back(v); temp[v].push_back(u); for(int i=1;i<=n;i++) if(!Indegree[i]) { LCA(i);//找到树根 break; } } return 0; }
邻接表的用处比较的多,写一下关于邻接表的知识,邻接表是图的一种链式存储结构,它对于图中的每个顶点建立一个单链表,第i个单链表中的结点表示依附于顶点vi的边(对有向图是以顶点vi为尾的弧)。
下面是用邻接表的做法:
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int MAX=10010; #define CLR(arr,val) memset(arr,val,sizeof(arr)) int n,Indegree[MAX],ancestors[MAX]; bool visited[MAX]; class UnionFindSet { public: void Union(int u,int v) { u=Find(u),v=Find(v); if(u==v) return ; if(rank[u]>rank[v]) { rank[u]+=rank[v]; father[v]=u; } else { rank[v]+=rank[u]; father[u]=v; } } int Find(int u) { return father[u]==-1?u:Find(father[u]); } void Init() { CLR(father,-1); fill(rank,rank+MAX,1); } private: int father[MAX]; int rank[MAX]; }U; struct ArcNode{ void Link(int u,int v) { next[num]=prior[u]; data[num]=v; prior[u]=num++; } void Init() { CLR(prior,-1);num=0;} int prior[MAX],next[MAX],data[MAX],num; //prior[]该弧所指向的顶点的位置,即邻接点,next[]指向下一条弧或结点,data[]为顶点信息 }Arc,Que; void LCA(int u) { ancestors[U.Find(u)]=u; for(int i=Arc.prior[u];i!=-1;i=Arc.next[i]) { LCA(Arc.data[i]); U.Union(u,Arc.data[i]); ancestors[U.Find(u)]=u; } visited[u]=true; for(int i=Que.prior[u];i!=-1;i=Que.next[i]) { if(visited[Que.data[i]]) cout<<ancestors[U.Find(Que.data[i])]<<endl; } } int main() { int u,v,n,k; scanf("%d",&k); while(k--) { U.Init(); Arc.Init(); Que.Init(); CLR(Indegree,0); CLR(visited,false); scanf("%d",&n); for(int i=0;i<n-1;i++) { scanf("%d%d",&u,&v); Arc.Link(u,v); Indegree[v]++; } scanf("%d%d",&u,&v); Que.Link(u,v); Que.Link(v,u); for(int i=1;i<=n;i++) if(!Indegree[i]) {LCA(i); break; } } return 0; }
邻接表模板,转载自:http://blog.acmol.com/
template<int Max,int MaxE> struct Graph { void add(int u,int v,int len) { Next[top]=Head[u]; Len[top]=len; //用来保存权值 Num[top]=v; Head[u]=top++; } void init() { CLR(Head,-1);top=0; } int Head[Max],Next[MaxE],Num[MaxE],top,Len[MaxE]; };
不过邻接表模板会有一些小变种:
一、在有些图中不需要存边的权值,这时可以把Len数组省去,并且add函数中不需要len这个参数了。
二、有时候需要用到某个点出发的边的条数。可以用一个Size数组记录一下,add时增加一下Size数组的值即可。
template<int Max,int MaxE> struct Graph2 { void add(int u,int v) { Next[top]=Head[u]; Num[top]=v; Head[u]=top++; Size[u]++; } void init() { CLR(Head,-1);CLR(Size,0);top=0; } int Head[Max],Next[MaxE],Num[MaxE],Size[Max],top; };
三、在最大流中,需要每次加正向边的同时加一条容量为0的反向边也。
四、在最小费用最大流中,一般需要加一个容量为0,费用为正向边费用相反数的边。
涉及到LCA问题的题目:POJ 1470,1986,3237,3728,.3417,HDU 2586,1269,2874,3380等~