现在的位置: 首页 > 综合 > 正文

树链剖分小结 【HDU】5029 Relief grain 树链剖分+离线标记法 【HDU】5052 Yaoge’s maximum profit 树链剖分

2017年10月16日 ⁄ 综合 ⁄ 共 12029字 ⁄ 字号 评论关闭

树链剖分,计算机术语,指一种对树进行划分的算法,它先通过轻重边剖分将树分为多条链,保证每个点属于且只属于一条链,然后再通过数据结构(树状数组、SBT、SPLAY、线段树等)来维护每一条链。


树链剖分常用的划分算法是轻重边路径剖分,也就是将树上的边分成轻边以及重边。

设u为当前子树的根,siz[u]为u的子树规模(结点总数),定义u的重儿子为u的儿子中子树规模最大的一个。那么u到u的重儿子的边就是一条重边。u的其他儿子称为轻儿子,u到轻儿子的边则为轻边。首尾相连的重边构成了重路径,重路径的尾一定出现在叶子节点。

在给边这样定义以后,我们可以发现这样一个性质:从根结点到树上任意点经过的轻边以及重链都不会超过logn条。

轻儿子的性质:siz[v] <= siz[u] / 2(当且仅当u只有两棵子树规模相同的子树时等式成立)

(让我们来反证一下:假设siz[v] > siz[u] / 2,设x为u的重儿子,则有siz[x] + siz[v] <= siz[u]。由于轻重儿子的定义,则有siz[x] >= siz[v] ,因为x和v都是u的儿子且不相同,则有siz[x] + siz[v] > siz[u] ,这和siz[x] + siz[v] <= siz[u]矛盾,假设不成立,得证)

因为有了上面性质,所以每当我们走过一条轻边以后,结点总数至少减少了一半,而一条轻边以后又是一条重链,重链以后又一定是轻边,不断走下去,可以发现轻边最多走了logn次。那么重链呢?因为轻边和重链是交错的,所以一条路径上路过的轻边数以及重链数差值的绝对值不会大于1,当n比较大的时候,路过重链的次数约等于轻边的次数,也为logn。

现在让我们先来找出树上的重边吧!

选择一个点作为根结点进行dfs。(一般选择编号为1的点,这还能方便接下来的操作,之后会了解到)

先来定义一些变量:

pre[x]:x的父亲

son[x]:x的重儿子

siz[x]:x的子树规模

dep[x]:x相对于根结点的深度

0 -> siz[0]
0 -> dep[1]
0 -> pre[1]
dfs ( u ) :
        1 -> siz[u]
        0 -> son[u]
        for every son v of u
                if v != pre[u]
                        u -> pre[v]
                        dep[v] = dep[u] + 1
                        dfs ( v ) ;
                        siz[u] += siz[v] ;
                        if siz[v] > siz[son[u]]
                                v -> son[u]
                        end if
                end if
        end for
end dfs

上面的算法dfs(v)结束后将v的子树规模加到u上,然后如果发现v的子树规模大于u的重儿子的子树规模时便进行重儿子的替换,相当于一个启发式选择的作用。

我们用一条边上深度较深的端点来表示这条边。

接下来,我们需要进行第二次dfs将得到的重边连成一条重链。

我们的方法是:

将重边重标号,使得一条重链上的重边的标号是相连的!

让我们再来定义几个变量:

top[x]:表示x所在的重链的深度最低位置(树上的最高位置)的端点(该端点表示的边不在重链上),也称为这条重链的头部。

pos[x]:x重标号后的标号

tree_idx:用以给所有边重标号。

dfs2 ( u , ancestor )
        ancestor -> top[u]
        tree_idx += 1
        pos[u] = tree_idx
        if son[u] != 0
                dfs ( son[u] , ancestor )
        end if
        for every son v of u
                if v != pre[u] and v != son[u]
                        dfs ( v , v )
                end if
        end for
end dfs2

build_tree ()
        0 -> tree_idx
        dfs2 ( 1 , 1 )
end build_tree


可以发现经过上述操作以后所有的重链中的重边的标号都是连续的。

这样我们就得到了一个很好的性质:整棵树被划分成一共有不超过logn条标号连续的线段(重链)。

