点分治总结

基本原理

点分治属于一种基于 分治思想离线 处理 树上路径 问题的代码框架。

其根本思想是将树上所有的路径分为两类:

  1. 经过当前根节点 r o o t root root
  2. 不经过当前根节点 r o o t root root

对于经过 r o o t root root 的路径,基于树结构的性质,一定可以将其看作是由从根节点出发的两条链拼接而成

那么结合其他数据结构或算法,高效的统计出任意两条链拼出的路径对答案的贡献,至此完成 1.

接下来,只需将当前 r o o t root root 打上标记,整颗树就被分成了 若干块以 r o o t root root 子节点为根的 子树

接下来在每一棵子树中找到新的根,重复 1.

直到每一个节点都被打上标记,即所有树上路径都被考虑到


复杂度分析

来考虑 r o o t root root 如何选择

选在当前子树的重心会使得复杂度最优

回顾一下重心的定义:

一棵树中 最大连通块节点数 最少的节点 称为重心

由此不难发现,若当前树的节点数为 n n n ,那么经过重心的划分,剩余子树的最大节点数必定小于 n 2 \dfrac{n}{2} 2n

那么整体来看,划分次数不会超过 l o g n log n logn

对于每次划分,实际上是对整棵树作一次 1. 操作的统计

而统计通常是能保证在 n   l o g n n\ logn n logn 以内的

那么点分治就拥有约为 n   l o g n n\ logn n logn 优于 n 2 n^2 n2 的复杂度


代码实现

#include<bits/stdc++.h>	
using namespace std ;
int n ;
const int N = 1e5 + 10 ;
struct nn
{
	int lst , to , val ;
}E[2*N] ;
int head[N] , tot ;
void add( int x , int y , int v ) 
{
	E[++tot] = (nn){ head[x] , y , v } ;
	head[x] = tot ;
}
int ans ;
bool vis[N] ; // 标记数组

int root , fm[N] , siz[N] , plk ;
void get_root( int now , int f ) 
{
	fm[now] = 0 , siz[now] = 1 ;
	for(int i = head[now] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( t == f || vis[t] ) continue ;
		get_root( t , now ) ;
		siz[now] += siz[t] ;
		fm[now] = max( fm[now] , siz[t] ) ;
	}
	fm[now] = max( fm[now] , plk - siz[now] ) ;
	if( fm[now] < fm[root] ) root = now ;
}
void get_size(int now , int f ) 
{
	siz[now] = 1 ;
	for(int i = head[now] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( t == f || vis[t] ) continue ;
		get_size( t , now ) ;
		siz[now] += siz[t] ;
	}
} // 找重心

int dis[N] ;
int bt[N] ; // 这里数据结构用的是桶,也常与其他的结合

void solve( int rt ) 
{
	vis[rt] = 1 ; // 标记当前根节点
	// 变量数组清空,赋初值
	
	calc() ; // 计算当前根节点的答案
	
	// 换一个根
	for(int i = head[rt] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if ( vis[t] ) continue ;
		root = 0 ;
		get_size( t , 0 ) ;
		plk = siz[t] ;
		get_root( t , rt ) ;
		solve( root ) ;
	}
}

int main()
{
	scanf("%d" , &n ) ;
	int x , y , v ;
	for(int i = 1 ; i < n ; i ++ ) {
		scanf("%d%d%d" , &x , &y , &v ) ;
		add( x , y , v ) ;
		add( y , x , v ) ;
	}
	fm[root] = 1e9 ;
	plk = n ;
	get_root( 1 , 0 ) ;
	solve( root ) ;
	printf("%d" , ans ) ;
  	return 0 ;
}


例题

[IOI2011]Race

题面

给一棵树,每条边有权。求一条简单路径,权值和等于 k k k ,且边的数量最小。

n ⩽ 2 × 1 0 5 n \leqslant 2\times 10^5 n2×105
k ⩽ 1 0 6 k \leqslant 10^6 k106

最优值类问题

注意到 k k k 的范围,我们可以考虑开桶 b t [ x ] bt[x] bt[x] 维护权值和为 x x x 的最小边数

#include<bits/stdc++.h>	
using namespace std ;

const int N = 2e5 + 10 ;

