最近公共祖先(LCA)

7 篇文章 0 订阅
1 篇文章 0 订阅

刚刚做了这个版块的题,所以趁热打铁..(有些铁已经凉了)

基本定义

LCA,就是在一棵树上找两个节点最近的公共祖先(可以理解在哪个点的时候最先相交)。

针对这类问题,我们可以用倍增的方法实现

由于以前博客写过代码,所以就不写了:自己来复习

现在开始讲例题:

习题题解:

1.Meet 紧急集合

思路

这道题其实就是三个点的LCA,但是不同的是,由于有三个点,哪一个作为集合点呢?

其实可知,集合点一定在a-b,a-c,b-c这三条路径中重合的某一个点上肯定最小。

那么怎么确定是哪一个点呢?

我们可以分类讨论,三种情况:

(一)

没有说的,选点1

(二)

这种情况是选到1吗?

其实不是,是选到a才最短

(三)

那这种情况的话,a或c都可以

经验证,我们可以得出选择最深的LCA是最好的

所以就开始码代码了

代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>
using namespace std;
const int MAXN = 5e5 + 3;
int n , m;
int f[MAXN][30];
int sz[MAXN] , fa[MAXN];
vector< int > G[MAXN];
void dfs( int x , int fa1 ){
    for( int i = 0 ; i < G[x].size() ; i ++ ){
        int v = G[x][i];
        if( fa1 == v ) continue;
        sz[v] = sz[x] + 1;
        fa[v] = x;
        f[v][0] = x;
        dfs( v , x );
    }
}
void pre( ){
    sz[1] = 1;
    dfs( 1 , 1 );
    for( int j = 1 ; j <= 20 ; j ++ )
        for( int i = 1 ; i <= n ; i ++ )
            f[i][j] = f[f[i][j-1]][j-1];
}
void jump( int &x , int mu ){
    for( int i = 20 ; i >= 0 ; i -- )
        if( sz[f[x][i]] >= mu )
            x = f[x][i];
}
int LCA( int x , int y ){
    if( sz[x] > sz[y] )
        jump( x , sz[y] );
    else if( sz[x] < sz[y] )
        jump( y , sz[x] );
    if( x != y ){
        for( int i = 20 ; i >= 0 ;i  -- )
            if( f[x][i] != f[y][i] )
                x = f[x][i] , y = f[y][i];
        x = f[x][0];
    }
    return x;
}
int main()
{
    scanf( "%d%d" , &n , &m );
    for( int i = 1 ; i < n ; i ++ ){
        int x , y;
        scanf( "%d%d" , &x , &y );
        G[x].push_back( y );
        G[y].push_back( x );
    }
    pre();
    for( int i = 1 ; i <= m ; i ++ ){
        int x , y , z;
        scanf( "%d%d%d" , &x , &y , &z );
        int ans1 = LCA( x , y );
        int ans2 = LCA( y , z );
        int ans3 = LCA( x , z );
        int p = ans1;
        if( sz[ans1] < sz[ans2] )
            p = ans2;
        if( sz[p] < sz[ans3] )
            p = ans3;
        printf( "%d " , p );
        int sum = ( sz[x] - sz[ans1] + sz[y] - sz[ans1] ) + ( sz[y] - sz[ans2] + sz[z] - sz[ans2] ) + ( sz[x] - sz[ans3] + sz[z] - sz[ans3] );
        printf( "%d" , sum / 2 );
        if( i != m )
            printf( "\n" );
    }
    return 0;
}

2.跳跳棋

思路

这也是一道神题....

我们首先考虑暴力搜索思路,那么其实可供转换的只有三种状态

1.中间的棋子往两边跳(两种)

2.边上的棋子距离中点更近的可以往中间跳

然后,这就是一颗二叉树了

1是通向儿子,2是通向父亲

那么大概思路就有了:

我们把三个棋子的位置看做一种状态

我们用类似LCA的做法来做。

首先,我们去判断是否这两种状态的根相同,就是两边的向中间的跳

这样可以直接判断输出NO

但是如果一步步跳太慢了,我们可以几步几步地跳:

 

画得真得丑,能看懂就行

那么第一步就实现了,现在我们已经求出了深度(步数),现在我们要把两个节点移到统一高度

一样的做法

接着,我们就开始用LCA的方法做就行了。