到这里是不是发现可以用一些能解决线段上问题的数据结构来处理这logn条线段?

还记得我们一开始所提及的数据结构吗?树状数组、平衡树、线段树、伸展树都可用以维护这些线段!

当然我这里只举能用树状数组或者是线段树能解决的问题,涉及到平衡树以及splay的可以移歩动态树了。

通过对上述算法的思考我们可以发现,所有的重链上的重边都是连续的(喂!这不是废话么),并且所有边的标号都是不一样的(这才是重点)!

那么我们还需不需要每条重链都用一棵线段树或者树状数组维护?

当然可以不需要!

我们以重标号后的标号为线段树的下标,便可以构造出一棵长度为【1,N】的线段树。其中每条重链占一部分连续的区间。

如果是维护边,那么如果以1为根结点,则又因为所有边都以深度较深的结点代替,所以剩下在线段树中的可用的部分为【2,N】,这样选择1作为根结点绝对是明智的选择啊!

接下来是树链剖分维护线段树(树状数组)的精华所在了!

假设要更新路径x到y上的所有边的信息。

一条路径x->y便是x到lca(x,y)到y。我们更新的方式便是从两端的x,y一路操作到lca(x,y)。

首先我们知道点x以及y所在的重链的头部top[x],top[y],以及头部的深度dep[top[x]]以及dep[top[y]]。

现在我们分两种情况分别讨论。

1.top[x]!=top[y],即x和y不在一条重链上。

每次我们选择深度较深的重链进行操作。为了方便操作,我们每次只操作x,即当y的深度大于x的时候直接swap(x,y)即可。

因为知道x的同时我们也知道x所在重链的顶端top[x],如果top[x]!=top[y],则说明x与y并不在一条重链上。

此时我们直接更新区间【pos[top[x]],pos[x]】。

有没有发现我们把一条轻边也更新了?pos[top[x]]正是轻边的编号(重链的头部结点的编号表示的正是轻边)。

为什么我们连轻边也更新了?

因为如果x与y不在一条重链上,则他们到lca(x,y)的路径上一定包括这条轻边,所以一并更新便可。

之后让x=pre[top[x]],即让x跳到在他所在的这条重链之上的一条重链。

然后反复进行如上操作,直到top[x]=top[y],x和y在同一条重链上时为止,然后进入情况2。

2.top[x]=top[y],即x和y在同一条重链上。

此时如果x不等于y,那么我们让x为深度较低的结点,y为深度较深的结点。(这样的好处就是,pos[x] < pos[y],更新区间的时候方便)现在大家都在一条重链上了,皆大欢喜了,直接更新吧~

但是现在更新的区间有所变化了,为【pos[x]+1,pos[y]】,为什么要+1,还记得pos[x]代表的是在x之上的那条边吧?那条边是不属于我们这条x-y路径的哦!于是我们是不可以修改它的值的,不然评测姬们可是会来找你麻烦的2333。所以如果当x=y时直接结束函数调用吧。

再给一段更新路径上的边的代码好了:

#define root 1 , 1 , n //第一个为线段树结点编号,1为根结点的编号,第二第三个分别是区间左端点以及右端点下标
void update ( int x , int y ) {
	while ( top[x] != top[y] ) {
		if ( dep[top[x]] < dep[top[y]] ) swap ( x , y ) ;//保证top[x]为深度较深的
		sub_update ( pos[top[x]] , pos[x] , root ) ;
		x = pre[top[x]] ;
	}
	if ( x == y ) return ;//如果是更新路径上的点,这句话不要
	if ( dep[x] > dep[y] ) swap ( x , y ) ;//保证x的深度小于等于y
	sub_update ( pos[x] + 1 , pos[y] , root ) ;//如果是更新路径上的点,去掉+1
}

明白了更新,还想了解查询不?

反正我是不说了,套路和更新是一样的,不过是把更新的子操作变成了查询而已。

代码也给个:

