首页 > 其他分享 >树上启发式合并

树上启发式合并

时间:2024-07-20 10:52:23浏览次数:14  
标签:儿子 int siz void 合并 son 启发式 树上 col

树上启发式合并(DSU on tree),和莫队一样都是暴力美学,用于解决一些树上离线问题,尤其是指“对于每个节点,询问关于子树的信息”这类问题。而且虽然是暴力做法,但是时间复杂度神奇的来到了 \(O(nlogn)\) 。

启发式合并

启发式合并是一种思想,在合并两个集合时,优先将小集合合并到大集合中去。这样就能保证合并的总时间复杂度在nlogn以内。

树上启发式合并

对于树上维护的信息,需要将子节点维护的信息全部合并到父亲节点中。但是无论是时间复杂度还是空间复杂度都不允许我们在每一个节点都维护一个数据结构,因此需要实现资源的“复用”(引用Pecco大神说法)。轻儿子只统计自己内部答案,因此求完贡献以后需要将贡献删去;重儿子统计完答案后不会将贡献删除,因此实现了资源的“复用”。(更加简单的说法就是,每次都将轻儿子合并到重儿子中)

具体实现:

  1. 先将整颗树进行轻重链剖分,重儿子相当于一个大集合,我们在合并的过程中都是轻儿子不断的和重儿子合并。

  2. 我们用一个全局变量记录答案和要维护的值,每次合并完都直接记录答案,是离线算法。

  3. 每次递归先进入轻儿子,因为轻儿子只负责自己内部的统计,因此轻儿子所统计的结果要全部删掉。

  4. 轻儿子统计完后,统计重儿子。重儿子的结果在统计完后不删除保留,因为此时轻儿子们已经都算完了,没有用了,我们直接遍历所有轻儿子的所有子树,子树对应的是一段区间,直接将这些儿子合并。

  5. 统计答案,再判断u节点是来自于轻边还是重边,是轻边要记得删掉以u为子树的所有信息。

洛谷题单

题目1

维护此时字数上出现最大颜色的次数与颜色总类型数,如果两者相乘等于子节点数则说明各种颜色出现次数相等。

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define ll long long
#define pii pair<int,int>
#define inf 0x3f3f3f3f
#define endl '\n'
const int N=2e5+5;

vector<int>g[N];
//子树大小与重儿子编号
int siz[N],son[N];
//dfs序
int l[N],r[N],id[N],rnk[N],tot;

void dfs1(int u,int f) 
{
    l[u]=id[u]=++tot;
    rnk[tot]=u;
    siz[u]=1;
    for(auto v:g[u])
    {
        if(v==f) continue;
        dfs1(v,u);
        siz[u]+=siz[v];
        if(!son[u]||siz[v]>siz[son[u]]) son[u]=v;
    }
    r[u]=tot;
}

int w[N],ans;
int col[N],maxx,num,cn[N];

void add(int u) 
{ 
    u=w[u];
    if(col[u]==0) num++;
    cn[col[u]]--;
    col[u]++;
    maxx=max(col[u],maxx);
    cn[col[u]]++;
}
void del(int u) 
{ 
    u=w[u];
    cn[col[u]]--;
    if(cn[col[u]]==0) maxx=col[u]-1;
    col[u]--;
    if(col[u]==0) num--;
    cn[col[u]]++;
}
void dfs2(int u,int f,int keep)
{
    for(auto v:g[u])
    {
        if(v==f||v==son[u]) continue;
        dfs2(v,u,0); //先计算轻儿子的答案
    }
    if(son[u]) dfs2(son[u],u,1); //重儿子贡献保留
    for(auto v:g[u])
    {
        if(v==f||v==son[u]) continue;
        for(int i=l[v];i<=r[v];i++) {
            add(rnk[i]); //添加轻的贡献儿子    
        }
    }
    add(u);
    if(maxx*num==siz[u]) ans++;
    if(keep==0) {
        for(int i=l[u];i<=r[u];i++) {
            del(rnk[i]);
        }
    }
}
void solve()
{
    int n;cin>>n;
    for(int i=1;i<=n;i++)
    {
        int f;cin>>w[i]>>f;
        g[f].push_back(i),g[i].push_back(f);
    }
    dfs1(1,0); 
    dfs2(1,0,0);
    cout<<ans<<endl;
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int T=1;//cin>>T;
    while(T--) solve();
    return 0;
}