代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>
#include <algorithm>
using namespace std;
#define ll long long
ll a , b , c;
ll step1 = 0 , step2 = 0;
ll ma , mb , mc;
ll ans;
ll ma1 , mb1 , mc1 , a1 , b1 , c1;
ll s[5] , l[5];
void js( ll &x , ll &y , ll &z , ll &step ){
    ll d1 = y - x , d2 = z - y;
    while( d1 != d2 ){
        if( d1 < d2 ){
            ll s = d2 / d1 , m = d2 % d1;
            if( !m ){
                step += s - 1;
                x += d1 * (s - 1), y += d1 * (s - 1);
                return ;
            }
            step += s , x += d1 * s , y += d1 * s , d2 = m;
        }
        else{
            ll s = d1 / d2 , m = d1 % d2;
            if( !m ){
                step += s - 1;
                y -= d2 * (s - 1), z -= d2 * (s - 1);
                return ;
            }
            step += s , y -= d2 * s , z -= d2 * s , d1 = m;
        }
    }
}
void tz( ll &x , ll &y , ll &z , ll step ){
    ll d1 = y - x , d2 = z - y;
    while( step > 0 ){
        if( d1 < d2 ){
            ll s = d2 / d1 , m = d2 % d1;
            if( !m ){
                s --;
                if( step < s )
                    s = step ;
                x += d1 * s , y += d1 * s;
                step -= s;
                return ;
            }
            if( step < s )
                s = step;
            x += d1 * s , y += d1 * s , d2 = m;
            step -= s;
        }
        else{
            ll s = d1 / d2 , m = d1 % d2;
            if( !m ){
                s --;
                if( step < s )
                    s = step ;
                z -= d2 * s , y -= d2 * s;
                step -= s;
                return ;
            }
            if( step < s )
                s = step;
            z -= d2 * s , y -= d2 * s , d1 = m;
            step -= s;
        }
    }
}
int main(){
    for( int i = 1 ; i <= 3 ; i ++ )
        scanf( "%lld" , &s[i] );
    sort( s + 1 , s + 3 + 1 );
    a = s[1] , b = s[2] , c = s[3];
    for( int i = 1 ; i <= 3 ; i ++ )
        scanf( "%lld" , &l[i] );
    sort( l + 1 , l + 3 + 1 );
    ma = l[1] , mb = l[2] , mc = l[3];
    a1 = a , b1 = b , c1 = c;
    js( a1 , b1 , c1 , step1 );
    ma1 = ma , mb1 = mb , mc1 = mc;
    js( ma1 , mb1 , mc1 , step2 );
    l[1] = ma1 , l[2] = mb1 , l[3] = mc1;
    sort( l + 1 , l + 3 + 1 );
    s[1] = a1 , s[2] = b1 , s[3] = c1;
    sort( s + 1 , s + 3 + 1 );
    for( int i = 1 ; i <= 3 ; i ++ ){
        if( s[i] != l[i] ){
            printf( "NO\n" );
            return 0;
        }
    }
    printf( "YES\n" );
    if( step1 < step2 ){
        tz( ma , mb , mc , step2 - step1 );
        ans = step2 - step1;
    }
    else if( step1 > step2 ){
        tz( a , b , c , step1 - step2 );
        ans = step1 - step2;
    }
    ll l1 = 0 , r1 = min( step1 , step2 ) , cnt = 0;
    while( l1 <= r1 ){
        ll mid = ( l1 + r1 ) / 2;
        a1 = a , b1 = b , c1 = c;
        ma1 = ma , mb1 = mb , mc1 = mc;
        tz( a1 , b1 , c1 , mid );
        tz( ma1 , mb1 , mc1 , mid );
        l[1] = ma1 , l[2] = mb1 , l[3] = mc1;
        sort( l + 1 , l + 3 + 1 );
        s[1] = a1 , s[2] = b1 , s[3] = c1;
        sort( s + 1 , s + 3 + 1 );
        bool fla = 0;
        for( int i = 1 ; i <= 3 ; i ++ ){
            if( l[i] != s[i] ){
                fla = 1;
                break;
            }
        }
        if(!fla ) r1 = mid - 1 , cnt = mid * 2;
        else
            l1 = mid + 1;
    }
    printf( "%lld" , cnt + ans );
    return 0;
}

有些细节要扣,注意一下

3.求和

思想

由于k很小,可以在dfs的时候把k次方打出来就行了

接着,可以引进前缀和差分的思想(就像树状数组与前缀和)