#define root 1 , 1 , n //第一个为线段树结点编号,1为根结点的编号,第二第三个分别是区间左端点以及右端点下标
void query ( int x , int y ) {//假设我们求路径上的边权(点权)和
	int ans = 0 ;
	while ( top[x] != top[y] ) {
		if ( dep[top[x]] < dep[top[y]] ) swap ( x , y ) ;//保证top[x]为深度较深的
		ans += sub_query ( pos[top[x]] , pos[x] , root ) ;
		x = pre[top[x]] ;
	}
	if ( x == y ) return ans ;//如果是更新路径上的点,这句话不要
	if ( dep[x] > dep[y] ) swap ( x , y ) ;//保证x的深度小于等于y
	ans += sub_query ( pos[x] + 1 , pos[y] , root ) ;//如果是更新路径上的点,去掉
	return ans ;
}

现在既然基本知识都已经了解了,那么就让我们来看看下面的例题吧~

(要是这时能手握制图软件就好了= =,之前太懒了没去搞。。导致现在需要作图了做不出来)


----------------------------分割线-------------------------------


先来看几道例题了解了解下树链剖分吧。

1.边重标号的树链剖分。【SPOJ】375 Query on a tree

题目大意:

给定有N(N<=10000)个结点的树,结点编号从1开始,接下来N-1条边,每条边连接两个点,边的编号从1开始,按顺序给出。

然后有一共有一堆操作(我也不知道多少),每次操作可能为:

1. CHANGE x y 修改第x条边的边权为y。

2.QUERY u v 查询u到v的路径上最大边权。

3.DONE 结束输入。

先按照上面给的方法进行树链剖分。

然后线段树维护区间最大值,插入即单点插入,查询即路径查询。

#include <cmath>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;

typedef long long LL ;

#define repp( e , H , u ) for ( Edge* e = H[u] ; e ; e = e -> next )
#define rep( i , a , b ) for ( int i = ( a ) ; i <  ( b ) ; ++ i )
#define rev( i , a , b ) for ( int i = ( a ) ; i >= ( b ) ; -- i )
#define FOR( i , a , b ) for ( int i = ( a ) ; i <= ( b ) ; ++ i )
#define clr( a , x ) memset ( a , x , sizeof a )
#define cpy( a , x ) memcpy ( a , x , sizeof a )
#define ls ( o << 1 )
#define rs ( o << 1 | 1 )
#define lson ls , l , m
#define rson rs , m + 1 , r
#define root 1 , 2 , n
#define mid ( ( l + r ) >> 1 )

const int MAXN = 10005 ;
const int MAXE = 20005 ;

struct Edge {
	int v;
	Edge* next ;
} E[MAXE] , *H[MAXN] , *edge ;

struct Line {
	int x , y , c ;
	Line () {}
	Line ( int x , int y , int c ) : x ( x ) , y ( y ) , c ( c ) {}
} L[MAXN] ;

int maxv[MAXN << 2] ;
int siz[MAXN] ;
int pre[MAXN] ;
int dep[MAXN] ;
int top[MAXN] ;
int son[MAXN] ;
int pos[MAXN] ;
int val[MAXN] ;
int tree_idx ;
int n ;

void clear () {
	tree_idx = 0 ;
	edge = E ;
	clr ( H , 0 ) ;
	clr ( pre , 0 ) ;
	clr ( dep , 0 ) ;
	clr ( son , 0 ) ;
}

void addedge ( int u , int v ) {
	edge -> v = v ;
	edge -> next = H[u] ;
	H[u] = edge ++ ;
}

void dfs ( int u , int fa = 0 ) {
	dep[u] = dep[fa] + 1 ;
	pre[u] = fa ;
	siz[u] = 1 ;
	int maxv = 0 ;
	repp ( e , H , u ) {
		int v = e -> v ;
		if ( v == fa ) continue ;
		dfs ( v , u ) ;
		siz[u] += siz[v] ;
		if ( siz[v] > maxv ) {
			maxv = siz[v] ;
			son[u] = v ;
		}
	}
}

void build_tree ( int u , int top_element ) {
	pos[u] = ++ tree_idx ;
	top[u] = top_element ;
	if ( son[u] ) build_tree ( son[u] , top_element ) ;
	repp ( e , H , u ) {
		if ( e -> v != son[u] && e -> v != pre[u] ) {
			build_tree ( e -> v , e -> v ) ;
		}
	}
}

