最近公共祖先(LCA)

    xiaoxiao2023-11-19  147

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

    基本定义

    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个点开始一直向上跳步这段中所得到的最大权值

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

    f[x][0] 就为x到fa[x]的权值,

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

    代码:

    #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就可以了,且用的是现在并查集所得到的生成树做。

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

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

    最新回复(0)