int n , K ;
struct nn
{
	int lst , to , val ;
}E[2*N] ;
int head[N] , tot , ans = n ;
void add( int x , int y , int v ) 
{
	E[++tot] = (nn){ head[x] , y , v } ;
	head[x] = tot ;
}
bool vis[N] ;

int fm[N] , siz[N] , root , plk ;
void get_size( int now , int f ) 
{
	siz[now] = 1 ;
	for(int i = head[now] ; i ; i = E[i].lst ) {
		if( E[i].to == f || vis[E[i].to] ) continue ;
		get_size( E[i].to , now ) ;
		siz[now] += siz[E[i].to] ;
	}
}
void get_root( int now , int f ) 
{
	siz[now] = 1 ; fm[now] = 0 ;
	for(int i = head[now] ; i ; i = E[i].lst ) {
		if( E[i].to == f || vis[E[i].to] ) continue ;
		get_root( E[i].to , now ) ;
		siz[now] += siz[E[i].to] ;
		fm[now] = max( fm[now] , siz[E[i].to] ) ;

	}
	fm[now] = max( fm[now] , plk - siz[now] ) ;
	if( fm[now] < fm[root] ) root = now ;
}

int bt[1000100] ; // 权值和 为 x 的路径 , 最小边数 
int dep[N] , dis[N] ; // 分别表示 边数 、边权 
void calc( int now , int f ) 
{
	if( dis[now] > K ) return ; //防止越界 + 优化 
	ans = min( ans , dep[now] + bt[K-dis[now]] ) ; // 对答案的贡献
	for(int i = head[now] ; i ; i = E[i].lst ) {
		if( E[i].to == f || vis[E[i].to] ) continue ;
		dep[E[i].to] = dep[now] + 1 ;
		dis[E[i].to] = dis[now] + E[i].val ;
		calc( E[i].to , now ) ;
	}
}
void update( int now , int f ) 
{
	if( dis[now] > K ) return ;
	bt[dis[now]] = min( bt[dis[now]] , dep[now] ) ; // 更新桶数组
	for(int i = head[now] ; i ; i = E[i].lst ) {
		if( E[i].to == f || vis[E[i].to] ) continue ; 
		update( E[i].to , now ) ;
	}
}
void empty( int now , int f ) 
{
	if( dis[now] > K ) return ;
	bt[dis[now]] = n ; 
	for(int i = head[now] ; i ; i = E[i].lst ) {
		if( E[i].to == f || vis[E[i].to] ) continue ;
		empty( E[i].to , now ) ;
	}
}
void solve( int rt ) 
{
	vis[rt] = 1 ;
	bt[0] = 0 ; 

	// 统计贡献 
	for(int i = head[rt] ; i ; i = E[i].lst ) {
		if( !vis[E[i].to] ) {
			// 将当前子树 与 前面子树的桶 更新ans
			dep[E[i].to] = 1 ;
			dis[E[i].to] = E[i].val ;
			calc( E[i].to , 0 ) ; 
			// 更新桶 , 必须在更新 ans 之后 
			update( E[i].to , 0 ) ;
		}
	}
	
	for(int i = head[rt] ; i ; i = E[i].lst ) {
		if( !vis[E[i].to] ) empty( E[i].to , 0 ) ;
	}  // 清空以 rt 为根节点的 bt  
	
	// 换一个根
	for(int i = head[rt] ; i ; i = E[i].lst ) {
		if( vis[E[i].to] ) continue ;
		root = 0 ;
		get_size( E[i].to , 0 ) ;
		plk = siz[E[i].to] ;
		get_root( E[i].to , 0 ) ;
		solve( root ) ;
	} 
}
int main()
{
	scanf("%d%d" , &n , &K ) ;
	for(int i = 1 ; i < n ; i ++ ) {
		int x , y , v ;
		scanf("%d%d%d" , &x , &y , &v ) ;
		x ++ , y ++ ;
		add( x , y , v ) ;
		add( y , x , v ) ;
	}
	
	for(int i = 1 ; i <= K ; i ++ ) bt[i] = n ;
	plk = n ;
	ans = n ;
	fm[0] = n ;
	root = 0 ;
	get_root( 1 , 0 ) ;
	solve( root ) ;
	if( ans < n ) cout << ans ;
	else cout <<-1 ;
  	return 0 ;
}

数据结构与点分治的结合,以后经常出现

采药人的路径

题面

路径统计类问题,还是要往点分治上考虑。

