0x63.图论 - 树的直径与最近公共祖先

目录

  • 一、树的直径(Diameter)
    • 1.树形DP求树的直径
    • 2.两次BFS/DFS求树的直径
  • 1.POJ 1985.Cow Marathon(DFS求树的直径模板题)
  • 2.AcWing 350. 巡逻
  • 二、最近公共祖先( L C A LCA LCA
  • 1.树上倍增法
  • (1) P3379 【模板】最近公共祖先(LCA)
  • (2)HDOJ2586 How far away(LCA)
  • 2.LCA的Tarjan算法
  • 三、树上差分
  • 1.P3258 [JLOI2014]松鼠的新家(树上差分模板)
  • 2.POJ3417 闇の連鎖
  • 3.luogu P4556雨天的尾巴 (树上对点差分 + 动态开点 + 线段树合并)线段树合并模板离线在线详解
  • 四、LCA的综合应用

声明:
本系列博客是《算法竞赛进阶指南》+《算法竞赛入门经典》+《挑战程序设计竞赛》的学习笔记,主要是因为我三本都买了 按照《算法竞赛进阶指南》的目录顺序学习,包含书中的少部分重要知识点、例题解题报告及我个人的学习心得和对该算法的补充拓展,仅用于学习交流和复习,无任何商业用途。博客中部分内容来源于书本和网络(我尽量减少书中引用),由我个人整理总结(习题和代码可全都是我自己敲哒)部分内容由我个人编写而成,如果想要有更好的学习体验或者希望学习到更全面的知识,请于京东搜索购买正版图书:《算法竞赛进阶指南》——作者李煜东,强烈安利,好书不火系列,谢谢配合。


下方链接为学习笔记目录链接(中转站)

学习笔记目录链接


ACM-ICPC在线模板


一、树的直径(Diameter)

树上两点的距离定义为,从树上一点到另一点所经过的权值

当树上两点距离最大时,就称作树的直径,树的直径既可以指这个权值,也可以指这个路径 (路径也叫树的最长链)。

树的直径有两种方法,都是 O ( n ) O(n) O(n)的时间复杂度。

1.树形DP求树的直径

设以1号结点为根,那么n个结点n-1条边的无向图就可以看作一个有根树。
D [ x ] D[x] D[x]表示从结点x 出发走向以x为根的子树,能够到达的最远结点的距离。
设x的子结点为 y 1 , y 2 . . . y t , e d g e [ x , y ] y_1,y_2...y_t,edge[x,y] y1,y2...ytedge[x,y]表示边权
那么显然有:
D [ x ] = 1 ≤ i ≤ t m a x ( D [ y i ] + e d g e [ x , y i ] ) D[x] = ^{max}_{1≤i≤t}(D[y_i] + edge[x,y_i]) D[x]=1itmax(D[yi]+edge[x,yi])

F [ x ] F[x] F[x]为经过结点x的最长链的长度。
然后就是代码:

int ans;
int vis[N];
void dp(int u){
    vis[u] = 1;
    for(int i = head[u];i;i = nex[i]){
        int v = ver[i];
        if(vis[v])continue;
        dp(v);
        ans = max(ans,d[u] + d[v] + edge[i]);//看这条链是不是最大的
        d[x] = max(d[u],d[v] + edge[i]);//更新当前链长
    }
}

2.两次BFS/DFS求树的直径

我们可以先从任意一点开始DFS,记录下当前点所能到达的最远距离,这个点为P。

在从P开始DFS记录下所能达到的最远点的距离,这个点为Q。

P , Q P,Q P,Q就是直径的端点, d i s ( P , Q ) dis(P,Q) dis(P,Q)就是直径。
具体代码见下题

1.POJ 1985.Cow Marathon(DFS求树的直径模板题)

题意:有N个农田以及M条路,给出M条路的长度以及路的方向(这道题不影响,用不到),让你找到一条 两农田(任意的)间的路径,使得距离最长,并输出最长距离。
这里用dfs求直径,当然也可以用bfs和树形DP来做。

#include
#include
#include
#include
#include
#include
#include

#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;

typedef long long ll;
typedef pair<int,int> PII;
const int INF = 0x3f3f3f3f;
const int N = 5e5+7;
const int M = 2007;

int head[N],ver[N],tot,edge[N],nex[N];
int n,m,ans;
int dis[N],vis[N];

inline void add(int u,int v,int w){
    ver[++tot] = v;
    edge[tot] = w;
    nex[tot] = head[u];
    head[u] = tot;
}

//两次dfs一次求P一次求Q
void dfs(int u,int &ed){
    if(dis[u] > ans)ans = dis[u],ed = u;
    vis[u] = 1;
    for(int i = head[u];~i;i = nex[i]){
        int v = ver[i],w = edge[i];
        if(vis[v])continue;
        dis[v] = dis[u] + w;
        dfs(v,ed);
    }
    return ;
}


int p,q;
void solve(){
    dfs(1,p);
    ans = dis[p] = 0;
    memset(vis,0,sizeof vis);
    dfs(p,q);
    cout<<ans<<endl;
}
int main()
{
    while(scanf("%d%d",&n,&m) != EOF){
        memset(head,-1,sizeof head);
        memset(vis,0,sizeof vis);
        memset(dis,0,sizeof dis);
        tot = 0;
        over(i,1,m){
            int u,v,w;
            char ch[2];
            scanf("%d%d%d%s",&u,&v,&w,ch);
            add(u,v,w);
            add(v,u,w);
        }
        solve();
    }
return 0;
}

2.AcWing 350. 巡逻

0x63.图论 - 树的直径与最近公共祖先_第1张图片

我的博客详解:https://fanfansann.blog.csdn.net/article/details/106735359

二、最近公共祖先( L C A LCA LCA

搬一下我之前写的博客

LCA(Least Common Ancestors),即最近公共祖先,是指在有根树中,找出某两个结点u和v最近的公共祖先。 ———来自百度百科

0x63.图论 - 树的直径与最近公共祖先_第2张图片
比如这一颗二叉树,D和E的LCA很明显是根A,要注意的是D和B的LCA应该是B它本身

1.树上倍增法

F [ x , k ] F[x,k] F[x,k]表示x的 2 k 2^k 2k辈祖先,若该结点不存在则 F [ x , k ] = 0 F[x,k]=0 F[x,k]=0。除此之外, ∀ k ∈ [ 1 , l o g n ] , F [ x , k ] = F [ F [ x , k − 1 ] , k − 1 ] \forall k \in [1,logn],F[x,k] = F[F[x,k-1],k-1] k[1,logn],F[x,k]=F[F[x,k1],k1]
树上倍增法的预处理阶段时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),之后多次对于不同的x,y进行询问LCA,每次询问的时间复杂度为 O ( l o g n ) O(logn) O(logn)

(1) P3379 【模板】最近公共祖先(LCA)

P3379 【模板】最近公共祖先(LCA)

0x63.图论 - 树的直径与最近公共祖先_第3张图片
要找两个节点的LCA,暴力走的话就一步一步地往上爬,当然时间复杂度会贼高,不可取,你会发现一步一步往上爬就跟开篇我分享的那一篇博客里写的小兔子往前走一模一样,所以同样可以用倍增算法来优化。

就是按2的倍数来增大,也就是跳 1 , 2 , 4 , 8 , 16 , 32 … … 1,2,4,8,16,32…… 1,2,4,8,16,32 不过在这我们不是按从小到大跳,而是从大向小跳,即按 … … 32 , 16 , 8 , 4 , 2 , 1 ……32,16,8,4,2,1 32,16,8,4,2,1来跳,如果大的跳不过去,再把它调小。这是因为从小开始跳,可能会出现“悔棋”的现象。拿 5 5 5 为例,从小向大跳, 5 ≠ 1 + 2 + 4 5≠1+2+4 5=1+2+4,所以我们还要回溯一步,然后才能得出 5 = 1 + 4 5=1+4 5=1+4;而从大向小跳,直接可以得出 5 = 4 + 1 5=4+1 5=4+1。这也可以拿二进制为例, 5 ( 101 ) 5(101) 5(101),从高位向低位填很简单,如果填了这位之后比原数大了,那就不填了,这个过程是很好操作的。

所以整体思路就是用倍增算法来优化往上跳的时间,先用一个dfs预处理一下树,把所有节点的深度,父节点和它的 2 i 2^i 2i级的祖先全部用数组存起来,方便直接跳

其中几个重要的数组:

  • depth数组是记录每个节点的深度
  • f a [ i ] [ j ] fa[i][j] fa[i][j]是指节点 i i i 2 j 2^j 2j 级的祖先的编号
  • head数组是链式前向星的数组相信大家都会,这里就不展开了
  • lg数组是常数优化的数组,存的是log2N+1的值,注意用的时候要-1,开始之前先初始化一下,这样直接调用可以优化节约时间其中初始化的方法: l g [ i ] = l g [ i − 1 ] + ( 1 < < l g [ i − 1 ] = = i ) lg[i]=lg[i-1]+(1<lg[i]=lg[i1]+(1<<lg[i1]==i),自己手算一下很清楚的(lg[1~10]为1 2 2 3 3 3 3 4 4 4,应该很好懂吧)

预处理完了就要倍增求LCA了,我们先把两个点提到同一高度,再统一开始跳。

但我们在跳的时候不能直接跳到它们的LCA,因为这可能会误判,比如4和8,在跳的时候,我们可能会认为1是它们的LCA,但1只是它们的祖先,它们的LCA其实是3。所以我们要跳到它们LCA的下面一层,比如4和8,我们就跳到4和5,然后输出它们的父节点,这样就不会误判了。
然后就是代码了,里面藏着非常详细的注释,相信大家这么强一看就懂 q w q qwq qwq

#include
#include
#include
#include
#include
#include

#define ls (p<<1)
#define rs (p<<1|1)
#define mid (l+r)/2
#define over(i,s,t) for(register long long i=s;i<=t;++i)
#define lver(i,t,s) for(register long long i=t;i>=s;--i)

using namespace std;
typedef long long ll;//全用ll可能会MLE或者直接WA,试着改成int看会不会A
const ll N=500007;
const ll INF=1e9+9;
const ll mod=2147483647;
const double EPS=1e-10;//-10次方约等于趋近为0
const double Pi=3.1415926535897;
ll n,m;
struct node
{
    ll u,v,nex;
}e[N<<1];
ll head[N],cnt;

void add(ll u,ll v)
{
    e[++cnt].v=v;
    e[cnt].u=u;//没什么用,还白占空间
    e[cnt].nex=head[u];
    head[u]=cnt;
}

ll depth[N],fa[N][30],lg[N],s,x,y;

/*dfs函数的作用就是更新该点的所有祖先的fa数组,并通过递归把
该节点的所有的子节点和该节点一样去更新*/
void dfs(ll now,ll fath)//子节点和父节点
{
    fa[now][0]=fath;//更新一下fa数组,2^0=1就是父节点
    depth[now]=depth[fath]+1;//更新深度
    over(i,1,lg[depth[now]]-1)
        fa[now][i]=fa[fa[now][i-1]][i-1];
        /*更新now的所有 2^i 级的祖先。先找到now的2^(i-1)级祖先,再往上找
        该祖先的2^(i-1)级祖先,就是now的2^i祖先,必须一节一节地往上搜*/
    for(ll i=head[now];i;i=e[i].nex)//链式前向星遍历
        //如果now有子节点的话,就递归往子节点的子节点走(禁止套娃)
        if(e[i].v!=fath)//if(deep[v])continue;
            dfs(e[i].v,now);
}

inline ll LCA(ll x,ll y)
{
    if(depth[x]<depth[y])//用数学语言就是说不妨设x的深度比y的深度大
        swap(x,y);//这样下面只需要写一种代码就好了
    while(depth[x]>depth[y])
        //让x跳到y的高度(同一高度)
        x=fa[x][lg[depth[x]-depth[y]]-1];
    //如果跳到一块了那LCA肯定就是y了
    if(x==y)
        return x;
    for(ll k=lg[depth[x]]-1;k>=0;--k)//倒着从大到小地跳
        /*因为我们要求跳到x和y的LCA的下一层,所以没有跳到的时候就
        让x和y利用dfs里早就用倍增算法处理过的祖先路径快速地一块往上跳*/
        if(fa[x][k]!=fa[y][k])
            x=fa[x][k],y=fa[y][k];//往上跳
    return fa[x][0];//返回x,y的父节点(肯定是相同的嘛)
}

int main()
{
    scanf("%lld%lld%lld",&n,&m,&s);
    over(i,1,n-1)
    {
        scanf("%lld%lld",&x,&y);
        add(x,y),add(y,x);//无向图一定要记得建双向边
    }
    over(i,1,n)//预处理一下
    lg[i]=lg[i-1]+(1<<lg[i-1]==i);//log2(8)=3//这个手写的lg[]要-1才能用lg[8]=4;
    dfs(s,0);//从树根开始,因为用的是链式前向星所以给一个假想根0(其实就是到这儿停)
    //dfs一下,预处理各点的深度和祖先
    over(i,1,m)
    {
        scanf("%lld%lld",&x,&y);
        printf("%lld\n",LCA(x,y));
    }
    return 0;
}


(2)HDOJ2586 How far away(LCA)

题目大意:n结点的树,输出任意两个结点间的最小距离
思路分析:求两个点的LCA,最小距离即 d e e p [ a ] + d e e p [ b ] − 2 ∗ d e e p [ l c a ] deep[a]+deep[b]-2*deep[lca] deep[a]+deep[b]2deep[lca]

#include
#include
#include
#include
#include
#include
#include

#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;

typedef long long ll;
typedef pair<int,int> PII;
const int INF = 0x3f3f3f3f;
const int N = 5e4+7;
const int M = 2007;

int head[N],ver[N<<1],tot,edge[N<<1],nex[N<<1];
int n,m,ans,t;
int f[N][20],deep[N],dis[N];

inline void add(int u,int v,int w){
    ver[++tot] = v;
    edge[tot] = w;
    nex[tot] = head[u];
    head[u] = tot;
}

void init(){
    t = (int)(log(n) / log(2)) + 1;//以2为底的log2(n)
    over(i,1,n)head[i] = deep[i] = 0;//初始化deep深度
    tot = 0;
}

queue<int>q;
void bfs(){
    q.push(1);deep[1] = 1;
    while(q.size()){
        int u = q.front();q.pop();
        for(int i = head[u];i;i = nex[i]){
            int v = ver[i],w = edge[i];
            if(deep[v])continue;
            deep[v] = deep[u] + 1;
            dis[v] = dis[u] + w;
            f[v][0] = u;//父结点
            for(int j = 1;j <= t;++j)
                f[v][j] = f[f[v][j-1]][j-1];
            q.push(v);
        }
    }
}

int lca(int x,int y){
    if(deep[x] > deep[y])swap(x,y);
    lver(i,t,0)//先提到同一个高度
        if(deep[f[y][i]] >= deep[x])//倍增思想
            y = f[y][i];
    if(x == y)return x;
    lver(i,t,0)//再找公共祖先
        if(f[x][i] != f[y][i])
            x = f[x][i],y = f[y][i];
    return f[x][0];//return公共父结点
}
int T;
int main(){
    cin>>T;
    while(T--){
        scanf("%d%d",&n,&m);
        init();
        over(i,1,n-1){//n-1条边
            int x,y,z;
            scanf("%d%d%d",&x,&y,&z);
            add(x,y,z);
            add(y,x,z);
        }
        bfs();
        over(i,1,m){
            int x,y;
            scanf("%d%d",&x,&y);
            printf("%d\n",dis[x] + dis[y] - 2 * dis[lca(x,y)]);
        }
    }
    return 0;
}

2.LCA的Tarjan算法

LCA的Tarjan算法是离线算法,但时间复杂度也从树上倍增的 O ( ( n + m ) l o g n ) O((n+m)logn) O((n+m)logn)优化到了 O ( n + m ) O(n+m) O(n+m)虽然实测树上倍增更快一点,应该是意外
下面的代码是上面那道How far away的AC代码。

#include
#include
#include
#include
#include
#include
#include

#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;

typedef long long ll;
typedef pair<int,int> PII;
const int INF = 0x3f3f3f3f;
const int N = 5e4+7;
const int M = 2007;

int head[N],ver[N<<1],tot,edge[N<<1],nex[N<<1];
int n,m,T,t;
int fa[N],dis[N],vis[N],ans[N];
vector<int>query[N],query_id[N];


void add(int u,int v,int w){
    ver[++tot] = v;
    edge[tot] = w;
    nex[tot] = head[u];
    head[u] = tot;
}

void add_query(int x,int y,int id){
    query[x].push_back(y);query_id[x].push_back(id);
    query[y].push_back(x),query_id[y].push_back(id);
}

int Get(int x){
    if(x == fa[x])return x;
    return fa[x] = Get(fa[x]);
}

void tarjan(int u){
    vis[u] = 1;
    for(int i = head[u];i;i = nex[i]){
        int v = ver[i];
        if(vis[v])continue;
        dis[v] = dis[u] + edge[i];
        tarjan(v);
        fa[v] = u;
    }
    for(int i = 0;i < query[u].size();++i){
        int v = query[u][i],id = query_id[u][i];
        if(vis[v] == 2){
            int lca = Get(v);
            ans[id] = min(ans[id],dis[v] + dis[u] - 2 * dis[lca]);
        }
    }
    vis[u] = 2;
}

int main()
{
    cin>>T;
    while(T--){
        scanf("%d%d",&n,&m);
        over(i,1,n){
            head[i] = 0,fa[i] = i,vis[i] = 0;
            query[i].clear(),query_id[i].clear();
        }
        tot = 0;
        over(i,1,n-1){
            int x,y,z;
            scanf("%d%d%d",&x,&y,&z);
            add(x,y,z);add(y,x,z);
        }
        over(i,1,m){
            int x,y;
            scanf("%d%d",&x,&y);
            if(x == y)ans[i] = 0;
            else {
                add_query(x,y,i);
                ans[i] = 1<<30;
            }
        }
        tarjan(1);
        over(i,1,m)
        printf("%d\n",ans[i]);
    }
    return 0;
}

三、树上差分

对边差分

  • (u,v)上全部加上w,对于差分数组就是:
  • u加上w,v加上w,lca减去2 × w
  • 用子树中差分数组的和来还原信息
  • 每个点的信息记录的是其到父亲的边的信息

对点差分

  • (u,v)上全部加上w,对于差分数组就是:
  • u加上w,v加上w,lca减去w,Fatherlca减去w
  • 同样用子树中差分数组的和来还原信息

差分和数据结构结合

  • 对于一个支持单点修改、区间求和的数据结构,如果使用差分, 就可以支持区间加法、单点查询
  • 甚至可以支持区间加法、区间求和
  • 一个经典的例子就是用树状数组来完成这些事情
  • 用DFS序还可以把放到树上,区间变成子树

1.P3258 [JLOI2014]松鼠的新家(树上差分模板)

#include
#include
#include
#include
#include
#include
#include
#include

#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;

typedef long long ll;
typedef pair<int,int> PII;
const int INF = 0x3f3f3f3f;
const int N = 3e5+7;
const int M = 2007;

int head[N<<1],ver[N<<1],tot,nex[N<<1];
int n,m,T,t;
int f[N][30],vis[N];
int deep[N];
int s[N];//差分数组
int a[N];

void add(int u,int v){
    ver[++tot] = v;
    nex[tot] = head[u];
    head[u] = tot;
}

queue<int>q;

void bfs(){//lca的预处理
    q.push(1);
    deep[1] = 1;//根的深度为1
    while(q.size()){
        int u = q.front();q.pop();
        for(int i = head[u];i;i = nex[i]){
            int v = ver[i];
            if(deep[v])continue;
            f[v][0] = u;
            deep[v] = deep[u] + 1;
            for(int j = 1;j <= t;++j)
                f[v][j] = f[f[v][j-1]][j-1];
            q.push(v);
        }
    }
}

int lca(int x,int y){
    if(deep[x] > deep[y])swap(x,y);
    lver(i,t,0)
    if(deep[f[y][i]] >= deep[x])
        y = f[y][i];
    if(x == y)return x;
    lver(i,t,0)
    if(f[x][i] != f[y][i])
        x = f[x][i],y = f[y][i];
    return f[x][0];
}

void dfs(int u,int fa){
    for(int i = head[u];i;i = nex[i]){
        int v = ver[i];
        if(v == fa)continue;
        dfs(v,u);
        s[u] += s[v];
    }
}

int main()
{
    cin>>n;
    t = (int)(log(n) / log(2)) + 1;
    over(i,1,n)
    scanf("%d",&a[i]);
    over(i,1,n-1){
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y);add(y,x);
    }
    bfs();
    over(i,1,n-1){
        s[a[i]]++;
        s[a[i+1]]++;
        s[lca(a[i],a[i+1])]--;
        s[f[lca(a[i],a[i+1])][0]]--;
    }
    dfs(1,0);//从差分数组还原至原数组
    over(i,2,n)
    s[a[i]]--;//我们是直接正序循环一遍,这样从2开始每个点都是即当一次起点又当一次终点多加了一次
    over(i,1,n)
    printf("%d\n",s[i]);
    return 0;
}

2.POJ3417 闇の連鎖

0x63.图论 - 树的直径与最近公共祖先_第4张图片

3.luogu P4556雨天的尾巴 (树上对点差分 + 动态开点 + 线段树合并)线段树合并模板离线在线详解

0x63.图论 - 树的直径与最近公共祖先_第5张图片
https://fanfansann.blog.csdn.net/article/details/106729640

四、LCA的综合应用

你可能感兴趣的:(【算法竞赛学习笔记】,#,LCA及其应用)