算法:最近公共祖先(LCA)

有根树中,每一个点都有好几个祖先(在往根节点走的过程中遇到的都是它的祖先),一般化,把本身也称为它的祖先

对于两个点,离它们最近的一个公共祖先被称为最近公共祖先

法一:向上标记法(暴力做法)O(n)不常用

对于其中一个点,在走到根节点的过程中标记走过的点,然后另一个点开始往根节点走,走到第一个被标记过的点即为这两个点的最近公共祖先

法二:倍增法

预处理每个点向上走2^k步的节点,f[i,j]表示从i开始,向上走2^j步所能走到的节点(j大于等于0,j小于等于logn)

通过递推的方式来求

当j为0时,f(i,j)=i的父节点

当j大于0时,f(i,j)=f(f(i,j-1),j-1)

depth[i]表示深度

哨兵:如果从i开始跳2^j步会跳过根节点,那么f[i,j]=0,depth[0]=0

二进制拼凑:

比如已经有1,2,4,8,16

要想拼凑11,首先从大往小看,11小于16,不可行,11大于8,那么由于从大到小8是第一个出现,那么8可行,11减去8得3,第一个满足小于等于3的是2,所以2可行,3减2得1,第一个满足小于等于1的是1,所以1可行

利用这个思想可以把x和y跳到一个相同的位置上

x和y相差depth[x]-depth[y]步,用2的次幂拼凑出它们之间的距离

如果depth[f(x,k)]大于等于depth[y],那么表示跳2^k步之后到达的点还是在y的下面的,那么就可以作为拼凑的一部分,可以跳2^k步

当f(a,k)等于f(b,k)时,不一定到达的是最近的公共祖先,有可能到达的是最近公共祖先的上面一个点

当f(a,k)不等于f(b,k)时,说明还没有走到a,b的最近公共祖先

预处理O(nlogn)

查询 O(logn)

步骤:

1.先将两个点跳到同一层(距根节点的距离相同)

2.如果两个点此时不在同一个位置,那么让两个点同时往上跳,一直跳到它们的最近公共祖先的下一层(即它们的最近公共祖先的儿子节点)

例1:祖孙询问 

算法:最近公共祖先(LCA)_第1张图片 

算法:最近公共祖先(LCA)_第2张图片 

AC代码: 