0 0 0 1 1 1 转化为 − 1 -1 1 1 1 1 ,求路径和 为 0 0 0 的路径就是阴阳平衡的路径了。

如果题目 没有限制中间休息站 那就是比较裸的点分治。

类似于race 那道题,我们可以开桶数组,本题用两个数组 f [ ] f[] f[] g [ ] g[] g[]

f [ d i s ] f[dis] f[dis] :此时DFS的这棵子树里 到根距离为 d i s dis dis 的路径条数。

g [ d i s ] g[dis] g[dis] :此时DFS的这棵子树前 到根距离为 d i s dis dis 的路径条数。

然后里外配对一下统计答案即可。

这里的 因为包含负数,我们必须将其往右偏移 n n n ,即所有的 d i s dis dis 都要 + n +n +n

加上了休息站的限制,而对于一条路径来说,拼成它的两条链的休息站情况 实际上是独立

怎么理解?

在这里插入图片描述

假设当前 x − > y x->y x>y 的路径达到阴阳平衡,当 y − > r t y->rt y>rt 可以与 p − > r t p->rt p>rt 配对时, x − > p x->p x>p 也必然平衡

那么 x x x 即可作为中间站

换言之,只需考虑一条链上是否存在阴阳平衡的子链,接下来配对

设置状态:

f [ d i s ] [ 1 ] f[dis][1] f[dis][1] :此时DFS的这棵子树里 到根距离为 d i s dis dis ,有休息站的路径条数。

f [ d i s ] [ 0 ] f[dis][0] f[dis][0] :此时DFS的这棵子树里 到根距离为 d i s dis dis ,无休息站的路径条数。

g [ d i s ] [ 1 ] g[dis][1] g[dis][1] :此时DFS的这棵子树前 到根距离为 d i s dis dis ,有休息站的路径条数。

g [ d i s ] [ 0 ] g[dis][0] g[dis][0] :此时DFS的这棵子树前 到根距离为 d i s dis dis ,无休息站的路径条数。

统计贡献时即 a n s   + =   g [ n − j ] [ 1 ] ∗ f [ n + j ] [ 1 ] + g [ n − j ] [ 0 ] ∗ f [ n + j ] [ 1 ] + g [ n − j ] [ 1 ] ∗ f [ n + j ] [ 0 ] ans\ += \ g[n-j][1]*f[n+j][1]+g[n-j][0]*f[n+j][1]+g[n-j][1]*f[n+j][0] ans += g[nj][1]f[n+j][1]+g[nj][0]f[n+j][1]+g[nj][1]f[n+j][0]

那么接下来考虑 f , g f,g f,g 数组如何求得:

将链转化为前缀和相减的形式,定义 d i s [ n o w ] dis[now] dis[now] 为节点 n o w now now 到当前 r o o t root root 的距离

d i s [ x ] = = d i s [ y ] dis[x] == dis[y] dis[x]==dis[y] 时, x − > y x->y x>y 即为阴阳平衡的路径

开桶储存即可


#include<bits/stdc++.h>	
using namespace std ;
const int N = 1e5 + 10 ;
int n ;
struct nn
{
	int lst , to , val ;
}E[2*N] ;
int head[N] , tot ;
void add( int x , int y , int v ) 
{
	E[++tot] = (nn){ head[x] , y , v } ;
	head[x] = tot ;
} 
long long ans ;
bool vis[N] ;

int fm[N] , siz[N] , root , plk ;
void get_root( int now , int f ) 
{
	fm[now] = 0 , siz[now] = 1 ;
	for(int i = head[now] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( t == f || vis[t] ) continue ;
		get_root( t , now ) ;
		siz[now] += siz[t] ;
		fm[now] = max( fm[now] , siz[t] ) ;
	}
	fm[now] = max( fm[now] , plk - siz[now] ) ;
	if( fm[now] < fm[root] ) root = now ;
}
void get_size( int now , int f ) 
{
	siz[now] = 1 ;
	for(int i = head[now] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( t == f || vis[t] ) continue ;
		get_size( t , now ) ;
		siz[now] += siz[t] ;
	}
}

int bt[2*N] , dis[2*N] ;
long long F[2*N][2] , g[2*N][2] ;
// 当前子树dis桶  当前子树有/没有休息站路径数  前面子树有/无休息站路径数 
int max_dis ;