void build ( int o , int l , int r ) {
	if ( l == r ) {
		maxv[o] = val[l] ;
		return ;
	}
	int m = mid ;
	build ( lson ) ;
	build ( rson ) ;
	maxv[o] = max ( maxv[ls] , maxv[rs] ) ;
}

void update ( int pos , int v , int o , int l , int r ) {
	while ( l != r ) {
		int m = mid ;
		o <<= 1 ;
		if ( pos <= m ) r = m ;
		else {
			l = m + 1 ;
			o |= 1 ;
		}
	}
	maxv[o] = v ;
	while ( o != 1 ) {
		o >>= 1 ;
		maxv[o] = max ( maxv[ls] , maxv[rs] ) ;
	}
}

int query_max ( int L , int R , int o , int l , int r ) {
	if ( L <= l && r <= R ) return maxv[o] ;
	int m = mid ;
	if ( R <= m ) return query_max ( L , R , lson ) ;
	if ( m <  L ) return query_max ( L , R , rson ) ;
	return max ( query_max ( L , R , lson ) , query_max ( L , R , rson ) ) ;
}

int query ( int x , int y ) {
	int res = 0 ;
	while ( top[x] != top[y] ) {
		if ( dep[top[x]] < dep[top[y]] ) swap ( x , y ) ;
		res = max ( res , query_max ( pos[top[x]] , pos[x] , root ) ) ;
		x = pre[top[x]] ;
	}
	if ( x == y ) return res ;
	if ( dep[x] > dep[y] ) swap ( x , y ) ;
	return max ( res , query_max ( pos[x] + 1 , pos[y] , root ) ) ;
}

void solve () {
	char s[10] ;
	int x , y , c ;
	clear () ;
	scanf ( "%d" , &n ) ;
	rep ( i , 1 , n ) {
		scanf ( "%d%d%d" , &x , &y , &c ) ;
		L[i] = Line ( x , y , c ) ;
		addedge ( x , y ) ;
		addedge ( y , x ) ;
	}
	dfs ( 1 ) ;
	build_tree ( 1 , 1 ) ;
	rep ( i , 1 , n ) {
		int a = L[i].x , b = L[i].y ;
		if ( dep[a] < dep[b] ) swap ( a , b ) ;
		val[pos[a]] = L[i].c ;
	}
	build ( root ) ;
	while ( ~scanf ( "%s" , s ) && s[0] != 'D' ) {
		scanf ( "%d%d" , &x , &y ) ;
		if ( s[0] == 'Q' ) printf ( "%d\n" , query ( x , y ) ) ;
		else {
			int a = L[x].x , b = L[x].y ;
			if ( dep[a] < dep[b] ) swap ( a , b ) ;
			update ( pos[a] , y , root ) ;
		}
	}
}

int main () {
	int T ;
	scanf ( "%d" , &T ) ;
	while ( T -- ) solve () ;
	return 0 ;
}


2.点重标号的树链剖分。【HDU】3966 Aragorn's Story

题目大意:

给定有N(N<=50000)个结点的树,结点编号从1开始,一共有P(P<=100000)次操作,每次操作可能为:

1.I u v c 增加树上u到v的路径上所有点的权值。

2.D u v c 减少树上u到v的路径上所有点的权值。

3.Q u 询问树上的结点u的权值。

这题要维护的不是边,而是点了。

怎么办?

其实边和点不都一样嘛,想想我们上面树链剖分是怎么维护的?是不是将边转化成点维护?所以现在转化都不用了,直接维护吧!不过需要注意的是维护点的时候,如果当top[x]=top[y]时pos[x]是要维护的,因为现在维护的是点而不是边!具体YY一下就好。

用线段树维护树链剖分后的每条链,然后查询的时候单点查询即可。

由于点的权值是一开始就给出的,也就是说此时我们并不知道这个点在重标号后线段树的哪个结点上。于是乎,我们先放放,等到最后查询的时候查询到该结点的修改值以后,和一开始给的权值的代数和就是这个点在一系列修改后的权值。

