树上启发式合并

适用:

  • 询问子树的信息
  • 询问期间不进行修改

教练说所有能用树上启发式合并的题都能用线段树分治可是我还没学QwQ

是离线算法噢QwQ 

实现:(分三parts)

 part1:

运用树链剖分中的dfs1操作将结点分好轻重儿子

part2:

对于每一个子树,我们将其分为两部分:

  • 以重儿子为根的子树
  • 以轻儿子为根的子树

由轻重儿子的性质可知,设以轻儿子为根的子树结点总数为num轻,当前树的结点总数为sum,那么num轻一定小于等于sum/2

我们现在要求一个子树的相关信息,那么我们不妨把这个问题拆解成按顺序对其轻重儿子延伸出去的子树进行信息求解,再合并为当前子树的信息,中途加以信息记录和多余信息的删除。

这样我们就可以一求答多问,在离线的情况下达到预处理O(N log2 N),回答O(1)的优秀时间复杂度。

理解到这里其实树上启发式合并就已经差不多OK了,但是我们如果想要期望时间复杂度真正达到达到O(N log N),其实还需要浅运用一下贪心QwQ

上面说了对于求解当前子树信息的问题我们将其拆解为按顺序对其轻重儿子延伸出去的子树进行信息求解,再合并为当前子树的信息,中途加以信息记录和多余信息的删除。但是具体按什么顺序呢?先求解轻儿子的信息还是重儿子的信息呢?这里其实我们只需要模拟一遍求解当前树的信息的全过程就能一目了然到底要按什么顺序了:

模拟前约定:对于当前求解信息的树,我们称其为tree,对于其先求解信息的(轻/重)子树,我们称之为tree1,对于后求解信息的(轻/重)子树,我们称为tree2

求解实现思路概述:(提前定义好一个cnt全局数组,cnt[i]表示我们求解的信息的第i种当前被记录了多少次)我们先通过树链剖分记录每个节点的重儿子,然后通过dfs进入tree

  • 若我们选择先求解重儿子,那么我们可以直接通过之前的标记dfs进入tree的重儿子延伸出去的子树,并存下该子树的信息,然后我们删除之前dfs重儿子时在cnt数组的记录;                    接着我们开始dfs每一个轻儿子的子树,所有关于轻儿子的dfs完成后,我们保留dfs轻儿子时在cnt数组留下的信息,并记录轻儿子的子树的信息,然后重新将重儿子的信息传输进cnt数组中,最后记录tree的信息求解结果;
  • 若先选择轻儿子,其实跟上面的操作差不多
  • 总的来说就是dfs+删除dfs  tree1时在cnt数组留下的信息+dfs+重新将dfs tree1时的信息放入cnt数组中+得到最终的tree的信息求解

模拟开始:

选择先进行轻子树操作(也就是把轻子树当做tree1):

void dfs(tree){

    for(枚举tree中的每一个tree1){

           dfs(当前枚举到的tree1);

            删除dfs tree1时在cnt数组留下的信息;

    }

    if(tree有重儿子【因为如果是叶节点的话就没有儿子可言啦所以要特判】)

            dfs(重儿子);

    for(重新枚举每一个轻儿子)

            重新将tree1的相关信息记录在cnt中;

    记录tree的信息求解结果;

}

选择先进行重子树操作(也就是把重子树当做tree1):

void dfs(tree){

     if(tree有重儿子(tree1)【因为如果是叶节点的话就没有儿子可言啦所以要特判】){

            dfs(tree1);

            删除dfs tree1时在cnt数组留下的信息;

     }

    for(枚举tree中的每一个轻儿子【tree2】){

           dfs(当前枚举到的tree2);

    }

     if(tree有重儿子(tree1))

        重新将tree1的信息补充进cnt中;

    记录tree的信息求解结果;

}

我们感性理解一下,对于每一次dfs我们其实就是要进行两次tree1操作和一次tree2操作,也就是说,整个dfs的时间复杂度可以表示为O(2*操作tree1的时间+1*操作tree2需要的时间)

而通过轻重儿子的相关性质我们知道,以重为根的子树结点总数是相对它的兄弟子树来说最多的,也就是说对重儿子延伸出去的子树的相关信息进行删除补充等操作所需要的时间是相对较多的。

如果我们将重儿子延伸出去的子树作为tree1,那么就要操作以重儿子为根的子树两次,很显然这是很不划算的,所以我们将单次操作时间较少的轻儿子延伸出去的树作为tree1,将重儿子延伸出去的树作为tree2很显然是更合适的。

具体时间复杂度我没听懂教练的证明,所以不写啦!QwQ

Part 3 回答询问

开头说过啦,树上启发式合并是离线算法。

所以我们预处理完之后在不修改树上信息的情况下是可以根据预处理出来的信息实现O(1)回答每次不同的询问哒!

例题

洛谷 U41492 树上数颜色

题目描述

给一棵根为1的树,每次询问子树颜色种类数

输入格式

第一行一个整数n,表示树的结点数

接下来n-1行,每行一条边

接下来一行n个数,表示每个结点的颜色c[i]

接下来一个数m,表示询问数

接下来m行表示询问的子树

输出格式

对于每个询问,输出该子树颜色数

样例

样例输入 #1

5
1 2
1 3
2 4
2 5
1 2 2 3 3
5
1
2
3
4
5

样例输出 #1

3
2
1
1
1

