【概念】
給定一棵樹,若節點 z 既是結點 x 的祖先,也是節點 y 的祖先,並且在 x 和 y 的祖先中深度最大,稱爲 x , y 的 最近公共祖先(Lowest Common Ancestors)。
【算法】
1.向上標記法
從 x 向上走到根節點,並標記所有經過的點。
從 y 向上走到根節點,第一次遇到的已標記的節點即爲 x 與 y 的 最近公共祖先 。
對於每個詢問的時間複雜度爲 O(n)。
2.樹上倍增法
設 f[i][j] 代表從編號爲 i 的節點向上走 2^j 步到達的節點編號爲 f[i][j] 。若該節點不存在,則令 f[i][j]=0 (令 f[i][j]=-1 也可以,但是需要注意以 f[i][j] 作下標可能越界)。
若 x 與 y 不在同一深度,則利用二進制拆分思想 將 x 與 y 調整至同一深度 。
若 此時 x = y ,則已經找到 LCA, LCA(x,y)= x 。
否則 用二進制拆分思想,將 x 與 y 同時向上調整,並保持 x!= y 。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
設 x , y 向上移動D步所到達的節點爲他們最近公的共祖先。
移動 x ,y 我們可以發現:
當已經枚舉到第 k 位,通過一系列累加和(0/1*2^max_k+0/1*2^(max_k-1)+.....0/1*2^(k+1) )所得到的和爲D'。
若 D'+2^k < D 則 x, y 向上走 D'+2^k 步到達的節點不是他們的公共祖先,即 f[x][k] != f[y][k] (x 與 y 在不斷往上更新)。
若D'+2^k >= D 則 x, y 向上走D'+2^k 步到達的節點時他們的公共祖先,即 f[x][k] = f[y][k]
所以,當 f[x][k] != f[y][k] 時 x=f[x][k],y=f[y][k] (相當於D'+2^k) 我們保證了最後求的的 D' 比 D 小 1。
時間複雜度爲 O((n+m)log n)。
<代碼實現>
void bfs(int x)
{
queue<int> q;
q.push(x);
for(;!q.empty();)
{
int X=q.front();
q.pop();
for(int i=lin[X];i;i=e[i].next)
{
if(e[i].id==fa[X][0]) continue;//避免重複查詢
q.push(e[i].id);
d[e[i].id]=d[X]+1;//該節點深度爲父親節點的深度 +1
}
}
}//求每個節點的深度( d 記錄深度)
void lca(int x,int y)
{
for(int i=1;i<=n;i++)
if(!fa[i][0])
{
d[i]=0;
bfs(i);
for(int i=1;(1<<i)<n;i++)
{
for(int j=1;j<=n;j++)
{
if(fa[j][i-1]<0) fa[j][i]=-1;//如果 f[j][i] 不成立(即 j 向上 2^i 沒有節點)
else fa[j][i]=fa[fa[j][i-1]][i-1];// 否則 j 向上走 2^i 即爲向上走 2^(i-1)*2
}
}
break;
}//
if(d[x]>d[y]) x^=y^=x^=y;// y 的深度比 x 的深度大(方便操作)
for(int D=d[y]-d[x],i=0;D;D>>=1,i++)
if(D&1)
y=fa[y][i];//二進制拆分往上挪
if(x==y)
{
cout<<x<<endl;
return ;
}
int N=n;
int k=0;
for(;N;)
{
k++;
N/=2;
}// k 即爲最大向上走 2^k 步
for(int i=k;i>=0;i--)
if(fa[x][i]!=fa[y][i])
{
x=fa[x][i];
y=fa[y][i];
}
cout<<fa[x][0]<<endl;
return ;
}
3.Tarjan算法(離線)
Tarjan算法基本上使用並查集對“向上標記法”的優化。將 m 個詢問一次性讀入,統一計算。
時間複雜度爲:O(n+m)。
<代碼實現>
void Tarjan(int x)
{
vis[x]=1;
for(int i=lin[x];i;i=e[i].next)
{
if(vis[e[i].id]) continue;
Tarjan(e[i].id);
fa[e[i].id]=x;
}//遍歷
for(int i=l[x];i;i=q[i].next)
{
if(!vis[q[i].id]) continue;
int X=getfa(q[i].id);
ans[q[i].qu]=X;//q[i].qu 代表此時查詢的是第 qu[i].qu 個問題
}//查詢
}
// 初始值:fa[i]=i
【例題】
1> Poj1330 Nearest Common Ancestors
題目大意:給一個n個點的無根樹,詢問某兩個點的最近公共祖先。
題解:LCA模板。
AC次數:2 。錯誤原因:數組開太小 (建無向邊時數組大小*2很重要)。
<倍增>
#include<bits/stdc++.h>
using namespace std;
const int maxx=10000;
int n;
int lin[maxx+10]={};
int d[maxx+10]={};
int fa[maxx+10][22]={};
struct kk
{
int id;
int next;
} e[maxx*2+10]={};
int num=0;
int s,t;
void insert(int x,int y)
{
e[++num].next=lin[x];
lin[x]=num;
e[num].id=y;
}
void bfs(int x)
{
queue<int> q;
q.push(x);
for(;!q.empty();)
{
int X=q.front();
q.pop();
for(int i=lin[X];i;i=e[i].next)
{
if(e[i].id==fa[X][0]) continue;
q.push(e[i].id);
d[e[i].id]=d[X]+1;
}
}
}
void lca(int u,int v)
{
for(int i=1;i<=n;i++)
if(!fa[i][0])
{
d[i]=0;
bfs(i);
for(int i=1;(1<<i)<n;i++)
{
for(int j=1;j<=n;j++)
{
if(fa[j][i-1]<0) fa[j][i]=-1;
else fa[j][i]=fa[fa[j][i-1]][i-1];
}
}
break;
}
if(d[u]>d[v]) u^=v^=u^=v;
for(int D=d[v]-d[u],i=0;D;D>>=1,i++)
if(D&1)
v=fa[v][i];
if(u==v)
{
cout<<u<<endl;
return ;
}
int N=n;
int k=0;
for(;N;)
{
k++;
N/=2;
}
for(int i=k;i>=0;i--)
if(fa[u][i]!=fa[v][i])
{
u=fa[u][i];
v=fa[v][i];
}
cout<<fa[u][0]<<endl;
return ;
}
int main()
{
int T;
cin>>T;
for(int I=1;I<=T;I++)
{
memset(fa,0,sizeof(fa));
memset(lin,0,sizeof(lin));
num=0;
cin>>n;
for(int i=1;i<=n-1;i++)
{
int x,y;
cin>>x>>y;
insert(x,y);
insert(y,x);
fa[y][0]=x;
}
cin>>s>>t;
lca(s,t);
}
return 0;
}
題目大意:給一個n個點的無根樹,有q個詢問,每個詢問兩個點,問兩點的距離。
題解:樹上點距模板。求 x,y 的距離爲 dist[x]+dist[y]-2*dist[lca(x,y)] 。
AC次數:1 。
<tarjan>
#include<bits/stdc++.h>
using namespace std;
const int max_p=40000,max_q=200;
int n,m;
int lin[max_p+10]={};
struct kk
{
int id;
int next;
int v;
} e[max_p*2+10]={};
int l[max_p+10]={};
struct kkk
{
int id;
int next;
int q;
} qu[max_q*2+10]={};
int num=0;
int fa[max_p+10]={};
int vis[max_p+10]={};
int ans[max_p+10]={};
int dis[max_p+10]={};
void insert(int x,int y,int v)
{
e[++num].next=lin[x];
lin[x]=num;
e[num].id=y;
e[num].v=v;
}
void insert2(int x,int y,int q)
{
qu[++num].next=l[x];
l[x]=num;
qu[num].id=y;
qu[num].q=q;
}
int getfa(int x){ return x==fa[x] ? x : fa[x]=getfa(fa[x]); }
void Tarjan(int x)
{
vis[x]=1;
for(int i=lin[x];i;i=e[i].next)
{
if(vis[e[i].id]) continue;
dis[e[i].id]=dis[x]+e[i].v;
Tarjan(e[i].id);
fa[e[i].id]=x;
}
for(int i=l[x];i;i=qu[i].next)
{
if(vis[qu[i].id])
{
int X=getfa(qu[i].id);
ans[qu[i].q]=dis[x]+dis[qu[i].id]-2*dis[X];
}
}
return ;
}
int main()
{
int T;
cin>>T;
for(int I=1;I<=T;I++)
{
cin>>n>>m;
for(int i=1;i<=n;i++)
fa[i]=i,dis[i]=vis[i]=ans[i]=lin[i]=l[i]=0;
num=0;
for(int i=1;i<=n-1;i++)
{
int x,y,v;
cin>>x>>y>>v;
insert(x,y,v);
insert(y,x,v);
}
num=0;
for(int i=1;i<=m;i++)
{
int x,y;
cin>>x>>y;
insert2(x,y,i);
insert2(y,x,i);
}
Tarjan(1);
for(int i=1;i<=m;i++)
cout<<ans[i]<<endl;
}
return 0;
}
題目大意:給定一棵大小爲n的樹,有m組詢問,每組詢問給三個點x, y, z,求到這三個點距離和最小的點及最小距離和。
題解:對於每兩個點進行求一次LCA。有兩種情況:如果每組的LCA相同,即輸出;否則輸出只有出現一次的LCA。
AC次數:1 。
#include<bits/stdc++.h>
using namespace std;
const int maxx=500000;
int n,m;
int lin[maxx+10]={};
struct kk
{
int id;
int next;
int v;
} e[maxx*2+10]={};
int l[maxx+10]={};
struct kkk
{
int id;
int next;
int q;
int t;
} qu[maxx*6+10]={};
int num=0;
int fa[maxx+10]={};
int vis[maxx+10]={};
int Vis[maxx+10][4]={};
int ans[maxx+10][4][2]={};
int dis[maxx+10]={};
void insert(int x,int y,int v)
{
e[++num].next=lin[x];
lin[x]=num;
e[num].id=y;
e[num].v=v;
}
void insert2(int x,int y,int q,int t)
{
qu[++num].next=l[x];
l[x]=num;
qu[num].id=y;
qu[num].q=q;
qu[num].t=t;
}
int getfa(int x){ return x==fa[x] ? x : fa[x]=getfa(fa[x]); }
void Tarjan(int x)
{
vis[x]=1;
for(int i=lin[x];i;i=e[i].next)
{
if(vis[e[i].id]) continue;
dis[e[i].id]=dis[x]+e[i].v;
Tarjan(e[i].id);
fa[e[i].id]=x;
}
for(int i=l[x];i;i=qu[i].next)
{
if(vis[qu[i].id]&&!Vis[qu[i].q][qu[i].t])
{
int X=getfa(qu[i].id);
Vis[qu[i].q][qu[i].t]=1;
ans[qu[i].q][++ans[qu[i].q][0][0]][0]=X;
ans[qu[i].q][ans[qu[i].q][0][0]][1]=dis[x]+dis[qu[i].id]-2*dis[X];
}
}
return ;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=n-1;i++)
{
int x,y;
cin>>x>>y;
insert(x,y,1); insert(y,x,1);
}
num=0;
for(int i=1;i<=m;i++)
{
int a,b,c;
cin>>a>>b>>c;
insert2(a,b,i,1); insert2(b,a,i,1);
insert2(b,c,i,2); insert2(c,b,i,2);
insert2(a,c,i,3); insert2(c,a,i,3);
}
Tarjan(1);
for(int i=1;i<=m;i++)
{
int S=(ans[i][1][1]+ans[i][2][1]+ans[i][3][1])/2;
if(ans[i][1][0]==ans[i][2][0])
{
if(ans[i][1][0]==ans[i][3][0])
cout<<ans[i][2][0]<<' '<<S<<endl;
else
cout<<ans[i][3][0]<<' '<<S<<endl;
}
else
{
if(ans[i][1][0]==ans[i][3][0])
cout<<ans[i][2][0]<<' '<<S<<endl;
else
cout<<ans[i][1][0]<<' '<<S<<endl;
}
}
return 0;
}
題目大意:n個城市,m條邊相連,每次詢問兩點之間最大值最小是多少。
題解:最小生成樹+倍增。
AC次數:6 。
#include<bits/stdc++.h>
using namespace std;
const int max_n=50000,max_m=100000;
int n,m,k,K,num;
int lin[max_n+10]={},d[max_n+10]={},vis[max_n+10]={},fa[max_n+10]={},f[max_n+10][22],g[max_n+10][22];
struct kk
{
int id;
int next;
int v;
} e[max_n*2+10]={};
struct Kk
{
int u;
int v;
int d;
} E[max_m+10]={};
void insert(int x,int y,int v)
{
e[++num].next=lin[x];
lin[x]=num;
e[num].id=y;
e[num].v=v;
}
int getfa(int x){ return x==fa[x] ? x : fa[x]=getfa(fa[x]) ; }
bool mycmp(Kk x,Kk y){ return x.d<y.d ; }
void kru()
{
sort(E+1,E+m+1,mycmp);
for(int i=1;i<=m;i++)
{
int x=getfa(E[i].u);
int y=getfa(E[i].v);
if(x!=y)
{
fa[x]=y;
insert(E[i].u,E[i].v,E[i].d);
insert(E[i].v,E[i].u,E[i].d);
}
}
return ;
}
void dfs(int x)
{
for(int i=lin[x];i;i=e[i].next)
{
if(!vis[e[i].id])
{
f[e[i].id][0]=x;
g[e[i].id][0]=e[i].v;
d[e[i].id]=d[x]+1;
vis[e[i].id]=1;
dfs(e[i].id);
}
}
}
int lca(int x,int y)
{
int ans=0;
if(d[x]>d[y]) x^=y^=x^=y;
for(int D=d[y]-d[x],i=0;D;D>>=1,i++)
if(D&1) ans=max(ans,g[y][i]),y=f[y][i];
if(x==y) return ans;
for(int i=K;i>=0;i--)
if(f[x][i]!=f[y][i])
ans=max(ans,max(g[x][i],g[y][i])),x=f[x][i],y=f[y][i];
ans=max(ans,max(g[x][0],g[y][0]));
return ans;
}
void find()
{
kru();
vis[1]=1,d[1]=0;
dfs(1);
int N=n;
for(;N;) N/=2,K++;
for(int i=1;i<=K;i++)
for(int j=1;j<=n;j++)
f[j][i]=f[f[j][i-1]][i-1],g[j][i]=max(g[j][i-1],g[f[j][i-1]][i-1]);
}
void clen()
{
num=0,K=0;
for(int i=1;i<=n;i++)
vis[i]=0,fa[i]=i,lin[i]=0;
}
int main()
{
for(int I=0;scanf("%d%d",&n,&m)!=EOF;I++)
{
if(I) cout<<endl;
clen();
for(int i=1;i<=m;i++) scanf("%d%d%d",&E[i].u,&E[i].v,&E[i].d);
find();
scanf("%d",&k);
int s,t;
for(int i=1;i<=k;i++)
{
scanf("%d%d",&s,&t);
int X=lca(s,t);
printf("%d\n",X);
}
}
return 0;
}
5.次小生成樹
題目大意:求嚴格的次小生成樹。
題解:每次選擇一條邊加入,然後刪去這條邊兩端端點 x,y 的 LCA 的子樹裏的最大或次大邊(存在相同的情況)。
AC次數:6 。(判斷錯誤,數組太小)
#include<bits/stdc++.h>
using namespace std;
const int max_n=100000,max_m=300000;
int n,m,num;
long long ans;
int lin[max_n+10],fa[max_n+10],vis[max_n+10],f[max_n+10][22],g[max_n+10][22][2],d[max_n+10],cnt[max_n+10];
struct kk
{
int id;
int next;
int v;
} e[max_n*2+10]={};
struct Kk
{
int u,v;
int w;
} E[max_m+10]={};
bool mycmp(Kk x,Kk y){ return x.w<y.w ; }
void insert(int x,int y,int v){ e[++num].next=lin[x]; lin[x]=num; e[num].id=y; e[num].v=v; }
int getfa(int x){ return x==fa[x] ? x : fa[x]=getfa(fa[x]) ; }
void dfs(int x)
{
for(int i=lin[x];i;i=e[i].next)
{
if(e[i].id==f[x][0]) continue;
f[e[i].id][0]=x;
g[e[i].id][0][0]=e[i].v;
d[e[i].id]=d[x]+1;
dfs(e[i].id);
}
}
int lca(int t)
{
int x=E[t].u,y=E[t].v;
if(d[x]>d[y]) x^=y^=x^=y;
for(int D=d[y]-d[x],i=0;D;D>>=1,i++)
if(D&1) y=f[y][i];
if(x==y) return x;
int k=0,N=n;
for(;N;) N/=2,k++;
for(int i=k;i>=0;i--)
if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
return f[x][0];
}
int ches(int x,int y,int v)
{
if(d[x]>d[y]) x^=y^=x^=y;
int max1,max2;
max1=max2=0;
for(int D=d[y]-d[x],i=0;D;D>>=1,i++)
if(D&1){
if(g[y][i][0]>max1)
{
max2=max(max1,g[y][i][1]);
max1=g[y][i][0];
}
else
if(g[y][i][0]==max1)
max2=max(max2,g[y][i][1]);
else
max2=max(max2,g[y][i][0]);
y=f[y][i];
}
if(max1!=v) return v-max1;
else return v-max2;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++) scanf("%d%d%d",&E[i].u,&E[i].v,&E[i].w);
sort(E+1,E+m+1,mycmp);
for(int i=1;i<=n;i++) fa[i]=i;
int Cnt=0;
for(int i=1;i<=m;i++)
{
int x=getfa(E[i].u);
int y=getfa(E[i].v);
if(x!=y)
{
fa[x]=y,ans+=(long long)E[i].w;
insert(E[i].u,E[i].v,E[i].w);
insert(E[i].v,E[i].u,E[i].w);
cnt[i]++;
Cnt++;
if(Cnt>=n-1) break;
}
}
f[1][0]=-1;
dfs(1);
for(int i=1;(1<<i)<n;i++)
for(int j=1;j<=n;j++)
if(f[j][i-1]<0) f[j][i]=-1;
else{
f[j][i]=f[f[j][i-1]][i-1];
if(g[j][i-1][0]==g[f[j][i-1]][i-1][0])
{
g[j][i][0]=g[j][i-1][0];
g[j][i][1]=max(g[j][i-1][1],g[f[j][i-1]][i-1][1]);
}
else
{
if(g[j][i-1][0]>g[f[j][i-1]][i-1][0])
{
g[j][i][0]=g[j][i-1][0];
g[j][i][1]=max(g[f[j][i-1]][i-1][0],g[j][i-1][1]);
}
else
{
g[j][i][0]=g[f[j][i-1]][i-1][0];
g[j][i][1]=max(g[j][i-1][0],g[f[j][i-1]][i-1][1]);
}
}
}
long long ad=E[m].w;
for(int i=1;i<=m;i++)
if(!cnt[i])
{
int x=lca(i);
ad=min(ad,(long long)ches(E[i].u,x,E[i].w));
ad=min(ad,(long long)ches(E[i].v,x,E[i].w));
}
printf("%lld\n",ans+ad);
return 0;
}
(並不明顯的)小結:數組大小很重要!!!