其他都没什么了。很简单的,看代码即可。(貌似我是树状数组写的,偷偷闪人!)

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;

typedef long long LL ;

#define travel( e , H , u ) for ( Edge* e = H[u] ; e ; e = e -> next )
#define rep( i , a , b ) for ( int i = ( a ) ; i <  ( b ) ; ++ i )
#define rev( i , a , b ) for ( int i = ( a ) ; i >= ( b ) ; -- i )
#define FOR( i , a , b ) for ( int i = ( a ) ; i <= ( b ) ; ++ i )
#define clr( a , x ) memset ( a , x , sizeof a )
#define cpy( a , x ) memcpy ( a , x , sizeof a )

const int MAXN = 50005 ;
const int MAXE = 100005 ;
const int INF = 0x3f3f3f3f ;

struct Edge {
	int v ;
	Edge* next ;
} E[MAXE] , *H[MAXN] , *edge ;

int size[MAXN] ;
int pos[MAXN] ;
int val[MAXN] ;
int top[MAXN] ;
int son[MAXN] ;
int pre[MAXN] ;
int dep[MAXN] ;
int tree_idx ;
int T[MAXN] ;
int n , q ;

void clear () {
	edge = E ;
	clr ( H , 0 ) ;
	clr ( T , 0 ) ;
	tree_idx = 0 ;
	size[0] = 0 ;
	dep[1] = 0 ;
}

void addedge ( int u , int v ) {
	edge -> v = v ;
	edge -> next = H[u] ;
	H[u] = edge ++ ;
	edge -> v = u ;
	edge -> next = H[v] ;
	H[v] = edge ++ ;
}

void dfs ( int u ) {
	size[u] = 1 ;
	son[u] = 0 ;
	travel ( e , H , u ) {
		int v = e -> v ;
		if ( v != pre[u] ) {
			pre[v] = u ;
			dep[v] = dep[u] + 1 ;
			dfs ( v ) ;
			size[v] += size[u] ;
			if ( size[v] > size[son[u]] ) son[u] = v ;
		}
	}
}

void rewrite ( int u , int top_element ) {
	top[u] = top_element ;
	pos[u] = ++ tree_idx ;
	if ( son[u] ) rewrite ( son[u] , top_element ) ;
	travel ( e , H , u ) {
		int v = e -> v ;
		if ( v != pre[u] && v != son[u] ) rewrite ( v , v ) ;
	}
}

void add ( int x , int v ) {
	while ( x ) {
		T[x] += v ;
		x -= x & -x ;
	}
}

int sum ( int x , int res = 0 ) {
	while ( x <= n ) {
		res += T[x] ;
		x += x & -x ;
	}
	return res ;
}

void modify ( int x , int y , int v ) {
	while ( top[x] != top[y] ) {
		if ( dep[top[x]] < dep[top[y]] ) swap ( x , y ) ;
		add ( pos[top[x]] - 1 , -v ) , add ( pos[x] , v ) ;
		x = pre[top[x]] ;
	}
	if ( dep[x] > dep[y] ) swap ( x , y ) ;
	add ( pos[x] - 1 , -v ) , add ( pos[y] , v ) ;
}

void scanf ( int& x , char c = 0 ) {
	while ( ( c = getchar () ) < '0' || c > '9' ) ;
	x = c - '0' ;
	while ( ( c = getchar () ) >= '0' && c <= '9' ) x = x * 10 + c - '0' ;
}

void solve () {
	char op ;
	int u , v , c ;
	clear () ;
	FOR ( i , 1 , n ) scanf ( val[i] ) ;
	rep ( i , 1 , n ) {
		scanf ( u ) , scanf ( v ) ;
		addedge ( u , v ) ;
	}
	dfs ( 1 ) ;
	rewrite ( 1 , 1 ) ;
	while ( q -- ) {
		while ( ( op = getchar () ) == ' ' || op == '\n' ) ;
		if ( op == 'I' ) {
			scanf ( u ) , scanf ( v ) , scanf ( c ) ;
			modify ( u , v , c ) ;
		} else if ( op == 'D' ) {
			scanf ( u ) , scanf ( v ) , scanf ( c ) ;
			modify ( u , v , -c ) ;
		} else {
			scanf ( u ) ;
			printf ( "%d\n" , val[u] + sum ( pos[u] ) ) ;
		}
	}
}

