基本原理
点分治属于一种基于 分治思想 的 离线 处理 树上路径 问题的代码框架。
其根本思想是将树上所有的路径分为两类:
- 经过当前根节点 r o o t root root 的
- 不经过当前根节点 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
n⩽2×105
k
⩽
1
0
6
k \leqslant 10^6
k⩽106
最优值类问题
注意到 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[n−j][1]∗f[n+j][1]+g[n−j][0]∗f[n+j][1]+g[n−j][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
n⩽3×105
v
a
l
⩽
1
0
6
val \leqslant 10^6
val⩽106
同样观察到 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;
}