dsu on tree

参考博客

子树类查询问题

dsu on tree和长链剖分都是解决子树类静态查询问题的统计类算法。
这类问题首先要是“子树”查询,并且是“静态”不带修改的。
注意某些问题其实不太有必要上dsu on tree,比如子树元素和,子树元素最大值之类的。(可合并区间信息)
例如子树众数,子树元素种类数这些不可合并区间信息的问题适合dsu on tree

dsu on tree和长链剖分可以解决的场景与线段树合并的场景很多情况是相同的,但实际上线段树合并并不难学。
我是建议都学的,然后在真正的使用过程中,dsu on tree与长链剖的常数更小。
如果你都会的话,并且线段树合并与dsu on tree长链剖都能解决问题,我建议你选后者,避免被卡常。

我们先复习一下,在不使用dsu on tree的情况下如何解决子树类区间问题。
*借助dfs序列,将树映射到数组区间。
然后可以使用各种各样线性序列中的数据结构和算法。
比如莫队,线段树。

dsu on tree

dsu on tree这个算法的本质是“优雅的暴力”,也就是在思想上这个算法和莫队更接近一些。
但是在复杂度证明和原理上,它更偏向于轻重树链剖分。
当然它还要一些别名,比如“树上启发式合并”、“轻重树链分治”等。

前提

既然思想上这个算法和莫队相同,我们这里先复习一下莫队的思路
假定现在有一个数组a,数组里面有若干个元素,然后你已知[l1,r1]这段区间的区间和,假设区间[l1,r1]与区间[l2,r2]有大面积重合
重合率达到90%。问你如何在已知[l1,r1]区间和的情况下计算[l2,r2]的区间和?

那么只要把原本的lr端点向右移动到下一个查询的位置,然后移动的时候把需要维护的信息加入或者删除。这样就节约了计算它们公共部分的时间复杂度。
那么任意两个相邻查询之间就能节约出一个“公共子问题大小”的时间复杂度。

我们想一下子树问题中是否存在这种“公共子问题”。
我们发现对于子树类查询问题,当且仅当两个查询的子树根节点x,y存在祖先关系时,它们存在“重复子问题”,并且子问题重复率是100%。
简单来说,子树问题就两种情况,包含或分离

比如上图,4节点的子树就包含在2节点内,2节点的子树与3节点的子树之间是分离关系

具体分析

先给你这么一棵树,根节点是1号节点有n-1条有向边链接,假设每个点有点权,第i个点的权值为val[i]。
我进行若干次查询,每次查询以某点为根所表示的子树权值和。

假设我是这样暴力统计子树和的。

我要查询节点{1,4}的权值和,在不改变算法的情况下,如何降低复杂度?
那无非就是两种查询的顺序。
1、先查1再查4 2、先查4再查1
那显然是先查4再查1,复杂度更优。
因为4是1的子问题,但是反过来并不是。
先查1再查4的过程:
dfs(1); init(); dfs(4);
先查4再查1的过程:
dfs(4); dfs(1);
现在引入一个新的问题:
假设我全查了,如何安排顺序使得复杂度最小化(假设init函数不消耗时间)?
dfs(5); dfs(3); init();
dfs(7); init();
dfs(6); dfs(4); dfs(2); dfs(1);
这样的最优解让原本\(O(N^2)\)的变为\(O(NlogN)\)
那这个所谓的“最优求解顺序”是怎么来的呢。
对于一个树的子树情况,无非就两种情况,我们不妨分情况讨论。(这里其实还有叶节点的情况,但是叶节点没孩子嘛,所以不用考虑)
1、单链
2、分叉

单链

单链是最简单的,只要按照dfs(n),dfs(n-1)....dfs(1)的顺序执行即可,时间复杂度为\(O(N)\)

分叉

由单链的情况,我们已经知道了,在最优顺序里面,一定是先dfs,x,y,z这三个子节点之后再进行dfs(rt)。
换句话说rt对于x,y,z的顺序已经定了一定是rt在后,只需要对xyz进行排序即可。
我们发现,dfs(x),dfs(y),dfs(z)这三个函数没有任何交集,换句话说就是根本没有重复子问题。
那么无论先处理谁,只要后面需要调用dfs(x),dfs(y),dfs(z)中的任意一个,都必须推倒重来,执行一次init()。
因为接下来会调用dfs(rt),反倒是谁最后被执行,谁就可以被保留不计算复杂度。
那也就是说
dfs(y);init();dfs(z)init;dfs(x);dfs(rt);
dfs(z);init();dfs(y)init;dfs(x);dfs(rt);
这两个求解顺序都是复杂度最低的求解顺序。

