dsu on tree(树上启发式合并)

简介

对于一颗静态树,O(nlogn)时间内处理子树的统计问题。是一种优雅的暴力。

算法思想

很显然,朴素做法下,对于每颗子树对其进行统计的时间复杂度是平方级别的。考虑对树进行一个重链剖分。虽然都基于重链剖分,但不同于树剖,我们维护的不是树链。
对于每个节点,我们先处理其轻儿子所在子树,轻子树在处理完后消除其影响。然后处理重儿子所在子树,保留其贡献。然后再暴力跑该点的轻子树,统计该点子树的最终答案。如果该点子树是轻子树,则消除该子树的影响,否则保留。用代码描述的话,大概是这个流程:

void dfs(int u,int fa,int hvy)
{
    for(v :G[u])//处理轻子树
    {
        if(v==f||v==son[u])
            continue;
        dfs(v,u,0);
    }
    if(son[u])//处理重子树
        dfs(son[u],u,1);
    calc(u,fa,1);//暴力统计轻子树对该点答案的贡献
    ans[u]=res;
    if(!hvy)
        calc(u,fa,-1);//若点u所在子树是轻子树,则逆着原来统计的操作来消除其影响。
}

以上体现大概思想,但遇到具体题目可能有很多细节需要思考。

复杂度分析

这个可能不能很容易的明白其为何高效,如何达到O(nlogn)。因此我们考虑每个节点对时间复杂度的贡献。如果真的明白上述的算法流程,可以知道我们执行暴力统计的都是对轻边所连的子树,因此每个点被遍历到的次数与它往上到根的轻边数量有关。而任一点到根的路径上,轻边的数量不会超过logn。因此每个点最多被遍历logn次。这样想应该好理解很多。

举例

Lomsat gelral
这是一道比较经典的入门题,有兴趣的可以练手,感受一下算法的思想,再做下一题。在此不给出代码。
下面稍微讲一下D. Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths
感觉这道题还是挺难的,要考虑不少细节。
题意大概就是每条边有一个字符(a-v),求每颗子树下最长的一条简单路径,其上的字符可重组成回文串。显然就是要至多只有一个字符出现奇数次。
我们把每种字符看作二进制上的一个位,即2的幂。则满足条件的简单路径,其边权异或结果必须为0或2的幂。
因此用到dp和dsu on tree的思想。a[i]表示点i到根的路径异或值,dp[i]表示a[x]=i的点中,深度最大的x的深度。
对于一颗以u为根的子树,它的答案路径(该路径默认包含u,因此可能不是最终答案)可能是1.u到其子树中某点的简单路径;2.u的两颗不同子树中的两点间的路径。前者直接判断来更新答案;对于后者两颗子树间的情况,需要不断更新每个异或值下的最大深度,方便对于跑到的点可以知道此时与它满足条件的另一点的最大深度,从而得知路径长来更新答案。然后若该子树为重子树,则保留dp信息,否则重置。
附上代码