AC~ QwQ

#include <bits/stdc++.h>
using namespace std;
#define MAXN 6
int n, m, ecnt, num, co[MAXN], fir[MAXN], siz[MAXN];
int son[MAXN], cnt[MAXN], ans[MAXN];
struct edge
{
    int v, nxt;
} e[MAXN << 1];
void adde(int a, int b)
{
    e[++ecnt].v = b, e[ecnt].nxt = fir[a], fir[a] = ecnt;
    e[++ecnt].v = a, e[ecnt].nxt = fir[b], fir[b] = ecnt;
}
void dfs1(int no, int fa)
{
    siz[no]++;
    int maxz = 0;
    for (int i = fir[no]; i; i = e[i].nxt)
    {
        int v = e[i].v;
        if (v != fa)
        {
            dfs1(v, no);
            if (siz[v] > maxz)
                maxz = siz[v], son[no] = v;
            siz[no] += siz[v];
        }
    }
}
void dfs2(int, int), up(int, int, int);
int main()
{
    freopen("1.in", "r", stdin);
    freopen("1.out", "w", stdout);
    int a, b;
    scanf("%d", &n);
    for (int i = 1; i < n; i++)
    {
        scanf("%d %d", &a, &b);
        adde(a, b);
    }
    for (int i = 1; i <= n; i++)
        scanf("%d", &co[i]);
    dfs1(1, 0);
    dfs2(1, 0);
    scanf("%d", &m);
    for (int i = 1; i <= m; i++)
    {
        scanf("%d", &a);
        printf("ans: %d\n", ans[a]);
    }
    return 0;
}
void dfs2(int no, int fa)
{
    for (int i = fir[no]; i; i = e[i].nxt)
    {
        int v = e[i].v;
        if (v != son[no] && v != fa)
        {
            dfs2(v, no);
            up(v, no, -1);
        }
    }
    if (son[no])
        dfs2(son[no], no);
    cnt[co[no]]++;
    if (cnt[co[no]] == 1)
        num++;
    for (int i = fir[no]; i; i = e[i].nxt)
    {
        int v = e[i].v;
        if (v != son[no] && v != fa)
            up(v, no, 1);
    }
    ans[no] = num;
}
void up(int no, int fa, int k)//删除/添加点no的贡献
{
    cnt[co[no]] += k;
    if (!cnt[co[no]] && k < 0)
        num--;
    else if (cnt[co[no]] == 1 && k > 0)
        num++;
    for (int i = fir[no]; i; i = e[i].nxt)
    {
        int v = e[i].v;
        if (v != fa)
            up(v, no, k);
    }
}
/*

5
1 2
1 3
2 4
2 5
1 1 1 2 3
5
5 4 3 2 1

*/

练习2 CF600E Lomsat gelral

洛谷 Lomsat gelral

树的节点有颜色,一种颜色占领了一个子树,当且仅当没有其他颜色在这个子树中出现得比它多。求占领每个子树的所有颜色之和。

思想总结:

每次的信息删除其实就是将当前所有查找到的信息全部删除

AC~

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll M=200005;
ll n,e[M*2],to[M*2],co[M],sz[M],son[M],cnt,fir[M],ans[M],note[M],maxx,sum;
void add(ll a,ll b){
    e[++cnt]=fir[a],to[cnt]=b,fir[a]=cnt;
    e[++cnt]=fir[b],to[cnt]=a,fir[b]=cnt;
}
void dfs1(ll no,ll fa){
    ll maxx=0;
    sz[no]++;
    for(ll i=fir[no];i;i=e[i]){
        ll v=to[i];
        if(v!=fa){
            dfs1(v,no);
            if(sz[v]>maxx)
                son[no]=v,maxx=sz[v];
            sz[no]+=sz[v];
        }
    }
}
void dfs(ll,ll),change(ll,ll,ll);
int main(){
    scanf("%d",&n);
    for(ll i=1;i<=n;i++)
        scanf("%lld",&co[i]);
    for(ll i=1;i<n;i++){
        ll a,b;
        scanf("%lld%lld",&a,&b);
        add(a,b);
    }
    dfs1(1,0);
    dfs(1,0);
    for(ll i=1;i<=n;i++){
        printf("%lld ",ans[i]);
    }
    return 0;
}
void dfs(ll no,ll fa){
    for(ll i=fir[no];i;i=e[i]){
        ll v=to[i];
        if(v!=fa&&v!=son[no]){
            dfs(v,no);
            change(v,no,-1);
        }
    }
    if(son[no])
        dfs(son[no],no);
    ++note[co[no]];
    if(note[co[no]]>maxx)
        sum=co[no],maxx=note[co[no]];
    else if(note[co[no]]==maxx)
        sum+=co[no];
    for(ll i=fir[no];i;i=e[i]){
        ll v=to[i];
        if(fa!=v&&son[no]!=v)
            change(v,no,1);
    }
    ans[no]=sum;
}
void change(ll no,ll fa,ll k){
    note[co[no]]+=k;
    if(k<0)
        maxx=0,sum=0;
    else{
        if(note[co[no]]>maxx)
            maxx=note[co[no]],sum=co[no];
        else if(note[co[no]]==maxx)
            sum+=co[no];
    }
    for(ll i=fir[no];i;i=e[i]){
        ll v=to[i];
        if(v!=fa)
            change(v,no,k);
    }
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值