总结一下

1、优先处理子树查询,再处理根节点
2、当出现多个子树的时候,优先处理子树尺寸较小的子树,然后使用init清空,最后处理尺寸最大的子树,然后保留信息的情况下处理根节点。
换句话总结一下
1、优先处理子树查询,再处理根节点。
2、当出现多个子树的时候,优先处理轻儿子,处理完每个轻儿子之后使用init清空,最后处理重儿子并保留信息的情况下处理根节点。

现在再反过来看一开始提出的例题

最优求解顺序:
dfs(5); dfs(3); init();
dfs(7); init();
dfs(6); dfs(4); dfs(2); dfs(1); //init()
124~6是一条重链,链首是1
3~5是一条重链,链首是3
7是一条重链,链首是7
“最优求解序列”就是轻重树链剖分的dfs序列的倒置,并且在每条重链的链首处进行了一次init()

复杂度分析

那么“最优求解序列”究竟效率如何呢?
为了解决这个问题,还是要借助树链剖分。
定理:树上任意节点到根节点的简单路径中,经过轻边的数目不多于\(𝑙𝑜𝑔_2 𝑁\)
我们知道,对于任意一个节点x,我们需要统计它的信息仅当调用dfs(y),y是x的祖先时。
那么只要求出所有的dfs(y)中遍历x的次数,把它们加起来也就是这个算法的最终复杂度。
因为y是x的祖先,所以dfs(y)时只要之前调用过init函数,x就必须要重新统计。
换句话说只要统计dfs(y)时init的调用次数即可。
因为只有在链首处才会调用init,所以这个问题的答案就等于从x到根节点的简单路径中,经过轻边的数目。
那对于任意节点x,它对总时间复杂度的贡献不多于\(𝑙𝑜𝑔_2 𝑁\) ,那么这个算法的最终复杂度就不多于\(O(N𝑙𝑜𝑔_2𝑁)\)

具体实现

首先是“如何求出最优求解序列”,前面也说了,这个东西就是“树链剖分dfs序的倒序”,翻转一个序列最简单的方法就是压到栈里面再输出,我们知道递归算法是一个天然的堆栈。所以我们只要利用dfs按照树链剖分的规则再来一遍,然后在return之前求解即可。

那因为我们需要用到树链剖分的dfs序嘛,那肯定是要真的树剖一下,我们做一下树剖然后记录一些信息。
第一步也就是预处理的过程,它就是一个树剖。

代码讲解见博客

总结

dsu(x)
{
1、处理轻儿子
2、处理重儿子
3、把轻儿子的信息往重儿子上边并
4、计算答案
5、需要清空时清空统计用的数据结构
}

例题

1.DongDong数颜色

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#define ll long long 
#define pa pair<int,int>
#define fi first
#define se second
#define mp make_pair
#define pb push_back
using namespace std;
const int maxn=400000+101;
const int MOD=998244353;
const int inf=2147483647;
const double pi=acos(-1);
int read(){
    int x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}

int n,m,val[maxn];
int tot,head[maxn],to[maxn],nx[maxn];
void add(int x,int y){to[++tot]=y;nx[tot]=head[x];head[x]=tot;}
int fa[maxn],L[maxn],R[maxn],Index[maxn],cnt;
int top[maxn],son[maxn],sz[maxn];
void dfs(int x){
    L[x]=++cnt;Index[cnt]=x;sz[x]=1;
    for(int i=head[x];i;i=nx[i]){
        int v=to[i];if(v==fa[x])continue;
        fa[v]=x;dfs(v);sz[x]+=sz[v];
        if(sz[son[x]]<sz[v])son[x]=v;
    }
    R[x]=cnt;
    return ;
}