可以将a到b之间的路径拆成两部分:a到它们LCA的部分和b到它们LCA的部分。再用差分思想将每个部分看成是点到根的路径去掉LCA到根的路径,这样只需维护每个点到根的路径上所有点的权值之和,就可以求出答案。​

注意LCA本身的权值会被减两次,要补回来

代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>
using namespace std;
#define ll long long
const int MAXN = 300003;
const int Mod = 998244353;
ll qz[MAXN][53];
int f[MAXN][23];
ll sz[MAXN][53];
int height[MAXN];
vector< int > G[MAXN];
int k , n , m;
void read( int &x ){
    x = 0;
    char s = getchar();
    while( s < '0' || s > '9' ){
        s = getchar();
    }
    while( s >= '0' && s <= '9' ){
        x = x * 10 + s - '0';
        s = getchar();
    }
}
ll qpow( ll x , ll y ){
    ll sum = 1;
    while( y ){
        if( y % 2 ) sum = sum * x % Mod;
        x = x * x % Mod;
        y /= 2;
    }
    return sum;
}
void dfs( int x , int fa1 ){
    sz[x][0] = 1;
    for( int i = 1 ; i <= 50 ; i ++ ){
        sz[x][i] = sz[x][i-1] * height[x] % Mod;
        qz[x][i] = ( ( qz[fa1][i] + sz[x][i] ) % Mod + Mod ) % Mod;
    }
    for( int i = 0 ; i < G[x].size() ; i ++ ){
        int v = G[x][i];
        if( v == fa1 ) continue;
        height[v] = height[x] + 1;
        f[v][0] = x;
        dfs( v , x );
    }
}
void jump( int &x , int mu ){
    for( int i = 20 ; i >= 0 ; i -- )
        if( height[f[x][i]] >= mu )
            x = f[x][i];
}
int LCA( int x , int y ){
    if( height[x] > height[y] )
        jump( x , height[y] );
    else if( height[x] < height[y] )
        jump( y , height[x] );
    if( x == y )
        return x;
    for( int i = 20 ; i >= 0 ; i -- ){
        if( f[x][i] != f[y][i] )
            x = f[x][i] , y = f[y][i];
    }
    return f[x][0];
}
int main()
{
    read( n );
    for( int i = 1 ; i < n ; i ++ ){
        int x , y;
        read( x );read( y );
        G[x].push_back( y );
        G[y].push_back( x );
    }
    memset( height , -1 , sizeof( height ) );
    height[1] = 0;
    dfs( 1 , 1 );
    for( int j = 1 ; j <= 20 ; j ++ )
        for( int i = 1 ; i <= n ; i ++ )
            f[i][j] = f[f[i][j-1]][j-1];
    read( m );
    for( int i = 1 ; i <= m ; i ++ ){
        int x , y;
        read( x );read( y );read( k );
        int lca = LCA( x , y );
        ll step = ( ( ( ( ( ( qz[x][k] - qz[lca][k] ) % Mod + Mod ) % Mod + ( ( qz[y][k] - qz[lca][k] ) % Mod + Mod ) % Mod) % Mod + Mod ) % Mod + sz[lca][k] ) % Mod + Mod )% Mod;
        printf( "%lld\n" , step );
    }
    return 0;
}

有些oj读优才卡过,注意取模

4.次小生成树 Tree

首先,我们可以很简单地求出最小生成树

这里我们求的是严格次小,所以一定有边与最小生成树不一样,但是大部分都是一样的

应该有n-2条边(总共n-1条)都是一样的,我们去枚举每一条没被选中的边,把这条边所连接的两个点a,b的树上路径中的严格最大权值算出来,再进行替换,这样依次枚举,就可以得到答案

现在的问题是什么是严格最大权值,怎么求呢?

就是大部分情况都是找最大值,但是如果我要替换的边是我现在找到的最大权值,那么我为了满足严格次小生成树,只能用次大边来进行替换了。

那么怎么做呢?

用倍增的思想。f[x][j]表示从第x个点开始一直向上跳2^{j}步这段中所得到的最大权值

而次大权值也是这样做的,所以可以用结构体

f[x][0] 就为x到fa[x]的权值,f[x][j] = max( f[x])[j-1] , f[f[x][j-1]][j-1])

注意替换的一些细节,一定要很仔细,庆幸自己一遍就写对了

