Hdu 2586 学习 LCA 的 ST 表做法

本文介绍了使用ST表求解树中两点间距离的问题,并详细解释了如何通过区间最值查询(RMQ)来高效计算最低公共祖先(LCA)。通过两道例题Hdu2586和SCU3365,展示了算法的具体实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

还是Hdu 2586 , 求一棵树中任意两点间距离,学习 ST 表。

ST 表是求区间最值的一种预处理 O(nlogn) , 查询 O(1) 的动态规划。

例如长度为8的序列,12 5 3 20 9 7 4 1 

用 dp[i][j] 代表从第 i 个开始(包括第 i 个)往后走 2^j 步,所涵盖的区间的最值。拿最小值为例,dp[1][0] = 12 , 因为走 2^0 步还是自己(包括自己是一步) ,dp[1][1] = 5 , 求的是 区间 [ 1 , 2 ] 的最小值,dp[1][2] = 3 ,求的是 [ 1 , 4 ] 的最小值,dp[1][3] = 1 , 求的是   [ 1 , 8 ]的最小值。

这里有区间的二分,应该说是 1 推导 2 , 2 推导 4 , 4 推导 8 , 8 推导 16。

区间 [ 1 , 8 ] 的最小值就是 [ 1 , 4 ] , [ 5 , 8 ] 两个区间最小值中更小的那个      , 子区间长度 4

区间 [ 1 , 4 ] 的最小值就是 [ 1 , 2 ] , [ 3 , 4 ] 两个区间最小值中更小的那个      , 子区间长度 2

区间 [ 1 , 2 ] 的最小值就是 [ 1 , 1 ] , [ 2 , 2 ] 两个区间最小值中更小的那个      , 子区间长度 1

所以要求 [ 1 , 8 ] 区间的最小值就可以先求子区间的最值,最后推导而出。

先看核心代码:

for( int i = 1 ; i <= len ; ++i )
    	dp[i][0] = a[i] ;
    for( int j = 1 ; (1<<j) <= len ; ++j )
    	for( int i = 1 ; i+(1<<j)-1 <= len ; ++i )  
    		dp[i][j] = min( dp[i][j-1] , dp[i+(1<<(j-1))][j-1] ) ; 
                 // 区间[ i , i+2^(j-1)-1 ] 和 [ i+2^(j-1) , i+2^j ]  
初始状态就是走 2^0 步,就是自己

第一层 for 循环,决定子区间长度的,长度为 7 ,就会有 4 , 2  , 1 长度的子区间,长度为 15 是 8 , 4 , 2 , 1 长度的子区间,长度为 16 , 就会有16 , 8 , 4 , 2 , 1 长度的子区间。

第二层 for 循环,决定在区间限制之内,可以求最值的起点,例如 10 ,最大子区间长度是 8 , 可以起步找的位置只能是  1 , 3 ,然后求 [ 1 , 10 ] 的最值,就可以求 [ 1 , 8 ] 和 [ 3 , 10 ] 的最值,这样一定会覆盖,不会漏,因为 2^(j-1) + 2^(j-1) 大于等于整个区间的长度,如果小于了, 求出来的 j 值可以更大,就不是刚才的  j 了。或者说 i + 2 ^ (j-1) 一定比整个区间的一半要更大。

整体两层 for 循环就是 子区间长度从  1 , 2 , 4 , 8 逐渐递推,更长的 2^j 长度区间由上一次的 2 ^ (j-1) 长度的区间推导而来。

查询,就是从区间左边往后走 2 ^ (j-1) 的距离,从区间右边往前走  2 ^ (j-1) 的距离,覆盖整个要查询的区间。


今天学习了 LCA 和 RMQ 的关系,挺有趣的。遍历整棵树,得出经过每个点的序列,查询两个点的LCA,就在序列中找到两个点第一次出现的位置,区间之内深度最小的那个点就是 LCA 。


这个很好理解的,深搜的顺序要回溯的嘛,回溯的过程中,如果访问到其他分支,深度就会增大,在 LCA 的地方深度最小