int ans[maxn],book[maxn],sum,skp;
void get_data(int x,int v){
    for(int i=L[x];i<=R[x];i++){
        if(Index[i]==skp){
            i=R[Index[i]];
            continue;
        }
        book[val[Index[i]]]+=v;
        if(book[val[Index[i]]]==1 && v==1)sum++;
        if(book[val[Index[i]]]==0)sum--;
    }
    return ;
}
void dsu(int x,bool del){
    for(int i=head[x];i;i=nx[i]){
        int v=to[i];if(v==fa[x] || v==son[x])continue;
        dsu(v,true);
    }
    if(son[x]){
        dsu(son[x],false);
        skp=son[x];
    }
    get_data(x,1);
    ans[x]=sum;
    if(del){
        skp=0;
        get_data(x,-1);
    }
    return ;
}

int main(){
    n=read();m=read();
    for(int i=1;i<=n;i++)val[i]=read();
    for(int i=1;i<n;i++){
        int u=read(),v=read();
        add(u,v);add(v,u);
    }
    dfs(1);dsu(1,false);
    while(m--)printf("%d\n",ans[read()]);
    return 0;
}

注意del这个东西,虽然我们要的效果是将cnt数组和nowans清零,但是实际上我们不能使用memset,否则复杂度会退化为\(O(N^2)\)
这里额外提一句,如果你之前做了完整的树剖,那么del这个参数可以不传,只需要把if(del)改写为if(x==top[x])这个条件即可。(重链的链首处进行init)

2.正经人谁吃泡菜肥牛啊?
将对x节点的所询问保存到qq[x]中,在dsu过程中,用树状数组维护不同泡菜肥牛值的个数,当dsu到节点x时,对q[x]中每个询问对应答案的值就是query(k)。

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#define ll long long 
#define pa pair<int,int>
#define fi first
#define se second
#define mp make_pair
#define pb push_back
using namespace std;
const int maxn=3000000+101;
const int MOD=998244353;
const int inf=2147483647;
const double pi=acos(-1);
int read(){
    int x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}

int n,m,val[maxn];
int tot,head[maxn],to[maxn],nx[maxn];
void add(int x,int y){to[++tot]=y;nx[tot]=head[x];head[x]=tot;}
int fa[maxn],L[maxn],R[maxn],Index[maxn],cnt;
int top[maxn],son[maxn],sz[maxn];
void dfs(int x){
    L[x]=++cnt;Index[cnt]=x;sz[x]=1;
    for(int i=head[x];i;i=nx[i]){
        int v=to[i];if(v==fa[x])continue;
        fa[v]=x;dfs(v);sz[x]+=sz[v];
        if(sz[son[x]]<sz[v])son[x]=v;
    }
    R[x]=cnt;
    return ;
}
int f[maxn];
int lowbit(int x){return x&(-x);}
void ad(int x,int v){
    for(int i=x;i<=1E6;i+=lowbit(i))f[i]+=v;
    return ;
}
int query(int x){
    int ans=0;
    for(int i=x;i;i-=lowbit(i))ans+=f[i];
    return ans;
}
vector<int>qq[maxn];
struct que{
    int x,k;
}q[maxn];
int ans[maxn],book[maxn],sum,skp;
void get_data(int x,int v){
    for(int i=L[x];i<=R[x];i++){
        if(Index[i]==skp){
            i=R[Index[i]];
            continue;
        }
        ad(val[Index[i]],v);
    }
    return ;
}
void dsu(int x,bool del){
    for(int i=head[x];i;i=nx[i]){
        int v=to[i];if(v==fa[x] || v==son[x])continue;
        dsu(v,true);
    }
    if(son[x]){
        dsu(son[x],false);
        skp=son[x];
    }
    get_data(x,1);
    for(auto i:qq[x])ans[i]=query(q[i].k);
    if(del){
        skp=0;
        get_data(x,-1);
    }
    return ;
}

int main(){
    n=read();
    for(int i=1;i<=n;i++)val[i]=read();
    for(int i=1;i<n;i++){
        int u=read(),v=read();
        add(u,v);add(v,u);
    }
    m=read();
    for(int i=1;i<=m;i++){
        q[i].x=read();q[i].k=read();
        qq[q[i].x].pb(i);
    }
    dfs(1);dsu(1,false);
    for(int i=1;i<=m;i++)printf("%d\n",ans[i]);
    return 0;
}

3.小睿睿的伤害

