复习
求LCA
LCA(Least Common Ancestors),即最近公共祖先,是指在有根树中,找出某两个结点u和v最近的公共祖先。通常我们会用倍增的方式来进行求解。
树上差分
可以修改一个点到树根的距离,并用离线的方式求出树上任两点间的距离。
引入
树上的操作有很多,仅会差分和求LCA是远远不够的。今天我来讲讲用于处理链和子树的树上操作。
正题
DFS序
我们该如何维护子树上的操作?比如给整个子树的每条边加上一个量,再求出子树中每条边的边权和。
这些操作看起来是不是和线段树支持的操作很像呢?如果我们能将树上的每个节点对应线段树上的点,那么我们是不是就可以将树上的操作变为线段树上的操作。
现在我们要解决的问题就是如何将子树上的节点映射在一段区间里。DFS序就可以做到。
DFS序,顾名思义,就是将树的每一个节点都按DFS的顺序编号。
如图,这样一颗树,不难发现他的子树节点的DFS序都是连续的。也就是我们可以用数据结构维护这个子树上的信息。注意每个节点的DFS序和节点的编号不一定一样。
DFS序代码如下:
void dfs ( int u , int fa ) {
in[u] = ++cnt , dfn[cnt] = u ;
for ( int i = head[u] ; i ; i = edge[i].nxt ) {
int v = edge[i].to ;
if ( v != fa ) dfs ( v , u ) ;
}
out[u] = cnt ;
}
例子
BZOJ4034
[HAOI2015]树上操作
有一棵点数为 N 的树,以点 1 为根,且树点有边权。然后有 M 个
操作,分为三种:
操作 1 :把某个节点 x 的点权增加 a 。
操作 2 :把某个节点 x 为根的子树中所有点的点权都增加 a 。
操作 3 :询问某个节点 x 到根的路径中所有点的点权和。
Input
第一行包含两个整数 N, M 。表示点数和操作数。接下来一行 N 个整数,表示树中节点的初始权值。接下来 N-1
行每行三个正整数 fr, to , 表示该树中存在一条边 (fr, to) 。再接下来 M 行,每行分别表示一次操作。其中
第一个数表示该操作的种类( 1-3 ) ,之后接这个操作的参数( x 或者 x a ) 。
Output
对于每个询问操作,输出该询问的答案。答案之间用换行隔开。
Sample Input
5 5
1 2 3 4 5
1 2
1 4
2 3
2 5
3 3
1 2 1
3 5
2 1 2
3 3
Sample Output
6
9
13
HINT
对于 100% 的数据, N,M<=100000 ,且所有输入数据的绝对值都不会超过 10^6 。
DFS序虽然很好理解,但真正做例题时还是有很多细节要考虑(特别是建线段树的部分)。我30分钟码完代码,又调试了将近30分钟才过。
AC代码如下:
#include <iostream>
#include <cstdio>
#include <cstring>
typedef long long LL ;
const int MAXN = 200000 ;
const int CH_TOP = 2e7 ;
using namespace std ;
char ch[CH_TOP] , *now_w = ch - 1 , *now_r = ch - 1 ;
inline int read ( ) {
int w = 1 ;
while( *++now_r < '0' || *now_r > '9' ) if ( *now_r == '-' ) w = -1 ;
register int x = *now_r ^ 48 ;
while( *++now_r >= '0' && *now_r <= '9' ) x = ( x << 1 ) + ( x << 3 ) + ( *now_r ^ 48 ) ;
return x * w ;
}
inline void write ( LL x ) {
static char st[20] ; static int top ;
if ( x < 0 ) *++now_w = '-' , x *= -1 ;
while( st[++top] = x % 10 ^ 48 , x /= 10 ) ;
while( *++now_w = st[top] , --top );
*++now_w = '\n' ;
}
int in[MAXN + 5] , out[MAXN + 5] , seg[MAXN + 5] , val[MAXN + 5] , cnt ;
bool io[MAXN + 5] ;
struct Node {
int to , nxt ;
} edge[MAXN + 5] ;
class Graph {
public :
Graph ( ) { memset ( head , -1 , sizeof ( head ) ) ; fa[1] = -1 ; }
void add ( int u , int v ) ;
void dfs ( int u ) ;
private :
int head[MAXN + 5] , tot , fa[MAXN + 5] ;
} G ;
void Graph :: add ( int u ,int v ) {
edge[tot] = ( Node ) { v , head[u] } , head[u] = tot++ ;
}
void Graph :: dfs ( int u ) {
in[u] = ++cnt , seg[cnt] = val[u] , io[cnt] = 1 ;
for ( int i = head[u] ; ~i ; i = edge[i].nxt ) {
int v = edge[i].to ;
if ( !fa[v] && v != fa[u] )
fa[v] = u , dfs ( v ) ;
}
out[u] = ++cnt , seg[cnt] = -val[u] , io[cnt] = 0 ;
}
class SegmentTree {
struct Tree {
LL delta , data , f ;
#define lson p << 1
#define rson p << 1 | 1
};
public:
void Push_Up ( int p ) {
s[p].data = s[lson].data + s[rson].data ;
s[p].f = s[lson].f + s[rson].f ;
}
void Push_Down ( int p , int len ) {
if ( s[p].delta ) {
s[lson].delta += s[p].delta , s[rson].delta += s[p].delta ;
s[lson].data += s[p].delta * s[lson].f , s[rson].data += s[p].delta * s[rson].f ;
s[p].delta = 0 ;
}
}
void build ( int p , int l , int r ) ;
void update ( int p , int l , int r , int x , int y , int cnt ) ;
LL query ( int p , int l , int r , int x ,int y ) ;
private:
Tree s[MAXN * 4 + 5] ;
} S ;
void SegmentTree :: build ( int p , int l , int r ) {
if ( l == r ) {
s[p].data = seg[l] , s[p].delta = 0 ;
if ( io[l] ) s[p].f = 1 ; else s[p].f = -1 ;
return ;
}
int mid = l + r >> 1 ;
build ( lson , l , mid ) , build ( rson , mid + 1 , r ) ;
Push_Up ( p ) ;
}
void SegmentTree :: update ( int p , int l , int r , int x , int y , int cnt ) {
if ( y < l || x > r ) return ;
if ( x <= l && y >= r ) {
s[p].data += cnt * s[p].f , s[p].delta += cnt ;
return ;
}
Push_Down ( p , r - l + 1 ) ;
int mid = l + r >> 1 ;
update ( lson , l , mid , x , y , cnt ) , update ( rson , mid + 1 , r , x , y , cnt ) ;
Push_Up ( p ) ;
}
LL SegmentTree :: query ( int p , int l , int r , int x , int y ) {
if ( y < l || x > r ) return 0 ;
if ( x <= l && y >= r ) return s[p].data ;
Push_Down ( p , r - l + 1 ) ;
int mid = l + r >> 1 ;
return query ( lson , l , mid , x , y ) + query ( rson , mid + 1 , r , x , y ) ;
}
int main ( ) {
fread ( ch , 1 , CH_TOP , stdin ) ;
int n = read ( ) , q = read ( ) ;
for ( int i = 1 ; i <= n ; ++i )
val[i] = read ( ) ;
for ( int i = 1 ; i < n ; ++i ) {
int u = read ( ) , v = read ( ) ;
G.add ( u , v ) , G.add ( v , u ) ;
}
G.dfs ( 1 ) , S.build ( 1 , 1 , cnt ) ;
while ( q-- ) {
int opt = read ( ) ;
if ( opt == 1 ) {
int x = read ( ) , a = read ( ) ;
S.update ( 1 , 1 , cnt , in[x] , in[x] , a ) , S.update ( 1 , 1 , cnt , out[x] , out[x] , a ) ;
}
else if ( opt == 2 ) {
int x = read ( ) , a = read ( ) ;
S.update ( 1 , 1 , cnt , in[x] , out[x] , a ) ;
}
else {
int x = read ( ) ;
write ( S.query ( 1 , 1 , cnt , 1 , in[x] ) ) ;
}
}
fwrite ( ch , 1 , now_w - ch , stdout ) ;
return 0 ;
}
树链剖分
如果说DFS序是用来维护子树的,那么树链剖分就是用来维护树上一条到树根的链。
首先先我们先引入几个概念:
假设当前我们在节点u。
1、重儿子:u的子节点中子树大小最大的儿子。
2、轻儿子:u的其它子节点。
3、重边:点u与其重儿子的连边。
4、轻边:点u与其轻儿子的连边。
5、重链:由重边连成的路径。
6、轻链:由轻边连成的路径。
如图,画上红圈的就是这棵树里的重儿子。由节点1,2,4,7,9,组成的就是一条重链。由节点3,6组成的也是一条重链。
找重儿子的代码如下:
void BuildTree ( int u ) {
depth[u] = depth[fa[u]] + 1 , sz[u] = 1 ;
for ( int i = head[u] ; i ; i = edge[i].nxt ) {
int v = edge[i].to ;
if ( !fa[v] && fa[u] != v ) {
fa[v] = u , BuildTree ( v ) , sz[u] += sz[v] ;
if ( sz[son[u]] < sz[v] ) son[u] = v ;
}
}
}
建重链的代码如下
void BuildChain ( int u ) {
if ( u == top[fa[u]] ) top[u] = top[fa[u]] ; else top[u] = u ;
for ( int i = head[u] ; i ; i = edge[i].nxt ) {
int v = edge[i].to ;
if ( fa[v] == u ) BuildChain ( v ) ;
}
}
例子
求LCA
那么我们可以用树链剖分做什么题呢?既然树链剖分可以维护链上的信息,那么我们就可以用它来求两点间的LCA。
怎么求呢?我们想一下,我们一开始学的最基础的朴素解法是不是把两点一个一个往上爬,直到两点爬到一个相同的点?这个算法在哪里可以优化时间复杂度呢?
对,就是爬得太慢,我们应该用一些高效的方法在树上向上爬。以前我们还学过用倍增求LCA,就是一种快速向上跳的方法。我们在树链剖分里维护了top数组,表示该节点所在重链最上端的节点。我们用top数组作为跳板往上跳也可以快速地求出LCA。
树链剖分求LCA核心代码如下:
inline int query ( int u , int v ) {
while ( top[u] != top[v] )
if ( depth[top[u]] > depth[top[v]] )
u = fa[top[u]] ;
else
v = fa[top[v]] ;
return depth[u] < depth[v] ? u : v ;
}
树链剖分是我认为好的求LCA方式,它求LCA的时间复杂度是O(0.37nlog2n)(鬼知道怎么算的),比倍增快3倍左右。编程复杂度也不比倍增多多少,NOIP中绝对有用。
洛谷P3384树链剖分模板
之前讲的求轻重链方法被卡了,换了一种终于过了(洛谷毒瘤)。一定要记得取模。
AC代码:
#prag\
ma GCC optimize( "O3" )
#include <iostream>
#include <cstdio>
#include <cstring>
const int MAXN = 200000 ;
const int CH_TOP = 2e7;
using namespace std ;
char ch[CH_TOP] , *now_w = ch - 1 , *now_r = ch - 1 ;
inline int read ( ) {
int w = 1 ;
while( *++now_r < '0' || *now_r > '9' ) if ( *now_r == '-' ) w = -1 ;
register int x = *now_r ^ 48 ;
while( *++now_r >= '0' && *now_r <= '9' ) x = ( x << 1 ) + ( x << 3 ) + ( *now_r ^ 48 ) ;
return x * w ;
}
inline void write ( int x ) {
static char st[20] ; static int top ;
if ( x < 0 ) *++now_w = '-' , x *= -1 ;
while( st[++top] = x % 10 ^ 48 , x /= 10 ) ;
while( *++now_w = st[top] , --top );
*++now_w = '\n' ;
}
int val[MAXN + 5] , cnt , MOD , root , w[MAXN + 5] , n , q ;
class SegmentTree {
struct Tree {
int delta , data ;
#define lson p << 1
#define rson p << 1 | 1
};
public:
void Push_Up ( int p ) {
s[p].data = s[lson].data + s[rson].data , s[p].data %= MOD ;
}
void Push_Down ( int p , int len ) {
if ( s[p].delta ) {
s[lson].delta += s[p].delta , s[rson].delta += s[p].delta ;
s[lson].data += s[p].delta * ( len - len / 2 ) , s[rson].data += s[p].delta * ( len / 2 ) ;
s[lson].data %= MOD , s[rson].data %= MOD ;
s[p].delta = 0 ;
}
}
void build ( int p , int l , int r ) ;
void update ( int p , int l , int r , int x , int y , int cnt ) ;
int query ( int p , int l , int r , int x ,int y ) ;
private:
Tree s[MAXN * 4 + 5] ;
} S ;
void SegmentTree :: build ( int p , int l , int r ) {
if ( l == r ) {
s[p].data = w[l] , s[p].delta = 0 ;
return ;
}
int mid = l + r >> 1 ;
build ( lson , l , mid ) , build ( rson , mid + 1 , r ) ;
Push_Up ( p ) ;
}
void SegmentTree :: update ( int p , int l , int r , int x , int y , int cnt ) {
if ( y < l || x > r ) return ;
if ( x <= l && y >= r ) {
s[p].data += cnt * ( r - l + 1 ) , s[p].delta += cnt ;
return ;
}
Push_Down ( p , r - l + 1 ) ;
int mid = l + r >> 1 ;
update ( lson , l , mid , x , y , cnt ) , update ( rson , mid + 1 , r , x , y , cnt ) ;
Push_Up ( p ) ;
}
int SegmentTree :: query ( int p , int l , int r , int x , int y ) {
if ( y < l || x > r ) return 0 ;
if ( x <= l && y >= r ) return s[p].data ;
Push_Down ( p , r - l + 1 ) ;
int mid = l + r >> 1 ;
return ( query ( lson , l , mid , x , y ) + query ( rson , mid + 1 , r , x , y ) ) % MOD ;
}
struct Node {
int to , nxt ;
} edge[MAXN + 5] ;
class Graph {
public :
Graph ( ) { memset ( head , -1 , sizeof ( head ) ) ; fa[root] = 0 ; depth[0] = -1 ; }
void add ( int u , int v ) ;
void BuildTree ( int u ) ; void BuildChain ( int u , int Top ) ;
void UpPath( int u ,int v , int cnt ) ; int QuPath ( int u , int v ) ;
void UpSubTree ( int x , int cnt ) ; int QuSubTree ( int x ) ;
private :
int head[MAXN + 5] , tot , fa[MAXN + 5] , size[MAXN + 5] , depth[MAXN + 5] , cnt , id[MAXN + 5] ,
sz[MAXN + 5] , son[MAXN + 5] , top[MAXN + 5] ;
} G ;
void Graph :: add ( int u ,int v ) {
edge[tot] = ( Node ) { v , head[u] } , head[u] = tot++ ;
}
void Graph :: BuildTree ( int u ) {
depth[u] = depth[fa[u]] + 1 , sz[u] = 1 ;
for ( int i = head[u] ; ~i ; i = edge[i].nxt ) {
int v = edge[i].to ;
if ( !fa[v] && v != fa[u] ) {
fa[v] = u , BuildTree ( v ) , sz[u] += sz[v] ;
if ( sz[son[u]] < sz[v] ) son[u] = v ;
}
}
}
void Graph :: BuildChain ( int u , int Top ) {
id[u] = ++cnt , w[cnt] = val[u] ; top[u] = Top ;
if ( !son[u] ) return ;
BuildChain ( son[u] , Top ) ;
for ( int i = head[u] ; ~i ; i = edge[i].nxt ) {
int v = edge[i].to ;
if ( v != fa[u] && v != son[u] ) BuildChain ( v , v ) ;
}
}
void Graph :: UpSubTree ( int x , int cnt ) {
S.update ( 1 , 1 , n , id[x] , id[x] + sz[x] - 1 , cnt ) ;
}
int Graph :: QuSubTree ( int x ) {
return S.query ( 1 , 1 , n , id[x] , id[x] + sz[x] - 1 ) % MOD ;
}
void Graph :: UpPath ( int u , int v , int cnt ) {
while ( top[u] != top[v] )
if ( depth[top[u]] > depth[top[v]] )
S.update ( 1 , 1 , n , id[top[u]] , id[u] , cnt ) , u = fa[top[u]] ;
else
S.update ( 1 , 1 , n , id[top[v]] , id[v] , cnt ) , v = fa[top[v]] ;
if ( depth[u] < depth[v])
S.update ( 1 , 1 , n , id[u] , id[v] , cnt ) ;
else
S.update ( 1 , 1 , n , id[v] , id[u] , cnt ) ;
}
int Graph :: QuPath ( int u , int v ) {
int ret = 0 ;
while ( top[u] != top[v] ) {
if ( depth[top[u]] > depth[top[v]] )
ret += S.query ( 1 , 1 , n , id[top[u]] , id[u] ) , u = fa[top[u]] ;
else
ret += S.query ( 1 , 1 , n , id[top[v]] , id[v] ) , v = fa[top[v]] ;
ret %= MOD ;
}
if ( depth[u] < depth[v])
ret += S.query ( 1 , 1 , n , id[u] , id[v] ) ;
else
ret += S.query ( 1 , 1 , n , id[v] , id[u] ) ;
return ret % MOD ;
}
int main ( ) {
fread ( ch , 1 , CH_TOP , stdin ) ;
n = read ( ) , q = read ( ) , root = read ( ) , MOD = read ( ) ;
for ( int i = 1 ; i <= n ; ++i )
val[i] = read ( ) ;
for ( int i = 1 ; i < n ; ++i ) {
int u = read ( ) , v = read ( ) ;
G.add ( u , v ) , G.add ( v , u ) ;
}
G.BuildTree ( root ) , G.BuildChain ( root , root ) , S.build ( 1 , 1 , n ) ;
while ( q-- ) {
int opt = read ( ) ;
if ( opt == 1 ) {
int u = read ( ) , v = read ( ) , w = read ( ) % MOD ;
G.UpPath ( u , v , w ) ;
}
else if ( opt == 2 ) {
int u = read ( ) , v = read ( ) ;
write ( G.QuPath ( u , v ) );
}
else if ( opt == 3 ) {
int x = read ( ) , cnt = read ( ) ;
G.UpSubTree ( x , cnt ) ;
}
else {
int x = read ( ) ;
write ( G.QuSubTree ( x ) ) ;
}
}
fwrite ( ch , 1 , now_w - ch , stdout ) ;
return 0 ;
}
总结
其实DFS序和树链剖分就是一种HASH方式。优点是能快速维护许多树上操作,缺点则是代码量比较大,容易写挂。