还是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 ;
}