题目2

考虑将一个节点 \(j\) 添加进一堆已有的点中,贡献变化。

对于所有小于该点点权的贡献和 \(\sum_{w_{p}< w_{j}}w_{j}\times(w_{j}-w_{p})=k\times w_{j}^{2}-w_{j}\times\sum_{w_{p}< w_{j}}w_{p}\) ;

对于所有大于该点点权的贡献和 $\sum_{w_{p}> w_{j}}w_{p}\times(w_{p}-w_{j})=\sum_{w_{p}> w_{j}}w_{p}^{2}-w_{j}\times\sum_{w_{p}> w_{j}}w_{p} $ 。

小于该数个数、前缀和、后缀和、后缀平方和的修改与查询都可以通过维护树状数组实现,总时间复杂度 \(O(nlog^{2}n)\) 。

#include <bits/stdc++.h>
using namespace std;
#define int unsigned long long
#define ll long long
#define pii pair<int,int>
#define inf 0x3f3f3f3f
#define endl '\n'
const int N=5e5+5;
const int M=1e6;

struct BIT
{
    int t[M+6];
    int lowbit(int x) { return x&(-x); }
    //单点修改
    void update(int pos,int k) {
        for(int i=pos;i<=M;i+=lowbit(i)) t[i]+=k;
    }
    //区间查询
    int ask(int pos) {
        int ans=0;
        for(int i=pos;i;i-=lowbit(i)) ans+=t[i];
        return ans;
    }
}B[3];

vector<int>g[N];
//子树大小与重儿子编号
int siz[N],son[N];
//dfs序
int l[N],r[N],id[N],y[N],tot;

void dfs1(int u,int f)
{
    l[u]=id[u]=++tot;
    y[tot]=u;
    siz[u]=1;
    for(auto v:g[u])
    {
        if(v==f) continue;
        dfs1(v,u);
        siz[u]+=siz[v];
        if(!son[u]||siz[v]>siz[son[u]]) son[u]=v;
    }
    r[u]=tot;
}

int w[N],ans,res[N];
void col(int u,int op)
{
    int num=B[0].ask(w[u]),sum=B[1].ask(w[u]);
    ans+=op*(w[u]*w[u]*num-w[u]*sum)*2;
    int bsum=B[1].ask(M)-B[1].ask(w[u]),bpf=B[2].ask(M)-B[2].ask(w[u]);
    ans+=op*(bpf-bsum*w[u])*2;

    B[0].update(w[u],op*1);
    B[1].update(w[u],op*w[u]);
    B[2].update(w[u],op*w[u]*w[u]);
}
void add(int u) { col(u,1); }
void del(int u) { col(u,-1); }
void dfs2(int u,int f,int keep)
{
    for(auto v:g[u])
    {
        if(v==f||v==son[u]) continue;
        dfs2(v,u,0); //先计算轻儿子的答案
    }
    if(son[u]) dfs2(son[u],u,1); //重儿子贡献保留
    for(auto v:g[u])
    {
        if(v==f||v==son[u]) continue;
        for(int i=l[v];i<=r[v];i++) {
            add(y[i]); //添加轻的贡献儿子    
        }
    }
    add(u);
    res[u]=ans;
    if(keep==0) {
        for(int i=l[u];i<=r[u];i++) {
            del(y[i]);
        }
    }
}
void solve()
{
    int n;cin>>n;
    for(int i=1;i<n;i++)
    {
        int u,v;cin>>u>>v;
        g[u].push_back(v),g[v].push_back(u);
    }
    for(int i=1;i<=n;i++) cin>>w[i];
    int ret=0;
    dfs1(1,0); 
    dfs2(1,0,0);
    for(int i=1;i<=n;i++) ret^=res[i];
    cout<<ret<<endl;
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int T=1;//cin>>T;
    while(T--) solve();
    return 0;
}

标签:儿子,int,siz,void,合并,son,启发式,树上,col
From: https://www.cnblogs.com/mhw-mathcode/p/18312836