拿这两幅图好好看,我觉得就可以了。我感觉和 Tarjan  算法有相似之处,都是根据 “当前” 的集合顶点(已经搜索完的点,所以最顶上的根是最后搜索完的),在回溯的过程中,从哪里 “可以” 开始 , 或者 “分叉”, 找到另一个点的地方,就是LCA 。

Tarjan 算法是离线的,就是只处理需要查的,批量查完,有些点之间的 LCA 可以不管 ;

根据区间最值,例如 ST 表,就是先得出所有点访问的顺序(点可重复),然后用二分合并(有点像倍增)的方法,求出所有点之间的 LCA 。LCA 就是在‘当前’访问序列上深度最低的点,这和 LCA 定义时吻合的。


Hdu  2586  求两点间距离:

#include <bits/stdc++.h>
using namespace std ;
int n , Q ;
int head[40001] , k ;
int dis[40001] ;
int first[40001] ;        // 记录这个点第一次出现的位置
int ver[40001<<1] , dep[40001<<1] , top ; // ver 记录 2*n-1 序列的点 , R 记录这些点的位置
int dp[40001<<1][18] ;    // dp 求 2*n-1 序列区间最值 , dp[i][j] 代表从 i 开始走 j 步的最值
struct Node{
	int v , w , next ;
} e[40001<<1|1] ;

void Add( int u , int v , int cost ){
	e[k].v = v , e[k].w = cost , e[k].next = head[u] , head[u] = k++ ;
}

void DFS( int u , int depth ){
	ver[++top] = u , first[u] = top , dep[top] = depth ;  
	int i , v ;
	for( i = head[u] ; i != -1 ; i = e[i].next ){
		v = e[i].v ;
		if( dis[v] ) continue ;
		dis[v] = dis[u] + e[i].w ;
		DFS( v , depth + 1 ) ;
		ver[++top] = u , dep[top] = depth ;  // 回溯记录经过的点共 2*n-1 个
	}                                            // ver 数组和 dep 数组两次记录,就是因为有回溯这一过程,经过点的序列可重复
}

void Get_dp(){
	int len = 2*n - 1 , i , j , a , b ;
	int k = (int)(log( (double)len ) / log(2.0)) ;
	for( i = 1 ; i <= len ; ++i )  // R[i] 走 0 步的最值就是自己
		dp[i][0] = i ;
	for( j = 1 ; j <= k ; ++j )
		for( i = 1 ; i+(1<<j)-1 <= len ; ++i ){
			a = dp[i][j-1] , b = dp[i+(1<<(j-1))][j-1] ; // 区间[ i , i+2^(j-1)-1 ] 和 [ i+2^(j-1) , i+2^j ]
			dp[i][j] = dep[a] < dep[b] ? a : b ;       // 找更高的点
		}
}

int RMQ( int l , int r ){   // 求 [ l , r ] 序列中的最小值 
	int k = (int)(log( (double)( r-l+1 ) ) / log(2.0)) ;
	int a = dp[l][k] , b = dp[r-(1<<k)+1][k] ;
	return dep[a] < dep[b] ? a : b ;
}

int LCA( int u , int v ){
	int l = first[u] , r = first[v] ;
	if( l > r ) swap( l , r ) ;
	return ver[RMQ( l ,r )] ;  // 序列中按照 first 位置, dep 值最小的就是 LCA
}

int main(){
	// freopen( "hdu 2586.txt" , "r" , stdin ) ;
	int cases , i , u , v , w ;
	scanf( "%d" , &cases ) ;
	while( cases-- ){
		scanf( "%d%d" , &n , &Q ) ;
		memset( head , -1 , sizeof( head ) ) ;
		memset( dis , 0 , sizeof( dis ) ) ;
		k = 0 ;
		for( i = 1 ; i <= n-1 ; ++i ){
			scanf( "%d%d%d" , &u , &v , &w ) ;
			Add( u , v , w ) ;
			Add( v , u , w ) ;
		}
		dis[1] = 1 , top = 0 ;
		DFS( 1 , 1 ) ;
		Get_dp() ;
		while( Q-- ){
			scanf( "%d%d" , &u , &v ) ;
			cout << dis[u] + dis[v] - 2*dis[LCA( u , v )] << endl ;
		}
	}
	return 0 ;
}