代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>
#include <algorithm>
#include <algorithm>
#include <climits>
#define ll long long
using namespace std;
const int MAXN = 300003;
int n , m;
struct node{
    int v ;
    ll w;
    node(){}
    node( int V , ll W ){
        v = V;
        w = W;
    }
};
struct edge{
    int s , l;
    ll w;
    bool flag ;
    friend bool operator < ( edge a , edge b ){
        return a.w < b.w;
    }
}a[MAXN];
vector < node > G[MAXN];
int fa[MAXN];
int f[MAXN][23] ;
int height[MAXN];
ll maxx[MAXN][23] , max1[MAXN][23];
ll p , k1;
void read( ll &x ){
    x = 0;
    char s = getchar();
    while( s < '0' || s > '9' ){
        s = getchar();
    }
    while( s >= '0' && s <= '9' ){
        x = x * 10 + s - '0';
        s = getchar();
    }
}
int findSet( int x ){
    if( x != fa[x] ) fa[x] = findSet( fa[x] );
    return fa[x];
}
void dfs( int x , int fa1 ){
    for( int i = 0 ; i < G[x].size() ; i ++ ){
        int v = G[x][i].v , w = G[x][i].w;
        if(v == fa1 )continue;
        height[v] = height[x] + 1;
        f[v][0] = x;
        maxx[v][0] = w;
        dfs( v , x );
    }
}
void jump( int &x , ll mu , ll &w , ll &w1 ){
    for( int i = 19 ; i >= 0 ; i -- ){
        if( height[f[x][i]] >= mu ){
            if( w < maxx[x][i] ){
                w1 = w;
                w = maxx[x][i];
            }
            else if( w > maxx[x][i] )
                w1 = max( w1 , maxx[x][i] );
            w1 = max( w1 , max1[x][i] );
            x = f[x][i];
        }
    }
}
void LCA( int x , int y ){
    if( height[x] > height[y] )
        jump( x , height[y] , p , k1 );
    else if( height[x] < height[y] )
        jump( y , height[x] , p , k1 );
    if( x == y )
        return ;
    for( int i = 19 ; i >= 0 ; i -- ){
        if( f[x][i] != f[y][i] ){
            p = max( maxx[x][i] , p );
            p = max( maxx[y][i] , p );
            k1 = max( k1 , max1[x][i] );
            k1 = max( k1 , max1[y][i] );
            if( p > maxx[x][i] )
                k1 = max( maxx[x][i] , k1 );
            if( p > maxx[y][i] )
                k1 = max( k1 , maxx[y][i] );
            x = f[x][i] , y = f[y][i];
        }
    }
    int i = 0;
    p = max( maxx[x][i] , p );
        p = max( maxx[y][i] , p );
        k1 = max( k1 , max1[x][i] );
        k1 = max( k1 , max1[y][i] );
        if( p > maxx[x][i] )
            k1 = max( maxx[x][i] , k1 );
        if( p > maxx[y][i] )
            k1 = max( k1 , maxx[y][i] );
        x = f[x][i] , y = f[y][i];
    //printf( "%lld %lld\n" , p , k1 );
}
int main(){
    scanf( "%d%d" , &n , &m );
    for( int i = 1;  i <= m ; i ++ )
        scanf( "%d%d" , &a[i].s , &a[i].l ) , read( a[i].w );
    sort( a + 1 , a + m + 1 );
    for( int i = 1 ; i <= n ; i ++ )
        fa[i] = i;
    int k = 0 ;
    ll MST = 0;
    for( int i = 1 ; i <= m ; i ++ ){
        int u = findSet( a[i].s ) , v = findSet( a[i].l );
        if( u == v ) continue;
        k ++;
        fa[u] = v;
        MST += a[i].w;
        a[i].flag = 1;
        G[a[i].s].push_back(node( a[i].l , a[i].w ) );
        G[a[i].l].push_back(node( a[i].s , a[i].w ) );
        if( k == n - 1 )
            break;
    }
    //printf( "%d" , MST );
    height[1] = 1;
    dfs( 1 , 0 );
    for( int j = 1 ; j <= 19 ; j ++ )
        for( int i = 1 ; i <= n ; i ++ ){
            f[i][j] = f[f[i][j-1]][j-1];
            ll x = maxx[i][j-1] , y = maxx[f[i][j-1]][j-1];
            maxx[i][j] = max( x , y );
            max1[i][j] = max( max1[i][j-1] , max1[f[i][j-1]][j-1] );
            if( x > y )
                max1[i][j] = max( max1[i][j] , y );
            else if( x < y )
                max1[i][j] = max( max1[i][j] , x );
        }
    ll sum = LLONG_MAX;
    for( int i = 1 ; i <= m ; i ++ ){
        if( a[i].flag ) continue;
        p = 0 , k1 = 0;
        LCA( a[i].s , a[i].l );
        if( a[i].w == p )
            p = k1;
        if( MST + a[i].w - p < sum )
            sum = MST + a[i].w - p;
    }
    printf( "%lld" , sum );
    return 0;
}