我也可以对每个点暴力枚举lca为这个点的点对,我们可以开一个桶m,来表示因数的个数,即m[i]表示因数i的个数。
对于每新加入一个数x,如何得到x和前面的数的最大gcd
只需要看x的最大能在m桶里找到的约数即可

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#define ll long long 
#define pa pair<int,int>
#define fi first
#define se second
#define mp make_pair
#define pb push_back
using namespace std;
const int maxn=2e5+101;
const int MOD=998244353;
const int inf=2147483647;
const double pi=acos(-1);
ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
vector<vector<int> >fac(maxn+1);
void init(){
    for(int i=1;i<=maxn;i++){
        for(int j=1;j<=i/j;j++){
            if(i%j)continue;
            fac[i].pb(j);
            if(i/j!=j)fac[i].pb(i/j);
        }
    }
    return ;
}

int n,a[maxn];
int tot,head[maxn],to[maxn],nx[maxn];
void add(int x,int y){to[++tot]=y;nx[tot]=head[x];head[x]=tot;}
struct wzq{
    int val,num;
}ans[maxn];
struct DSU{
    int fa[maxn],Index[maxn],L[maxn],R[maxn],sz[maxn],son[maxn],cnt;
    void dfs(int x){
        L[x]=++cnt;Index[cnt]=x;sz[x]=1;
        for(int i=head[x];i;i=nx[i]){
            int v=to[i];if(v==fa[x])continue;
            fa[v]=x;dfs(v);sz[x]+=sz[v];
            if(sz[son[x]]<sz[v])son[x]=v;
        }
        R[x]=cnt;return ;
    }

    int m[maxn];    
    void ad(int x,int val){for(auto i:fac[a[x]])m[i]+=val;}
    void get_ans(int x,int u,int val){
        if(val==-1)return ;
        for(auto i:fac[a[x]]){
            if(m[i] && i>ans[u].val){
                ans[u].val=i;ans[u].num=m[i];
                //当前约数i桶m中有,且比之前的答案大,更新答案
            }
            else if(i==ans[u].val)ans[u].num+=m[i];
        }
        return ;
    }
    void get_data(int x,int val){
        if(val==-1){
            for(int i=L[x];i<=R[x];i++)ad(Index[i],-1);
            return ;
        }
        get_ans(x,x,1);ad(x,1);//x跟子树内任意一个点的lca都是x,单独考虑
        for(int i=head[x];i;i=nx[i]){
            int v=to[i];if(v==fa[x] || v==son[x])continue;  //重儿子已经统计在m里了,并且统计过答案了
            for(int j=L[v];j<=R[v];j++)get_ans(Index[j],x,1);//对于每个分支单独考虑
            for(int j=L[v];j<=R[v];j++)ad(Index[j],1);  //每个分支计算完答案后再加入,避免分支内部计算
        }
        return ;
    }

    void dsu(int x,bool del){
        for(int i=head[x];i;i=nx[i]){
            int v=to[i];if(v==fa[x] || v==son[x])continue;
            dsu(v,true);
        }
        if(son[x])dsu(son[x],false);
        get_data(x,1);
        if(del)get_data(x,-1);
        return ;
    }
}D;

int main(){
    init();n=read();
    for(int i=1;i<n;i++){
        int x=read(),y=read();
        add(x,y);add(y,x);
    }
    for(int i=1;i<=n;i++)a[i]=read();
    D.dfs(1);D.dsu(1,false);
    for(int i=1;i<=n;i++)printf("%d %d\n",ans[i].val,ans[i].num);
    return 0;
}

4.E. XOR Tree

子树内信息查询适合DSU
大体思路是:
dfs遍历,对于当前遍历节点u,统计轻链信息往重链上合并
同时用set判断是否存在异或为0的路径,如果有,那么u这个节点的子树信息清除,继续向上回溯

为什么“存在异或为0的路径,如果有,那么u这个节点的子树信息清除”

因为通过改变u值可以使子树内不存在0异或路径,同时也可以使u到别的子树的路径异或不为0

如何合并信息