void get_f( int now , int f ) 
{
	max_dis = max( max_dis , abs( n - dis[now] ) ) ;
	if( bt[dis[now]] ) F[dis[now]][1] ++ ; // dis出现过,可构成休息站 
	else F[dis[now]][0] ++ ;
	bt[dis[now]] ++ ;
	for(int i = head[now] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( t == f || vis[t] ) continue ;
		dis[t] = dis[now] + E[i].val ;
		get_f( t , now ) ;
	}
	bt[dis[now]] -- ;// 注意回溯!!!点分治都要考虑这里
}
void calc_update_empty()
{
	for(int i = -max_dis ; i <= max_dis ; i ++ ) {
		ans += ( F[n+i][0]*g[n-i][1] + F[n+i][1]*g[n-i][0] + F[n+i][1]*g[n-i][1] ) ;
		// 这里因为 i 从负枚举到正,只考虑子树内 +i 即可 
	} 
	ans += F[n][0]*(g[n][0] - 1 ); // 以重心为中心点, 要减去自身 
	// f 更新 g 
	for(int i = -max_dis ; i <= max_dis ; i ++ ) {
		g[n+i][0] += F[n+i][0] ;
		g[n+i][1] += F[n+i][1] ;
		F[n+i][0] = F[n+i][1] = 0 ;
	}
	// 清空 bt
	for(int i = -max_dis ; i <= max_dis ; i ++ ) {
		bt[n+i] = 0 ;
	} 
}
void solve( int rt ) 
{
	vis[rt] = 1 ;
	g[n][0] = 1 ;// rt 
	dis[rt] = n ;
	max_dis = 0 ;// 确定枚举范围,减少枚举次数 
	
	// 处理所有子树
	for(int i = head[rt] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( vis[t] ) continue ;
		dis[t] = dis[rt] + E[i].val ;
		get_f( t , rt ) ;
		calc_update_empty() ;
	}

	for(int i = -max_dis ; i <= max_dis ; i ++ ) {
		g[n+i][0] = g[n+i][1] = 0 ;
	}// 清空当前根节点的 g
	
	// 换根
	for(int i = head[rt] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( vis[t] ) continue ;
		get_size( t , rt ) ;
		plk = siz[t] ;
		root = 0 ;
		get_root( t , rt ) ;
		solve( root ) ;
	}
}
int main()
{
	scanf("%d" , &n ) ;
	int x , y , v ;
	for(int i = 1 ; i < n ; i ++ ) {
		scanf("%d%d%d" , &x , &y , &v ) ;
		if( v == 0 ) v = -1 ;
		add( x , y , v ) ;
		add( y , x , v ) ;
	}
	plk = n ;
	fm[root] = 1e9 ;
	get_root( 1 , 0 ) ;
	solve( root ) ;
	cout << ans ;
  	return 0 ;
}

从简单往复杂考虑,画图打表;

技巧 0、1、-1 的转换;

性质。


路径规划

给定一棵n个点的无根树,树上的边有一个权值 val ,要求找出一条路径使得该路径权的最小值乘边权和最大

n ⩽ 3 × 1 0 5 n \leqslant 3\times 10^5 n3×105
v a l ⩽ 1 0 6 val \leqslant 10^6 val106

同样观察到 val 的范围,考虑桶

对于当前子树,假设 枚举到的节点到 r o o t root root 的路径最小边权为 m i n d min_d mind ,考虑如何与前面的配对

显然,最小边权大于等于 m i n d min_d mind 的比较好考虑,因为可以 直接 m i n d min_d mind 乘以 边权总和