还有一道差不多的题目 SCU 3365 也是 LCA 的基础题,重在理解 LCA 和 RMQ 的关系,多画几个图,就会明白的。

#include <bits/stdc++.h>
using namespace std ;
int n , Q ;
int head[1005] , k ;
int dis[1005] ;
int first[1005] ;        // 记录这个点第一次出现的位置
int ver[1005<<1] , dep[1005<<1] , top ; // ver 记录 2*n-1 序列的点 , R 记录这些点的位置
int dp[1005<<1][11] ;    // dp 求 2*n-1 序列区间最值 , dp[i][j] 代表从 i 开始走 j 步的最值
struct Node{
    int v , w , next ;
} e[1005<<1|1] ;

void Add( int u , int v , int cost ){
    e[k].v = v , e[k].w = cost , e[k].next = head[u] , head[u] = k++ ;
}

void DFS( int u , int depth ){
    ver[++top] = u , first[u] = top , dep[top] = depth ;
    int i , v ;
    for( i = head[u] ; i != -1 ; i = e[i].next ){
        v = e[i].v ;
        if( dis[v] ) continue ;
        dis[v] = dis[u] + e[i].w ;
        DFS( v , depth + 1 ) ;
        ver[++top] = u , dep[top] = depth ;  // 回溯记录经过的点共 2*n-1 个
    }
}

void Get_dp( int len ){
    int i , j , a , b ;
    for( i = 1 ; i <= len ; ++i )  // R[i] 走 0 步的最值就是自己
        dp[i][0] = i ;
    for( j = 1 ; (1<<j) <= len ; ++j )
        for( i = 1 ; i+(1<<j)-1 <= len ; ++i ){
            a = dp[i][j-1] , b = dp[i+(1<<(j-1))][j-1] ; // 区间[ i , i+2^(j-1)-1 ] 和 [ i+2^(j-1) , i+2^j ]
            dp[i][j] = dep[a] < dep[b] ? a : b ;       // 找更高的点
        }
}

int RMQ( int l , int r ){   // 求 [ l , r ] 序列中的最小值 
	// int k = (int)log ((double)( r-l+1 ))/ log(2.0) ;  // 这个是错的 
	int k = (int)(log ((double)(r-l+1))/ log(2.0));      // 这个是对的
    int a = dp[l][k] , b = dp[r-(1<<k)+1][k] ;
    return dep[a] < dep[b] ? a : b ;
}

int LCA( int u , int v ){
    int l = first[u] , r = first[v] ;
    if( l > r ) swap( l , r ) ;
    return ver[RMQ( l , r )] ;  // 序列中按照 first 位置, dep 值最小的就是 LCA
}

int main(){
	freopen( "SCU 3365.txt" , "r" , stdin ) ;
    int i , u , v , w ;
    while( ~scanf( "%d%d" , &n , &Q ) ){
        memset( head , -1 , sizeof( head ) ) ;
        memset( dis , 0 , sizeof( dis ) ) ;
        k = 0 ;
        for( i = 1 ; i <= n-1 ; ++i ){
            scanf( "%d%d%d" , &u , &v , &w ) ;
            Add( u , v , w ) ;
            Add( v , u , w ) ;
        }
        dis[1] = 1 , top = 0 ;
        DFS( 1 , 1 ) ;
        Get_dp( 2*n-1 ) ;
        while( Q-- ){
            scanf( "%d%d" , &u , &v ) ;
            cout << dis[u] + dis[v] - 2*dis[LCA( u , v )] << endl ;
        }
    }
    return 0 ;
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值