注意数组的大小与long long

5.松鼠的新家

同样用前缀和做,记得把LCA的父亲减一

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>
#include <algorithm>
#include <algorithm>
#define ll long long
using namespace std;
const int MAXN = 300003;
int n ;
int f[MAXN][30];
int s[MAXN];
int sz[MAXN] , fa[MAXN];
vector< int > G[MAXN];
void dfs( int x , int fa1 ){
    for( int i = 0 ; i < G[x].size() ; i ++ ){
        int v = G[x][i];
        if( fa1 == v ) continue;
        sz[v] = sz[x] + 1;
        fa[v] = x;
        f[v][0] = x;
        dfs( v , x );
    }
}
void pre( ){
    sz[1] = 1;
    dfs( 1 , 0 );
    for( int j = 1 ; j <= 29 ; j ++ )
        for( int i = 1 ; i <= n ; i ++ )
            f[i][j] = f[f[i][j-1]][j-1];
}
void jump( int &x , int mu ){
    for( int i = 29 ; i >= 0 ; i -- )
        if( sz[f[x][i]] >= mu )
            x = f[x][i];
}
int LCA( int x , int y ){
    if( sz[x] > sz[y] )
        jump( x , sz[y] );
    else if( sz[x] < sz[y] )
        jump( y , sz[x] );
    if( x != y ){
        for( int i = 29 ; i >= 0 ;i  -- )
            if( f[x][i] != f[y][i] )
                x = f[x][i] , y = f[y][i];
        x = f[x][0];
    }
    return x;
}
int sum[MAXN];
void find_( int x , int fa1 ){
    for( int i = 0 ; i < G[x].size() ; i ++ ){
        int v = G[x][i];
        if( v == fa1 ) continue;
        find_( v , x );
        sum[x] += sum[v];
    }
}
int main(){
    scanf( "%d" , &n );
    for( int i = 1; i  <= n ; i ++ )
        scanf( "%d" , &s[i] );
    for( int i = 1 ; i < n ; i ++ ){
        int x , y;
        scanf( "%d%d" , &x , &y );
        G[x].push_back( y );
        G[y].push_back( x );
    }
    pre();
    for( int i = 1 ; i < n ; i ++ ){
        sum[s[i]] ++;
        sum[s[i+1]] ++;
        sum[ LCA( s[i] , s[i+1] ) ] -- ;
        sum[ f[LCA( s[i] , s[i+1] )][0] ] -= 1 ;
    }
    find_( 1 , 0 );
    for( int i = 2 ; i <= n ; i ++ )
        sum[s[i]] --;
    for( int i = 1 ; i < n ; i ++ )
        printf( "%d\n" , sum[i] );
    printf( "%d\n" , sum[n]) ;
   // printf( "%d" , sum[1] );
    return 0;
}

6.冷战

考虑到是否连通,且是在线的,所以果断选择并查集

但是这道题还要输出最短使a,b联通的时间

那么可以把时间看做一条权值,用一个数组维护,做并查集的时候看哪一边深度小就放在哪一边的数组里(用秩合并):

void unionSet( int x , int y , int t ){
    int u = findSet( x ) , v = findSet( y );
    if( u == v ) return ;
    if( height[u] >= height[v] ){
        fa[v] = u;
        sum[v] = t;
        if( height[u] == height[v] ) height[u] ++;
    }
    else{
        sum[u] = t;
        fa[u] = v;
    }
}

可为什么把权值这样存呢?其实如果x与y相连,且height[x]>height[y] 那么这条边就会一直存在,也就是说fa[y] = x

那么y的父亲不会再改变了,所以就可以存在y里。

那么现在怎么找最大值呢?

直接普通的LCA就可以了,且用的是现在并查集所得到的生成树做。

至于为甚么,其实也可以分类讨论

因为儿子不变,所以权值不变

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值