相关文章

  • 合并排序数组
    合并排序数组(蓝桥杯题库)题目描述给定排序数组A和B,实现一个算法将B按排序顺序合并到A中。介绍如下:数组A和B的均为排序数组,数字按从小到大排列。数组A的的长度为 ......
  • FastStone Capture v10.6 解锁版 (一款优秀的支持屏幕录制、滚动截图、高清长图、图片
    前言FastStoneCapture是一款极简主义的应用程序,它简单易用,可以捕捉屏幕上的任意区域,提供多种捕获模式,包括活动窗口、指定窗口/对象、矩形区域、手绘区域、整个屏幕和滚动窗口等。此外,FastStoneCapture还附带屏幕录像机、放大镜、取色器和标尺等辅助功能。其体积小巧,但功能强......
  • PDF合并有哪些免费的好用方法?
    将两个或者多个pdf文件合并成一个PDF的操作,目前仍有许多人工手动进行合并,但为了提高我们的效率,小编今天分享如何将多个pdf文件合并成一个的方法,其中包含免费使用的方法哦。方法一、使用在线pdf合并ilovepdf中文在线工具是一款专门用来处理各种日常办公的PDF、office、图片等格式......
  • 多张图片合并成一个PDF文件有免费方法吗?
    大家在日常办公中也会经常使用图片文件,但是为了方便打印,我们一般都会选择将其转换成PDF格式。如何图片转pdf免费呢?下面我们就来分享3种转换方法,帮你解决转换难题!方法一:使用在线转换器ilovepdf中文在线工具是一款专门用来处理各种日常办公的PDF、office、图片等格式文档的工具,同......
  • C 语言实例 - 数组拆分与合并
    将一个数组拆分为两个数组,一个为奇数数组,一个为偶数数组实例#include<stdio.h>intmain(){intarray[10]={0,1,2,3,4,5,6,7,8,9};inteven[10],odd[10];intloop,e,d;e=d=0;for(loop=0;loop<10;loop++){......
  • Leetcoede编程基础0到1——1768. 交替合并字符串& 389. 找不同
    1768.交替合并字符串题目描述:给你两个字符串 word1 和 word2 。请你从 word1 开始,通过交替添加字母来合并字符串。如果一个字符串比另一个字符串长,就将多出来的字母追加到合并后字符串的末尾。返回 合并后的字符串 。输入输出实例:  示例1:输入:word1="ab......
  • 多人合作使用项目使用子模块替代merge繁琐合并
    问:我的main分支的b文件夹只想放b分支的b文件夹里的文件,并且希望b分支更改后我这边也自动更新,请问怎么是实现你希望 main 分支中的 b 文件夹自动保持与 b 分支中的 b 文件夹同步。可以使用子模块(submodule)来实现这种效果。这种方法允许你在一个仓库中包含另一个仓库,并且......
  • 721. 账户合并
    给定一个列表 accounts,每个元素 accounts[i] 是一个字符串列表,其中第一个元素 accounts[i][0] 是 名称(name),其余元素是 emails 表示该账户的邮箱地址。现在,我们想合并这些账户。如果两个账户都有一些共同的邮箱地址,则两个账户必定属于同一个人。请注意,即使两个账户具有......
  • 如何判断树上 $z$ 在 $x,y$ 的简单路径上
    P4606[SDOI2018]战略游戏狗屎虚树+圆方树。顺便第一次打欧拉序求LCA。注意特判根节点的情况即可,甚至不需要dp。P4334[COI2007]Policijasblhy直接给我交题解了,那我就不打了。说一个最重要的点:如何判断树上\(z\)在\(x,y\)的简单路径上?dfn序:满足两个条件。......
  • 如何将几个word文档合并成一个?
    很多人在合并Word文档上选择了最原始的方法,那就是手动将Word文档中的内容复制到另一个文档中,当然了,这个方法也很快,但是,如果你需要合并多份Word文档,那复制粘贴就有点麻烦了,如果可以一键合并,那么就最好不过了,还真的有一键合并的方法,今天就来给大家讲讲合并多个word文档的方法,看看是......