1.重儿子合并到父亲
首先我们知道dsu的时候重儿子的信息才会传递给父亲节点,轻儿子的信息就删除了
那么我们考虑重儿子信息传递给父亲
我们用sets;储存重儿子为起点的路径异或值
那么更新到父亲时,s中每个值都要重新异或一遍父亲节点的值,但这样肯定会超时
我们不妨打个lazytag,让lazytag异或上父亲节点的值
对于重儿子中每个路径dis异或上tag才是真正的父亲节点到重儿子子树中节点的路径异或和

2.轻儿子合并到父亲
例如下图,红色表示重链

假设处理到2节点,重儿子3的信息处理完了,当前tag=a[4]^a[3]
对于6节点合并到2时,要让a[6]^tag加入到set中
当新tag=tag^a[2]时
(新tag)\(^\) a[6]\(^\)tag=a[2]\(^\)a[6]也就是6到2的路径异或和

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=1e6+101;
const int MOD=998244353;
const int inf=2147483647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}

int n,m,a[maxn];
int tot,head[maxn],to[maxn],nx[maxn];
void add(int x,int y){to[++tot]=y;nx[tot]=head[x];head[x]=tot;}

struct DSU{
    int fa[maxn],L[maxn],R[maxn],Index[maxn],cnt;
    int top[maxn],son[maxn],sz[maxn];
    void dfs(int x){
        L[x]=++cnt;Index[cnt]=x;sz[x]=1;
        for(int i=head[x];i;i=nx[i]){
            int v=to[i];if(v==fa[x])continue;
            fa[v]=x;dfs(v);sz[x]+=sz[v];
            if(sz[son[x]]<sz[v])son[x]=v;
        }
        R[x]=cnt;
        return ;
    }
    set<int>s;
    vector<int>V;
    int success,ans[maxn],tag,cur;
    void get_xor(int x,int fa,int sum){
        if(ans[x] || success)return ;
        sum^=a[x];
        V.pb(sum);
        if(s.count(sum^tag^cur)){
            success=1;
            return ;
        }
        for(int i=head[x];i;i=nx[i]){
            int v=to[i];if(v==fa)continue;
            get_xor(v,x,sum);
        }
        return ;
    }

    void get_data(int x,int v){
        success=0;cur=a[x];
        for(int i=head[x];i;i=nx[i]){
            int v=to[i];if(v==fa[x] || v==son[x])continue;
            V.clear();
            get_xor(v,x,0);
            for(auto j:V)s.insert(j^tag);
            if(success)return ;
        }
        if(s.count(a[x]^tag))success=1;    //x到子树节点是否存在0路径
        return ;
    }

    void dsu(int x,bool del){
        for(int i=head[x];i;i=nx[i]){
            int v=to[i];if(v==fa[x] || v==son[x])continue;
            dsu(v,true);
        }
        if(son[x])dsu(son[x],false);
        s.insert(tag);   //插入当前节点x的值到集合中
        get_data(x,1);
        if(success){    //该节点要修改,删去当前子树的信息
            ans[x]=1;
            tag=0;
            s.clear();
        }
        else if(del){
            tag=0;
            s.clear();
            //消除影响直接clear,是线性的
        }
        else tag^=a[x];//当前节点是重儿子,打tag
        return ;
    }
}D;

int main(){
    n=read();
    for(int i=1;i<=n;i++)a[i]=read();
    for(int i=1;i<n;i++){
        int x=read(),y=read();
        add(x,y);add(y,x);
    }
    D.dfs(1);D.dsu(1,false);
    int ans=0;
    for(int i=1;i<=n;i++)ans+=D.ans[i];
    printf("%d\n",ans);
    return 0;
}

长链剖分

长链剖是dsu on tree的一种特殊情况。就是说现在不查整个子树,而是给定一个深度deep,你需要合并子树deep相同的信息。
“静态查询子树和深度有关的信息”它一定是“静态子树类查询问题”的子集,
那长链剖优秀在哪里呢,它优秀在复杂度上面了,它的基础复杂度为O(N)不像dsu on tree带log,这样的话长链剖套个线段树进去,就能比dsu on tree在相同复杂度上做到更多功能。
一般讲的“长链剖”是指dsu on tree的第一步(树链剖分预处理)这部分用长链剖分代替轻重树链剖分。
具体实现见博客

例题