#include<bits/stdc++.h>
#define dd(x) cout<<#x<<" = "<<x<<" "
#define de(x) cout<<#x<<" = "<<x<<"\n"
#define sz(x) int(x.size())
#define All(x) x.begin(),x.end()
#define pb push_back
#define mp make_pair
#define fi first
#define se second
using namespace std;
typedef long long ll;
typedef long double ld;
typedef pair<int,int> P;
typedef priority_queue<int> BQ;
typedef priority_queue<int,vector<int>,greater<int> > SQ;
const int maxn=5e5+10,mod=1e9+7,INF=0x3f3f3f3f;
int a[maxn],dp[maxn*10],sz[maxn],d[maxn],son[maxn],ans[maxn];
vector<int> G[maxn];
void dfs1(int u,int fa)
{
    sz[u]=1;
    d[u]=d[fa]+1;
    a[u]^=a[fa];
    for (auto& v:G[u])
    {
        dfs1(v,u);
        sz[u]+=sz[v];
        if (sz[v]>sz[son[u]])
            son[u]=v;
    }
}
int mx;
bool check(int x,int y)
{
    int t=x^y,cnt=0;
    for (int i=0;i<='v'-'a';++i)
        cnt+=(t>>i)&1;
    return cnt<=1;
}
void cal(int rt,int u)
{
    if (check(a[u],a[rt]))
        mx=max(mx,d[u]-d[rt]);
    mx=max(mx,dp[a[u]]+d[u]-2*d[rt]);
    for (int i=0;i<='v'-'a';++i)
        mx=max(mx,dp[a[u]^(1<<i)]+d[u]-2*d[rt]);
    for (auto& v:G[u])
        cal(rt,v);
}
void upd(int u,int ty)
{
    if (ty)
        dp[a[u]]=max(dp[a[u]],d[u]);
    else
        dp[a[u]]=-INF;
    for (auto& v:G[u])
        upd(v,ty);
}
void dfs2(int u,int hvy)
{
    for (auto&v :G[u])
    {
        if (v==son[u])
            continue;
        dfs2(v,0);
    }
    if (son[u])
        dfs2(son[u],1);
    mx=0;
    mx=max(mx,dp[a[u]]-d[u]);
    for (int i=0;i<='v'-'a';++i)
        mx=max(mx,dp[a[u]^(1<<i)]-d[u]);
    for (auto& v:G[u])
    {
        if (v==son[u])
            continue;
        cal(u,v);
        upd(v,1);
    }
    ans[u]=mx;
    if (hvy)
        dp[a[u]]=max(dp[a[u]],d[u]);
    else
    {
        for (auto& v:G[u])
            upd(v,0);
        dp[a[u]]=-INF;
    }
}
void solve(int u)
{
    for (auto& v:G[u])
    {
        solve(v);
        ans[u]=max(ans[u],ans[v]);
    }
}
int main()
{
    int n;
    cin>>n;
    char c[2];
    for (int i=2;i<=n;++i)
    {
        int f;
        scanf("%d%s",&f,c);
        G[f].pb(i);
        a[i]=1<<(c[0]-'a');
    }
    for (int i=1;i<maxn*10;++i)
        dp[i]=-INF;
    dfs1(1,0);
    dfs2(1,1);
    solve(1);
    for (int i=1;i<=n;++i)
        printf("%d ",ans[i]);
    return 0;
}

原文地址:https://www.cnblogs.com/orangee/p/10463899.html

时间: 2024-10-05 11:56:35

dsu on tree(树上启发式合并)的相关文章

“优美的暴力”——树上启发式合并

今天介绍一个神仙算法:Dsu On Tree[ 树上启发式合并 ] 这个算法用于离线处理询问子树信息,而且很好写. 但是在你没有理解它之前,这是个很鬼畜的算法. 理解后你才能真心感到它的美妙之处. 关键是它是有着媲美线段树合并的时间复杂度的“暴力”算法. 这里说一件事,我学这个东西时找了很多篇博客,它们无一例外地给出了这样一个流程: 1. 先统计一个节点所有的轻儿子 然后删除它的答案2. 再统计这个节点的重儿子 保留他的答案3. 再算一遍所有轻儿子 加到答案中上传 我当时就看的很懵逼,算一遍所有

图论-树上启发式合并(DSU On Tree)

Disjoint Set Union On Tree ,似乎是来自 Codeforces 的一种新操作,似乎被叫做"树上启发式合并". 在不带修改的有根树子树信息统计问题中,似乎树上莫队和这个 DSU On Tree 是两类常规操作. 先对树按轻重链剖分.对于每个节点,先计算轻儿子为根的子树信息,每次计算后消除影响,再去计算其他轻儿子.然后计算重儿子为根的子树信息,不消除影响,并把轻儿子们为根的子树信息加入,再合并这个节点本身的信息.由于一个大小 \(x\) 的子树被消除影响后,都把信

CodeForces 375D. Tree and Queries【树上启发式合并】