int main () {
	while ( ~scanf ( "%d%*d%d" , &n , &q ) ) solve () ;
	return 0 ;
}


3.树链剖分的副产物:求树上两点的LCA。【CodeForces】406D Hill Climbing

题目大意:按x坐标从左到右的顺序给你n(n<=100000)座山的横坐标以及纵坐标(高度),山的编号从1开始。

如果两座山x、y的山顶可以相互看到(连成的直线和别的山没有交点),那么这两座山之间会有缆车,可以直接从x到达y或者y到达x。现在有两个人x、y想要相遇。如果x所在的山在y所在的山的右边,x会站在这座山上等y。如果y所在的山在x所在的山的左边,那么y会选择坐缆车到他能够看到的最远的山。最后不断重复上述情况直到他们在同一座山上为止,最后输出他们所在的山的编号。

首先输入n(1 ≤ n ≤ 105),接下来n行每行两个整数xi,yi(1 ≤ xi ≤ 107;1 ≤ yi ≤ 1011)。(数据保证对所有i<j,xi<xj)

接下输入m(1 ≤ m ≤ 105),表示有m组询问。接下来m行每行两个整数ai,bi
1 ≤ ai, bi ≤ n),表示ai要和bi相遇。

输出占一行,m个整数,每个数之间用空格隔开,从左到右第i个数为第i个询问的答案。


分析:首先根据题意,如果人在一座山上会到达他能看到的最远的山上,那么我们可以给这两座山建边,可以发现建完所有的边以后这一定是一棵树,根结点便是最右边的山。如果建树完成了,那么题目其实就是要我们求两点间的LCA。

先不管这是不是LCA,我们先来考虑怎么建树。

首先一座山x能看到的最远的山y一定在x到根root的凸壳上,如果y不是在凸壳上,我们总能找到一个更高的山来代替y,而那座山一定会位于凸壳上。那么我们怎么快速的为每个点x求出它的y?

单调栈维护凸壳!

首先按照横坐标从大到小的顺序遍历每座山,遇到一座山x的时候边把他和栈中栈顶元素y以及栈次顶元素z比较,看叉积是不是满足形成上凸包的取值,满足,则栈顶元素y即这座山的父亲,建边(y,x),否则,将栈顶元素弹出,再继续执行判断操作,知道找到一个符合条件的栈顶元素或者栈中元素个数不足2个时。如果栈中没有元素,则这个遍历的山一定是根,直接压入栈(它永远不会被弹出栈)。如果栈中只有一个元素,则建边(root,x)。然后再将这座山x压入栈,继续遍历下一座山,直到所有的山都遍历完毕。

那么进行了上述的操作以后,我们的树便已经建成了。

现在我们对这棵树进行树链剖分。

参照一开始给的更新操作,我们发现更新x,y两点间路径的同时便就是在找他们的lca!

于是,树链剖分求lca的思路涌现而出:更新操作直接删除,抬升深度较深的点直到top[x] = top[y],最后x,y里面深度较浅的就是我们要求的x,y的lca!

是不是很简单啊~

副产物也要充分利用的你说是不~

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std ;

typedef long long LL ;

#define rep( i , a , b ) for ( int i = a ; i < b ; ++ i )
#define For( i , a , b ) for ( int i = a ; i <= b ; ++ i )
#define rev( i , a , b ) for ( int i = a ; i >= b ; -- i )
#define travel( e , H , u ) for ( Edge* e = H[u] ; e ; e = e -> next )
#define clr( a , x ) memset ( a , x , sizeof a )
#define cpy( a , x ) memcpy ( a , x , sizeof a )
#define ls ( o << 1 )
#define rs ( o << 1 | 1 )
#define lson ls , l , m
#define rson rs , m + 1 , r
#define mid ( ( l + r ) >> 1 )
#define root 1 , 1 , n

const int MAXN = 100005 ;
const int MAXE = 200005 ;