1.智乃酱的子树查询类问题
用线段树维护链的信息

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#define ll long long 
#define pa pair<int,int>
#define fi first
#define se second
#define mp make_pair
#define pb push_back
using namespace std;
const int maxn=400000+101;
const int MOD=998244353;
const int inf=2147483647;
const double pi=acos(-1);
int read(){
    int x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
int n,m,val[maxn];
struct ifo{
    ll sum;
    int maxx,minn;
    ifo(){sum=0;maxx=-1;minn=inf;}
    ifo operator+(ifo x){
        ifo a;
        a.sum=sum+x.sum;
        a.maxx=max(maxx,x.maxx);
        a.minn=min(minn,x.minn);
        return a;
    }
}tr[maxn<<1];

struct Tree{
    void up(int k){
        tr[k].sum=tr[k<<1].sum+tr[k<<1|1].sum;
        tr[k].maxx=max(tr[k<<1].maxx,tr[k<<1|1].maxx);
        tr[k].minn=min(tr[k<<1].minn,tr[k<<1|1].minn);
        return ;
    }

    void modify(int k,int l,int r,int L,int R,ifo val){
        if(r<L || R<l)return ;
        if(L<=l && r<=R){
            tr[k]=tr[k]+val;
            return ;
        }
        int mid=(l+r)>>1;
        modify(k<<1,l,mid,L,R,val);modify(k<<1|1,mid+1,r,L,R,val);
        up(k);return ;
    }

    ifo query(int k,int l,int r,int L,int R){
        if(L<=l && r<=R)return tr[k];
        int mid=(l+r)>>1;
        ifo ans;
        if(L<=mid)ans=query(k<<1,l,mid,L,R);
        if(R>mid)ans=ans+query(k<<1|1,mid+1,r,L,R);
        return ans;
    }
}T;

struct question{
    int x,l,r;
    ifo ans;
}q[maxn];
vector<int>qq[maxn];

struct DSUC{
    int tot,head[maxn],to[maxn],nx[maxn];
    void add(int x,int y){to[++tot]=y;nx[tot]=head[x];head[x]=tot;}
    
    int fa[maxn],son[maxn],len[maxn];
    void dfs1(int x){
        for(int i=head[x];i;i=nx[i]){
            int v=to[i];if(v==fa[x])continue;
            fa[v]=x;dfs1(v);
            if(len[son[x]]<len[v])son[x]=v;
        }
        len[x]=len[son[x]]+1;
        return ;
    }

    int cnt,L[maxn],R[maxn],id[maxn];
    void dfs2(int x){
        L[x]=++cnt;id[cnt]=x;R[x]=L[x]+len[x]-1;
        if(son[x])dfs2(son[x]);
        for(int i=head[x];i;i=nx[i]){
            int v=to[i];if(v==fa[x] || v==son[x])continue;
            dfs2(v);
        }
        return ;
    }

    void dsu(int x){
        if(son[x])dsu(son[x]);
        for(int i=head[x];i;i=nx[i]){
            int v=to[i];if(v==fa[x] || v==son[x])continue;
            dsu(v);
            for(int j=L[v],k=1;j<=R[v];j++,k++){
                ifo last=T.query(1,1,n,j,j);
                T.modify(1,1,n,L[x]+k,L[x]+k,last);
            }
        }
        ifo now;now.sum=now.maxx=now.minn=val[x];
        T.modify(1,1,n,L[x],L[x],now);
        for(auto i: qq[x]){
            q[i].ans=T.query(1,1,n,L[x]+q[i].l,min(R[x],L[x]+q[i].r));
        }
        return ;
    }
}D;

int main(){
    n=read();
    for(int i=1;i<=n;i++)val[i]=read();
    for(int i=1;i<n;i++){
        int u=read(),v=read();
        D.add(u,v);D.add(v,u);
    }
    m=read();
    for(int i=1;i<=m;i++){
        q[i].x=read();q[i].l=read();q[i].r=read();
        qq[q[i].x].pb(i);
    }
    D.dfs1(1);D.dfs2(1);D.dsu(1);
    for(int i=1;i<=m;i++){
        printf("%d %d %lld\n",q[i].ans.minn,q[i].ans.maxx,q[i].ans.sum);
    }
    return 0;
}
posted @ 2022-07-13 22:57  I_N_V  阅读(71)  评论(0编辑  收藏  举报