小于的呢?只需反着枚举一遍即可 ( 这种技巧同样很重要

只需动态维护所有 m i n d min_d mind 小于等于当前最小边权的 最大边权和 即可

线段树 与 后缀树状数组 应运而生

(朴素线段树版,需要卡常)

#include<bits/stdc++.h>
using namespace std ;

const int N = 3e5 + 10 , M = 1e6 + 10 ;

int n , maxM ;
struct nn
{
	int lst , to , val ;
}E[2*N] ;
int head[N] , tot ;
void add_E( int x , int y , int v ) 
{
	E[++tot] = (nn){ head[x] , y , v } ;
	head[x] = tot ;
}
bool vis[N] ;

int root , fm[N] , plk , siz[N] ;
void get_size( int now , int f ) 
{
	siz[now] = 1 ;
	for(int i = head[now] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( t == f || vis[t] ) continue ;
		get_size( t , now ) ;
		siz[now] += siz[t] ;
	}
}
void get_root( int now , int f ) 
{
	siz[now] = 1 , fm[now] = 0 ;
	for(int i = head[now] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( t == f || vis[t] ) continue ;
		get_root( t , now ) ;
		siz[now] += siz[t] ;
		fm[now] = max( fm[now] , siz[t] ) ;
	}
	fm[now] = max( fm[now] , plk - siz[now] ) ;
	if( fm[root] > fm[now] ) root = now ;
}

struct Segtree
{
	int l , r ;
	long long max_sum ;
}t[4*M] ; // 值域线段树 维护 max 
void build( int p , int l , int r ) 
{
	t[p].l = l , t[p].r = r ;
	if( l == r ) {
		return ; 
	}
	int mid = ( l + r ) >> 1 ;
	build( p<<1 , l , mid ) ;
	build( p<<1|1 , mid+1 , r ) ;
}
void add( int p , int x , long long d , bool flag )// 尝试将 x 的位置与 d 取 max  或  清空
{
	if( t[p].l == t[p].r ) {
		if( flag ) t[p].max_sum = max( t[p].max_sum , d ) ;
		else t[p].max_sum = 0 ;
		return ;
	}
	int mid = ( t[p].l + t[p].r ) >> 1 ;
	if( x <= mid ) add( p<<1 , x , d , flag ) ;
	else add( p<<1|1 , x , d , flag ) ;
	t[p].max_sum = max( t[p<<1].max_sum , t[p<<1|1].max_sum ) ;
}
long long ask( int p , int l , int r ) // 查询 l~r 中最大值 
{
	if( l <= t[p].l && t[p].r <= r ) {
		return t[p].max_sum ;
	} 
	int mid = ( t[p].l + t[p].r ) >> 1 ;
	long long res = 0 ;
	if( l <= mid ) res = max( res , ask( p<<1 , l , r ) );
	if( r > mid ) res = max( res , ask( p<<1|1 , l , r ) );
	return res ;
}

long long dis[N] , ans ;
int min_d ;
void calc( int now , int f )
{
	long long tmax = ask( 1 , min_d , maxM ) ; // 查询 最大边权和
	ans = max( ans , 1LL*min_d*( dis[now] + tmax ) ) ;
	for(int i = head[now] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( t == f || vis[t] ) continue ;
		int h = min_d ;
		min_d = min( min_d , E[i].val ) ;
		dis[t] = dis[now] + E[i].val ;
		calc( t , now ) ;
		min_d = h ; // 注意 回溯!!!!!!
	}
	
}
void update( int now , int f , bool flag ) // 更新/清空线段树 
{
	add( 1 , min_d , dis[now] , flag ) ;
	for(int i = head[now] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( t == f || vis[t] ) continue ;
		int h = min_d ;
		min_d = min( min_d , E[i].val ) ;
		update( t , now , flag ) ;
		min_d = h ; // 注意回溯!!!!!!
	}
}

int way[N] , len ;
void solve( int rt ) 
{
	vis[rt] = 1 ;
	len = 0 ;
	
	for(int i = head[rt] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( vis[t] ) continue ;
		min_d = E[i].val ;
		dis[t] = E[i].val ;
		way[++len] = i ;
		calc( t , 0 ) ; // 正序考虑 前面 比 当前距离小的 
		min_d = E[i].val ;
		update( t , 0 , 1 ) ;
	}

	for(int i = head[rt] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( vis[t] ) continue ;
		min_d = E[i].val ;
		update( t , 0 , 0 ) ;
	}

 	for(int i = len ; i >= 1 ; i -- ) {
		// 倒序考虑 后面 比 当前距离小的
		int t = E[way[i]].to ;
		if( t == rt || vis[t] ) continue ;
		min_d = E[way[i]].val ;
		calc( t , 0 ) ;
		min_d = E[way[i]].val ; 
		update( t , 0 , 1 ) ;
	}
	for(int i = head[rt] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( t == rt || vis[t] ) continue ;
		min_d = E[i].val ;
		update( t , 0 , 0 ) ;
	}

	for(int i = head[rt] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( t == rt || vis[t] ) continue ;
		get_size( t , rt ) ;
		plk = siz[t] ;
		root = 0 ;
		get_root( t , 0 ) ;
		solve( root ) ;
	}
}
int main()
{
	scanf("%d" , &n ) ;
	int x , y , v ;
	for(int i = 1 ; i < n ; ++ i ) {
		scanf("%d%d%lld" , &x , &y , &v ) ; 
		add_E( x , y , v ) ;
		add_E( y , x , v ) ;
		maxM = max( maxM , v ) ;
	}
	build( 1 , 1 , maxM ) ;
	fm[0] = 1e9 ;
	plk = n ;
	root = 0 ;
	get_root( 1 , 0 ) ;
	solve( root ) ;
	cout << ans ;
  	return 0 ;
}

(树状数组版,常数显著优于线段树)


long long t[M] ; // 值域树状数组维护后缀 max 
inline int lowbit( int x ) 
{
	return x & (-x) ;
}
// 前缀 add 要加 ,后缀减 
void add( int p , long long x , bool flag ) // p 的位置 与 x 取 max 
{
	while( p ) {
		if( flag ) t[p] = max( t[p] , x ) ;
		else t[p] = 0 ;
		p -= lowbit( p ) ;
	}
}
long long ask( int p )// 查询后缀 max  
{
	long long res = 0 ;
	while( p <= maxM ) {
		res = max( res , t[p] ) ;
		p += lowbit( p ) ; 
	}
	return res ;
}

long long dis[N] , ans ;
int min_d ;
void calc( int now , int f )
{
	long long tmax = ask( min_d ) ;
	ans = max( ans , 1LL*min_d*( dis[now] + tmax ) ) ;
	for(int i = head[now] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( t == f || vis[t] ) continue ;
		int h = min_d ;
		min_d = min( min_d , E[i].val ) ;
		dis[t] = dis[now] + E[i].val ;
		calc( t , now ) ;
		min_d = h ; // 注意 
	}
	
}
void update( int now , int f , bool flag ) // 更新/清空线段树 
{
	add( min_d , dis[now] , flag ) ;
	for(int i = head[now] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( t == f || vis[t] ) continue ;
		int h = min_d ;
		min_d = min( min_d , E[i].val ) ;
		update( t , now , flag ) ;
		min_d = h ;
	}
}

还是 点分治 与 数据结构 的结合

积累一些小技巧

Extra

如果题目中只询问某些特定路径怎么办?

传送门

首先我们当然需要把每个询问放到两端点的 “lca” 处

然而 lca 是与树根的选取有关的,我们应该对于每个分治中心实时维护 路径 ( x , y ) (x,y) (x,y) 的 lca

提供一种做法:

在每次 s o l v e solve solve 函数中传一个 v e c t o r vector vector v e ve ve 表示该连通块内的询问,总空间为 O ( n l o g n ) O(nlogn) O(nlogn)

将每个询问 ( x , y ) (x,y) (x,y) 分别放到两端点处的 v e c t o r vector vector q e [ ] qe[] qe[]

对于当前 r o o t root root 递归儿子时,遍历每个儿子的 q e qe qe,更新该询问的左右端点属于哪个儿子,这样每层复杂度是 O ( Q ) O(Q) O(Q)

然后遍历 v e ve ve ,若询问 I D ID ID 的左右端点不在一个儿子中,处理该询问

否则把询问放到 t m p [ s o n ] tmp[son] tmp[son] 中,即该儿子的 v e c t o r vector vector,换根时把 t m p [ s o n ] tmp[son] tmp[son] 传到下一层分治中

#include<bits/stdc++.h>
using namespace std;

typedef long long LL ;
const int N = 2e5+100 ;

int n , Q , a[N] ;
vector<int> E[N] , qe[N] ;
struct que
{
	int x , y , v ;
}q[N] ;
int siz[N] , plk , fm[N] , root ;
bool vis[N] ;
void get_size( int x , int fa )
{
	siz[x] = 1 ;
	for(int t : E[x] ) {
		if( t == fa || vis[t] ) continue ;
		get_size( t , x ) ;
		siz[x] += siz[t] ;
	}
}
void get_root( int x , int fa )
{
	siz[x] = 1 ; fm[x] = 0 ;
	for(int t : E[x] ) {
		if( t == fa || vis[t] ) continue ;
		get_root( t , x ) ;
		siz[x] += siz[t] ;
		fm[x] = max( fm[x] , siz[t] ) ;
	}
	fm[x] = max( fm[x] , plk-siz[x] ) ;
	if( fm[root] > fm[x] ) root = x ;
}
struct Base
{
	int b[20] ;
	void empty() {
		for(int i = 19 ; i >= 0 ; i -- ) b[i] = 0 ; 
	}
	void Insert( int x ) {
		for(int i = 19 ; i >= 0 ; i -- ) {
			if( (x>>i)&1 ) {
				if( b[i] ) x ^= b[i] ;
				else {
					b[i] = x ;
					break ;
				}
			}
		}
	}
}d[N] ;
int son[N][2] ; // i 号询问,当前属于哪个儿子 
void dfs( int x , int fa , int S )
{
	for(int ID : qe[x] ) {
		if( !son[ID][0] ) son[ID][0] = S ;
		else son[ID][1] = S ;
	}
	for(int t : E[x] ) {
		if( t == fa || vis[t] ) continue ;
		for(int j = 19 ; j >= 0 ; j -- ) d[t].b[j] = d[x].b[j] ;
		d[t].Insert( a[t] ) ;
 		dfs( t , x , S ) ; 
	}
}
bool ans[N] ;
int b[20] ;
vector<int> tmp[N] ;
void solve( int rt , vector<int> ve )
{
	vis[rt] = 1 ;
	d[rt].empty() ;
	d[rt].Insert( a[rt] ) ;
	for(int ID : qe[rt] ) {
		son[ID][0] = rt ;
	}
	for(int t : E[rt] ) {
		if( vis[t] ) continue ;
		for(int j = 19 ; j >= 0 ; j -- ) d[t].b[j] = d[rt].b[j] ;
		d[t].Insert( a[t] ) ;
		dfs( t , rt , t ) ;
	}
	for(int ID : ve ) {
		if( son[ID][0] != rt && son[ID][0] == son[ID][1] ) {
			tmp[son[ID][0]].push_back( ID ) ;
		}
		else {
			int x = q[ID].x , y = q[ID].y , v = q[ID].v ;
			for(int j = 19 ; j >= 0 ; j -- ) b[j] = d[x].b[j] ;
			for(int j = 19 ; j >= 0 ; j -- ) {
				if( d[y].b[j] == 0 ) continue ;
				int V = d[y].b[j] ;
				for(int k = j ; k >= 0 ; k -- ) {
					if( (V>>k)&1 ) {
						if( b[k] ) V ^= b[k] ;
						else {
							b[k] = V ;
							break ;
						}
					}
				}
			}
			for(int j = 19 ; j >= 0 ; j -- ) {
				if( (v>>j)&1 ) {
					if( b[j] ) v ^= b[j] ;
					else {
						ans[ID] = 1 ;
						break ;
					}
				}
			}
		}
		son[ID][0] = son[ID][1] = 0 ;
	}
	for(int t : E[rt] ) {
		if( vis[t] ) continue ;
		if( tmp[t].size() == 0 ) continue ;
		get_size( t , 0 ) ;
		plk = siz[t] ; root = 0 ;
		get_root( t , 0 ) ;
		vector<int> tt ; // 先清空,再递归进去
		swap( tt , tmp[t] ) ;
		solve( root , tt ) ;
	}
}

int main()
{
	scanf("%d" , &n ) ;
	for(int i = 1 ; i <= n ; i ++ ) scanf("%d" , &a[i] ) ;
	int x , y ;
	for(int i = 1 ; i < n ; i ++ ) {
		scanf("%d%d" , &x , &y ) ;
		E[x].push_back( y ) ;
		E[y].push_back( x ) ;
	}
	scanf("%d" , &Q ) ;
	vector<int> tt ;
	for(int i = 1 ; i <= Q ; i ++ ) {
		scanf("%d%d%d" , &q[i].x , &q[i].y , &q[i].v ) ;
		tt.push_back( i ) ;
		qe[q[i].x].push_back( i ) ;
		qe[q[i].y].push_back( i ) ;
	}
	root = 0 , fm[0] = 1e9 ; plk = n ;
	get_root( 1 , 0 ) ;
	solve( root , tt ) ;
	for(int i = 1 ; i <= Q ; i ++ ) {
		printf("%s\n" , ans[i] ? "NO" : "YES" ) ;
	}
	return 0;
 } 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值