#include
#include
#include
using namespace std;
const int N=40010,M=2*N;
int h[N],e[M],ne[M],idx;
int depth[N],fa[N][16];
int q[N];
int n,m;
void add(int a,int b){
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void bfs(int root){
    memset(depth,0x3f,sizeof depth);
    depth[0]=-1,depth[root]=0;
    int hh=0,tt=0;
    q[0]=root;
    while(hh<=tt){
        int t=q[hh++];
        for(int i=h[t];i!=-1;i=ne[i]){
            int j=e[i];
            if(depth[j]>depth[t]+1){
                depth[j]=depth[t]+1;
                q[++tt]=j;
                fa[j][0]=t;//j跳一步跳到t点,即t为j的父节点
                for(int k=1;k<=15;k++) fa[j][k]=fa[fa[j][k-1]][k-1];//预处理点j跳2^0,2^1,...2^15跳到哪个点
            }
        }
    }
}
int lca(int a,int b){
    if(depth[a]=0;k--){
        if(depth[fa[a][k]]>=depth[b]) a=fa[a][k];//如果a跳2^k步还在b的下面,那么就可以跳,哨兵的好处在于当跳2^k步后跳出根节点,那么depth为0
    }
    //此时a和b已经在同一层了,即距离根节点的距离是相同的
    if(a==b) return a;//如果a等于b,那么a就是a和b的最近公共祖先
    //否则a和b同时往上跳
    for(int k=15;k>=0;k--){
        //如果跳到的点不是同一个点,那么说明还没有跳到最近公共祖先
        //哨兵的第二个好处在于当跳出根节点时,跳到的点为0,那么两者就相同了
        if(fa[a][k]!=fa[b][k]){
            a=fa[a][k];
            b=fa[b][k];
        }
    }
    //此时a和b跳到了它们最近公共祖先的儿子节点
    return fa[a][0];
}
int main()
{
    cin>>n;
    int root=0;
    memset(h,-1,sizeof h);
    for(int i=0;i>a>>b;
        if(b==-1) root=a;
        else add(a,b),add(b,a);
    }
    bfs(root);
    cin>>m;
    while(m--){
        int a,b;
        cin>>a>>b;
        int p=lca(a,b);
        if(p==a) cout<<1<

vector,queue版: 

#include
#define endl '\n'
//#define int long long
using namespace std;
const int N=4e4+10,M=17;
int fa[N][M];
int depth[N];
int n,m;
vector>e(N);
queueq;
void bfs(int root){
    memset(depth,0x3f,sizeof depth);
    depth[0]=-1,depth[root]=0;
    q.push(root);
    while(q.size()){
        int t=q.front();
        q.pop();
        for(auto v:e[t]){
            if(depth[v]>depth[t]+1){
                depth[v]=depth[t]+1;
                q.push(v);
                fa[v][0]=t;
                for(int k=1;k=0;k--){
        if(depth[fa[a][k]]>=depth[b]) a=fa[a][k];
    }
    if(a==b) return a;
    for(int k=16;k>=0;k--){
        if(fa[a][k]!=fa[b][k]){
            a=fa[a][k];
            b=fa[b][k];
        }
    }
    return fa[a][0];
}
void solve() {
    cin>>n;
    int root;
    for(int i=0;i>a>>b;
        if(b==-1) root=a;
        else {
            e[a].push_back(b);
            e[b].push_back(a);
        }
    }
    bfs(root);
    cin>>m;
    while(m--){
        int x,y;
        cin>>x>>y;
        int l=lca(x,y);
        if(l==x) cout<<1<>t;
    while(t--) {
        solve();
    }
    return 0;
}

法三:Tarjan----离线求LCA O(n+m)(向上标记法的优化)

在线:对于每一个询问单独处理

离线:对于所有询问,全部存起来,一起处理,再一起输出

在深度优先遍历时,将所有点分三大类:(1)已经遍历过,且回溯过的点 (2)正在搜索的分支 (3)还未搜索到的点

算法:最近公共祖先(LCA)_第3张图片

tarjan在存储询问的时候,正反都会存一次,当lca(u,v)等于u,v其中一个时,lca会求两次,其它情况lca求一次,正反都存一次的目的是为了防止漏求lca

例:

算法:最近公共祖先(LCA)_第4张图片

AC代码: 

#include
#include
#include
#include
using namespace std;
typedef pairPII;
const int N=20010,M=2*N;
int n,m;
int h[N],e[M],w[M],ne[M],idx;
int dist[N];
int p[N];
int res[N];
int vis[N];
vectorquery[N];//first存查询的另外一个点,second存查询编号
void add(int a,int b,int c){
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u,int fa){
    for(int i=h[u];i!=-1;i=ne[i]){
        int j=e[i];
        if(j==fa) continue;
        dist[j]=dist[u]+w[i];
        dfs(j,u);
    }
}
int find(int x){
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}
void tarjan(int u){
    vis[u]=1;//入u时,标记u
    for(int i=h[u];i!=-1;i=ne[i]){
        int j=e[i];
        if(!vis[j]){
            tarjan(j);
            p[j]=u;//回u时,v指向u,回u表示u往下搜完其中的一条路回到u
        }
    }
    //离u时,枚举LCA
    //离u表示u的子树已经全部搜完了
    for(auto item:query[u]){
        int y=item.first,id=item.second;
        if(vis[y]){
            int anc=find(y);
            res[id]=dist[u]+dist[y]-dist[anc]*2;
        }
    }
}
int main()
{
    cin>>n>>m;
    memset(h,-1,sizeof h);
    for(int i=0;i>a>>b>>c;
        add(a,b,c),add(b,a,c);
    }
    for(int i=0;i>a>>b;
        if(a!=b){
            query[a].push_back({b,i});
            query[b].push_back({a,i});
        }
    }
    for(int i=1;i<=n;i++) p[i]=i;
    dfs(1,-1);
    tarjan(1);
    for(int i=0;i

算法:最近公共祖先(LCA)_第5张图片 

算法:最近公共祖先(LCA)_第6张图片 

[BJOI2018] 求和 - 洛谷

该题是点前缀和

AC代码:

#include
#define endl '\n'
#define int long long
using namespace std;
const int N=3e5+10,mod=998244353;
int depth[N],fa[N][22];
int mi[60];//mi[j]表示depth[v]的j次幂
int s[N][60];//s[v][j]表示从根节点到v的路径节点的深度的j次幂之和
int n,m;
int root;
vector>e(N);
queueq;
void bfs(int root){
    memset(depth,0x3f,sizeof depth);
    depth[0]=-1,depth[root]=0;
    q.push(root);
    while(q.size()){
        int t=q.front();
        q.pop();
        for(auto v:e[t]){
            if(depth[v]>depth[t]+1){
                depth[v]=depth[t]+1;
                q.push(v);
                fa[v][0]=t;
                for(int k=1;k<=20;k++) fa[v][k]=fa[fa[v][k-1]][k-1];
                for(int j=1;j<=50;j++) mi[j]=mi[j-1]*depth[v]%mod;
                for(int j=1;j<=50;j++) s[v][j]=(mi[j]+s[t][j])%mod;
            }
        }
    }
}
int lca(int a,int b){
    if(depth[a]=0;k--){
        if(depth[fa[a][k]]>=depth[b]) a=fa[a][k];
    }
    if(a==b) return a;
    for(int k=20;k>=0;k--){
        if(fa[a][k]!=fa[b][k]){
            a=fa[a][k];
            b=fa[b][k];
        }
    }
    return fa[a][0];
}
void solve() {
    cin>>n;
    for(int i=0;i>a>>b;
        e[a].push_back(b);
        e[b].push_back(a);
    }
    root=1;
    mi[0]=1;
    bfs(root);
    cin>>m;
    for(int i=0;i>a>>b>>k;
        int l=lca(a,b);
        cout<<(s[a][k]+s[b][k]-s[l][k]-s[fa[l][0]][k]+2*mod)%mod<>t;
    while(t--) {
        solve();
    }
    return 0;
}

 树上差分

算法:最近公共祖先(LCA)_第7张图片 

 算法:最近公共祖先(LCA)_第8张图片

[USACO15DEC] Max Flow P - 洛谷

AC代码:

#include
#define endl '\n'
//#define int long long
using namespace std;
const int N=5e4+10;
int depth[N],fa[N][22];
int diff[N];//差分数组
int n,k;
int root;
int ans;
vector>e(N);
queueq;
void bfs(int root){
    memset(depth,0x3f,sizeof depth);
    depth[0]=-1,depth[root]=0;
    q.push(root);
    while(q.size()){
        int t=q.front();
        q.pop();
        for(auto v:e[t]){
            if(depth[v]>depth[t]+1){
                depth[v]=depth[t]+1;
                q.push(v);
                fa[v][0]=t;
                for(int k=1;k<=20;k++) fa[v][k]=fa[fa[v][k-1]][k-1];
            }
        }
    }
}
void dfs(int u,int fa){
    for(auto v:e[u]){
        if(v==fa) continue;
        dfs(v,u);
        diff[u]+=diff[v];
    }
    ans=max(ans,diff[u]);
}
int lca(int a,int b){
    if(depth[a]=0;k--){
        if(depth[fa[a][k]]>=depth[b]) a=fa[a][k];
    }
    if(a==b) return a;
    for(int k=20;k>=0;k--){
        if(fa[a][k]!=fa[b][k]){
            a=fa[a][k];
            b=fa[b][k];
        }
    }
    return fa[a][0];
}
void solve() {
    cin>>n>>k;
    for(int i=0;i>a>>b;
        e[a].push_back(b);
        e[b].push_back(a);
    }
    root=1;
    bfs(root);
    while(k--){
        int a,b;
        cin>>a>>b;
        int l=lca(a,b);
        diff[a]++,diff[b]++;
        diff[l]--,diff[fa[l][0]]--;
    }
    dfs(1,0);
    cout<>t;
    while(t--) {
        solve();
    }
    return 0;
}

你可能感兴趣的:(算法学习,算法,c++)