struct Edge {
	int v ;
	Edge* next ;
} E[MAXE] , *H[MAXN] , *edge ;

struct Point {
	LL x , y ;
	int idx ;
	Point () {}
	Point ( LL x , LL y ) : x ( x ) , y ( y ) {}
	Point operator - ( const Point& p ) const {
		return Point ( x - p.x , y - p.y ) ;
	}
	void input ( int index ) {
		scanf ( "%I64d%I64d" , &x , &y ) ;
		idx = index ;
	}
} p[MAXN] , S[MAXN] ;

int top[MAXN] ;
int siz[MAXN] ;
int son[MAXN] ;
int pre[MAXN] ;
int dep[MAXN] ;
int point ;
int n , q ;

void clear () {
	edge = E ;
	siz[0] = 0 ;
	point = 0 ;
	clr ( H , 0 ) ;
	clr ( son , 0 ) ;
	clr ( pre , 0 ) ;
	clr ( top , 0 ) ;
}

void addedge ( int u , int v ) {
	edge -> v = v ;
	edge -> next = H[u] ;
	H[u] = edge ++ ;
}

void dfs ( int u ) {
	siz[u] = 1 ;
	son[u] = 0 ;
	travel ( e , H , u ) {
		int v = e -> v ;
		if ( v == pre[u] ) continue ;
		pre[v] = u ;
		dep[v] = dep[u] + 1 ;
		dfs ( v ) ;
		if ( siz[v] > siz[son[u]] ) son[u] = v ;
	}
}

void rewrite ( int u , int top_element ) {
	top[u] = top_element ;
	if ( son[u] ) rewrite ( son[u] , top_element ) ;
	travel ( e , H , u ) {
		int v = e -> v ;
		if ( v != son[u] && v != pre[u] ) rewrite ( v , v ) ;
	}
}

int query ( int x , int y ) {
	while ( top[x] != top[y] ) {
		if ( dep[top[x]] > dep[top[y]] ) x = pre[top[x]] ;
		else y = pre[top[y]] ;
	}
	if ( dep[x] < dep[y] ) return x ;
	else return y ;
}

int cross ( const Point& a , const Point& b ) {
	return a.x * b.y - a.y * b.x > 0 ;
}

void solve () {
	int x , y ;
	clear () ;
	For ( i , 1 , n ) p[i].input ( i ) ;
	rev ( i , n , 1 ) {
		while ( point > 1 && cross ( S[point - 1] - p[i] , S[point - 2] - p[i] ) ) -- point ;
		S[point ++] = p[i] ;
		if ( point > 1 ) {
			addedge ( S[point - 2].idx , S[point - 1].idx ) ;
			//printf ( "%d -> %d\n" , S[point - 2].idx , S[point - 1].idx ) ;
		}
	}
	dfs ( n ) ;
	rewrite ( n , n ) ;
	scanf ( "%d" , &q ) ;
	while ( q -- ) {
		scanf ( "%d%d" , &x , &y ) ;
		printf ( "%d\n" , query ( x , y ) ) ;
	}
}
	
int main () {
	while ( ~scanf ( "%d" , &n ) ) solve () ;
	return 0 ;
}

4.树链剖分结合离线标记法。【HDU】5029 Relief grain

直接看我blog里的文章好了。。写的有点累了。。。。。

【HDU】5029 Relief grain 树链剖分+离线标记法 


5.标记在树上的传递性质。【HDU】4987
Little Devil I

【HDU】4987 Little Devil I 树链剖分


6.查询时维护两端标记。【HDU】5052 Yaoge’s maximum profit

【HDU】5052 Yaoge’s maximum profit 树链剖分

这类题要注意的是维护的时候不能简单交换两边的标记了。

处理方法:

1.交换的时候注意产生的变化。

2.索性不交换,维持原来的性质。



-----------------------------------我是分割线-------------------------------


那么树链剖分就先小结到这里吧。树链剖分更多的应用技巧还需要多多发现啊~

一句话总结树链剖分:这货就是用来破树成链用的!然后它就是多条链了,线段上能用的技巧也就可以类比到树上了。(咦?好像不止一句话?闪人!)

抱歉!评论已关闭.