• 【学习笔记】树上启发式合并 概念+基础运用


    引入

    现在考虑这样一类树上统计问题:

    • 无修改操作,询问允许离线

    • 对子树信息进行统计(链上的信息在某些条件下也可以统计

    用什么来做呢?暴力?树上差分?树上莫队?

    今天要介绍的这种算法 将吊打以上

    有请它闪亮登场——dsu on tree!!!

    看到这个名字,嗯?dsu?不是并查集嘛(至少我第一眼是这么想的),树上并查集?
    其实它和并查集没有半毛钱关系,用dsu这个名字可能只是因为它和并查集都运用了一种思想——启发式合并

    什么意思呢?还记得我们并查集的按秩合并吗?

    在这个合并的过程中 我们把小的集合合并到了大集合中去

    同样的 树上启发式合并实际就是把一棵高度小的子树合并到了大的子树去,这样的一个优化。

    算法内容

    先思考下面这个问题

    给定一棵树 树的每个结点都有一个确定的颜色,询问一些子树的颜色数量(颜色可以重复)

    我们先想想O(N²)的暴力做法

    对于每个节点,暴力遍历子树,将它们的数据统计出来得到当前节点的答案,然后再暴力将这棵子树的数据清空,以免影响到别的节点。

    由于空间的限制,我们不可能对于每一个节点开一个数组来记录数据,只能开一个全局数组。
    在这个全局数组内,如果不清空,就会影响到别的子树,答案混淆于是导致WA

    但是可以发现,统计儿子节点时最后那个节点其实没有必要清空,因为它不再会影响到它的兄弟节点。那么该如何优化呢?

    轻重链剖分优化

    对于节点x,可以在做子树答案时保留最后一棵子树u的数据不清空,然后统计x的答案时绕过u节点统计别的子树。那么u选哪个呢?注意我们是选择一个

    子树来保存答案,对于其他的非重儿子进行暴力,为了复杂度最优,当然是选子树个数多的咯
    于是我们可以通过重儿子进行算法优化。

    可以发现,每个节点的答案由其子树和其本身得到,考虑利用这个性质处理问题。

    首先我们可以先预处理出每个节点子树的size和它的重儿子,重儿子同树链剖分一样,是拥有节点最多子树的儿子,这个过程显然可以O(n)完成

    接下来,对于每一个结点,我们按照这样的方式进行遍历

    • 遍历所有轻儿子,递归结束时消除它们的贡献

    • 遍历所有重儿子,保留它的贡献

    • 再计算当前子树中所有轻子树的贡献

    • 更新答案

    • 如果当前点是轻儿子,消除当前子树的贡献

    这样,对于一个节点,我们遍历了一次重子树,两次非重子树,显然是最划算的。

    通过执行这个过程,我们获得了这个节点所有子树的 ans

    思路已经很清晰了,通过这样的优化,最终的时间复杂度我们发现应该为那么总复杂度就是 O (n log ⁡n)

    最主要的原因

    • 根据树链剖分相关理论,每个点到根的路径上有logn条重链与logn条轻链
    • 即一个点的信息只会上传logn次(详细证明暂且不写,如需可查阅其他大佬证明)

    大致的代码如下所示(随便口糊的,不要真的去测,有个思路就好)

    预处理:

    inline void dfs1(int x,int fa){
        size[x]=1;
        //depth 
        for(int i=head[x];i;i=t[i].nxt){
            int to=t[i].to;
            if(to!=fa){
                dfs1(to,x);
                size[x]+=size[to];
                if(size[to]>size[son[x]])
                    son[x]=to;
            }
        }
    }//dfs预处理 

    求ans

    inline int dfs2(int u,int fa,int isson,int keep){
        if(keep){
            for(int i=head[u];i;i=t[i].nxt){
                int to=t[i].to;
                if(to!=fa&&to!=son[u]){
                    dfs2(to,u,0,1);
                }
            }
        }
        int tmp=0;
        if(!keep&&son[u])tmp+=dfs2(son[u],u,1,0);
        else if(son[u]) tmp+=dfs2(son[u],u,1,1);
        for(int i=head[u];i;i=t[i].nxt){
            int v=t[i].to;
            if(v!=fa&&v!=son[u]){
                tmp+=dfs2(v,u,0,0);
            }
        }
        if(!cnt[c[u]]){
            tmp++;
        }
        cnt[c[u]]++;
        if(keep)ans[u]=tmp;
        if(keep&&!isson) memset(cnt,0,sizeof(cnt));
        return tmp;
    }

    练习

    给一些很不错的例题

    CF600E Lomsat gelral

    UOJ284 快乐游戏鸡

    树上数颜色

    CF208E Blood Cousins

    CF570D Tree Requests

    CF246E Blood Cousins Return

    U95602 射手座之日

     
     
  • 相关阅读:
    Mysql中判断一个点是否落在多边形内
    Linux进阶之环境变量文件/etc/profile、/etc/bashrc、/etc/environment
    gitlab打开group_member页面500错误
    jenkins连接k8s集群
    svn备份迁移
    npm管理nodejs版本
    linux远程执行shell脚本或者python脚本无法获取远程主机的环境变量
    ansible-playbook对服务器安装阿里云日志服务agent--ilogtail
    linux安装Arachni进行web网站扫描
    nginx使用免费ssl证书
  • 原文地址:https://www.cnblogs.com/yjyl0098/p/14169996.html
Copyright © 2020-2023  润新知