传送门 题意 给出一棵 \(n\) 个结点的树,每个结点有一个颜色 \(c_i\) . 询问 \(q\) 次,每次询问以 \(v\) 结点为根的子树中,出现次数 \(\ge k\) 的颜色有多少种.树的根节点是 \(1\). 题解 反正我看见这个 \(\ge k\) 就觉得要用线段树,实际上好像不用写线段树的 Orz. 还是树上启发式合并,记录每种颜色出现的次数,然后线段树记录某种次数有多少颜色,更改就在线段树上改. 这是最后一道树上启发式合并的例题了,以后遇到再刷. #include <bit

【基本操作】树上启发式合并の详解

树上启发式合并是某些神仙题目的常见操作. 这里有一个讲得详细一点的,不过为了深刻记忆,我还是再给自己讲一遍吧! DSU(Disjoint Set Union),别看英文名挺高级,其实它就是并查集…… DSU on tree,也就是树上的启发式合并(众所周知,并查集最重要的优化就是启发式合并). 然后咱们来考虑一个基础题:给出一棵树,每个节点有颜色,询问一些子树中不同的颜色数量(颜色可重复).祖传数据($100000$). 当然,这道题可以被各种方法切,比如带修莫队(做法自行百度). 但莫队的时空

HDU - 4358 Boring counting (树上启发式合并/线段树合并)

题目链接 题意:统计树上每个结点中恰好出现了k次的颜色数. dsu on tree/线段树合并裸题. 启发式合并1:(748ms) 1 #include<bits/stdc++.h> 2 using namespace std; 3 typedef long long ll; 4 const int N=1e5+10; 5 int n,m,k,a[N],b[N],nb,fa[N],son[N],siz[N],cnt[N],ans[N],now,ne,hd[N],ka; 6 struct E {

树上启发式合并入门

前言 树上启发式合并,即\(DSU\ on\ Tree\),是一个挺好用.挺实用的树上信息维护方法. 由于它比较简单,容易理解,因此这里也就简单记录一下吧. 前置知识:重儿子 什么是重儿子? 这应该是树链剖分中的一个概念吧.重儿子就是某个节点的子节点中,子树大小最大的节点. 适用情况 你可以很方便地给每个点染上白色和黑色,且你需要对于每个点都分别得到其子树内节点为黑.子树外节点为白的局面. 具体实现 这是一个比较贪心的过程. 考虑\(dfs\)遍历时,对于当前点的每个儿子,除最后操作的儿子以外,

CodeForces 600E. Lomsat gelral【树上启发式合并】

传送门 好像大家都是拿这道题作为树上启发式合并的板子题. 树上启发式合并,英文是 dsu on tree,感觉还是中文的说法更准确,因为这个算法和并查集(dsu)没有任何关系.一般用来求解有根树的所有子树的统计问题. 根据轻重儿子的各种性质,可以证明这个算法的时间复杂度为 \(O(nlogn)\),虽然看起来暴力的不行,但是却是一个很高效的算法. 算法的核心其实就是对于每个节点,先计算轻儿子,再计算重儿子,把自己和轻儿子的所有贡献累计到重儿子上去,如果自己是轻儿子,就把贡献清空. 如果掌握了树链

【CF600E】Lomsat gelral——树上启发式合并

(题面来自luogu) 题意翻译 一棵树有n个结点,每个结点都是一种颜色,每个颜色有一个编号,求树中每个子树的最多的颜色编号的和. ci <= n <= 1e5 树上启发式合并裸题.统计时先扫一遍得到出现次数最大值,然后再扫一遍看哪个颜色的出现次数与mxCnt相等.注意用一个bool数组判重,清空轻儿子贡献时要顺手把bool数组也打成false. 代码: #include <iostream> #include <cstdio> #include <cctype&

CF 600E 树上启发式合并

DUS on tree 难得都不会,会的都是板子,可悲,可悲 题意:略 先想一个O(n^2)的写法,然后想办法去掉重复计算.究竟哪里重复 了呢? 假设p是x的儿子,p有很多个.每次计算答案的时候,如果“重儿子”(子孙最多的p)的答案可以直接用的话, 就可以省去很多的重复计算,这就是书上启发式合并   DUS ON TREE写法类似板子(恕我无知,没见过其他整法) #include<cstdio> #include<vector> #include<algorithm>