dsu on tree:关于一类无修改询问子树可合并问题

dsu on tree:关于一类无修改询问子树可合并问题

开始学长讲课的时候听懂了但是后来忘掉了....最近又重新学了一遍

所谓\(dsu\ on\ tree\)就是处理本文标题:无修改询问子树可合并问题。

\(dsu\)是并查集,\(dsu\ on\ tree\)是树上启发式合并,基于树剖(轻重链剖分)。

无修改好理解,询问子树也好理解,啥是可合并啊?

举个简单的例子,集合的\(gcd\)就是可以合并的,就是两个集合\(gcd\)的\(gcd\);桶也是能合并的,对应位置相加就好了,诸如此类。

怎么处理呢?

用一个例题来讲吧(网上几乎所有博客都是这两个例题,我也就不例外了

CF 600 E.Lomsat gelral

我们考虑这样的一个暴力:对于第\(i\)棵子树,暴力扫整棵子树然后更新答案,时间复杂度是\(O(\sum size)\),\(n^2\)的。

显然,很多点都被重复扫了很多遍,可不可以优化这个过程?

我们把整棵树轻重链剖分一下。

假设我们每次\(dfs?\)这棵子树后就会把当前点的答案算出来。

暴力的时候我们就是维护了一个桶,现在我们要:承接重儿子的桶然后暴力扫轻儿子。

每个点肯定都属于一条重链,如果不是叶子就一定有重儿子,重儿子更新答案后的桶我们不删,留着。

然后暴力扫这个点的所有轻儿子来得到这棵子树的桶,之后把轻儿子的桶都删掉。

这样的话显然是对的,时间复杂度怎么保证?

考虑:除了正常的递归之外,每个点会被当做某个点的轻儿子的子孙暴力扫多少遍?

不难发现,只有在这个点的祖先中,存在一对父子关系使得儿子是父亲的轻儿子这个点才会被暴力扫一次。

而一旦出现了这种关系就表示那对父子关系中的儿子是一条重链的链头。

所以一个点的被更新次数就是这个点到根的重链链头数(减一,因为除了根节点

最多只有\(log\)对这样的关系,为什么?

重链剖分其实是保证了这个事情的。

因为每次存在这种关系那么这个轻儿子的\(size\)至多是其父亲\(size\)的一半。

假设这个点到根的链上存在\(num\)对这样的关系,每存在这样一对关系就表示其父亲对\(size\)至少\(\times 2\)。

所以整棵树的\(size\)至少是\(2^{num}\),显然\(num\)最多就\(log\)。

故此总时间复杂度是\(O(nlogn)\)。

代码:

#include <bits/stdc++.h>
#define N 1000010
using namespace std;
typedef long long ll;
int to[N<<1],nxt[N<<1],head[N],tot;
int col[N],Stack[N],son[N],size[N];
int mx=0; ll sum=0,ans[N];
char *p1,*p2,buf[100000];
#define nc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,100000,stdin),p1==p2)?EOF:*p1++)
int rd() {int x=0,f=1; char c=nc(); while(c<48) {if(c=='-') f=-1; c=nc();} while(c>47) x=(((x<<2)+x)<<1)+(c^48),c=nc(); return x*f;}
inline void add(int x,int y) {to[++tot]=y; nxt[tot]=head[x]; head[x]=tot;}
// 求出重儿子。
void dfs1(int p,int fa)
{
    size[p]=1;
    for(int i=head[p];i;i=nxt[i]) if(to[i]!=fa)
    {
        dfs1(to[i],p);
        size[p]+=size[to[i]];
        if(size[to[i]]>size[son[p]]) son[p]=to[i];
    }
}
void add(int p,int fa,int val)
{
    // 暴力遍历以p为根的子树,直接统计出答案。
    Stack[col[p]]+=val;
    if(Stack[col[p]]>=mx)
    {
        if(Stack[col[p]]>mx) mx=Stack[col[p]],sum=0;
        sum+=col[p];
    }
    for(int i=head[p];i;i=nxt[i]) if(to[i]!=fa) add(to[i],p,val);
}
void dfs2(int p,int fa,int opt)
{
    // 因为重儿子的信息不删除,所以可能对处理轻儿子的答案时有影响,故此我们先处理轻儿子再处理重儿子。
    for(int i=head[p];i;i=nxt[i]) if(to[i]!=fa&&to[i]!=son[p]) dfs2(to[i],p,0);
    if(son[p]) dfs2(son[p],p,1);
    // add函数是暴力遍历一颗子树。我们就暴力的把轻儿子整体的信息扫出来。
    for(int i=head[p];i;i=nxt[i]) if(to[i]!=fa&&to[i]!=son[p]) add(to[i],p,1);
    // 把当前点的信息加上,然后统计当前点答案。
    Stack[col[p]]++;
    if(Stack[col[p]]>=mx)
    {
        if(Stack[col[p]]>mx) mx=Stack[col[p]],sum=0;
        sum+=col[p];
    }
    // 记录答案。
    ans[p]=sum;
    // 如果当前点是轻儿子那么我们需要把当前子树全部清空。
    if(!opt) add(p,fa,-1),sum=mx=0;
}
int main()
{
    int n=rd();
    for(int i=1;i<=n;i++) col[i]=rd();
    for(int i=1;i<n;i++) {int x=rd(),y=rd(); add(x,y),add(y,x);}
    dfs1(1,1); dfs2(1,1,1);
    for(int i=1;i<=n;i++) printf("%lld ",ans[i]); puts("");
    return 0;
}

通过这个题我们能大致看到这个算法的精髓。

就是当那些信息可以合并的时候,

我就只保留重儿子留下来的,反正可以通过合并得到所以轻儿子的暴力处理再合并就好了呗。

接下来我们看看被讲烂的第二题:

CF 741 D.Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths

这个题要求的东西还是挺奇葩的。

其实就是求一个简单路径,使得这上面只有一种字符出现了奇数次。

发现这个字符只有\(a\)到\(v\),一共就\(22\)个....这提示就有点大了,我们状压一下。

如果第\(i\)为\(1\)就表示这个字符出现了奇数次,反之出现了偶数次

设\(val[x]\)表示点\(x\)到根的路径的字符状态。

我们发现一个很重要的事情:就是如果两个点\(x\)和\(y\)满足:\(val[x]\oplus val[y]\)只有一位是\(1\),那么他们俩之间的简单路径中也满足只有一位是\(1\)。

因为被算重的部分是\(lca(x,y)?\)到根的路径,被算了两边所以不影响答案。

故此我们就维护\(st[S]\)表示状态为\(S\)的点到根路径的最大深度。

毋庸置疑这鬼东西显然能合并啊,暴力就好了嘛

更新答案的话,不仅要统计经过当前点的,还要和儿子们的答案取个\(max\)。

这里要注意一下,因为是这个合并是个\(max\)所以轻儿子删除的时候需要赋值成极小值。

所以我们\(dfs?\)时候的遍历循序不能一个一个合并,应该是先轻儿子再重儿子。这样重儿子往上传输的答案不会在轻儿子删除的时候覆盖掉。

#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define N 500010
using namespace std;
int to[N<<1],head[N],nxt[N<<1],tot;
int st[1<<23],val[N],son[N],dep[N],ans[N],mdlans,size[N];
inline void add(int x,int y) {to[++tot]=y; nxt[tot]=head[x]; head[x]=tot;}
void dfs1(int p,int fa)
{
    dep[p]=dep[fa]+1; size[p]=1; val[p]^=val[fa];
    for(int i=head[p];i;i=nxt[i]) if(to[i]!=fa)
    {
        dfs1(to[i],p);
        size[p]+=size[to[i]];
        if(size[to[i]]>size[son[p]]) son[p]=to[i];
    }
}
inline void fix(int p) {st[val[p]]=max(st[val[p]],dep[p]);}
void update(int p,int fa,int opt)
{
    if(opt) fix(p);
    else st[val[p]]=-inf;
    for(int i=head[p];i;i=nxt[i]) if(to[i]!=fa) update(to[i],p,opt);
}
inline void pushup(int p)
{
    mdlans=max(mdlans,st[val[p]]+dep[p]);
    for(int i=0;i<23;i++) mdlans=max(mdlans,st[val[p]^(1<<i)]+dep[p]);
}
void get_ans(int p,int fa)
{
    pushup(p);
    for(int i=head[p];i;i=nxt[i]) if(to[i]!=fa) get_ans(to[i],p);
}
void dfs2(int p,int fa,int opt)
{
    for(int i=head[p];i;i=nxt[i]) if(to[i]!=fa&&to[i]!=son[p]) dfs2(to[i],p,0);
    if(son[p]) dfs2(son[p],p,1),ans[p]=ans[son[p]];
    for(int i=head[p];i;i=nxt[i]) if(to[i]!=fa&&to[i]!=son[p])
    {
        get_ans(to[i],p);
        ans[p]=max(ans[p],ans[to[i]]);
        update(to[i],p,1);
    }
    pushup(p);
    fix(p);
    ans[p]=max(ans[p],mdlans-(dep[p]*2)),mdlans=0;
    if(!opt) update(p,fa,0);
}
int main()
{
    char s[10]; int x;
    int n; cin >> n ;
    for(int i=2;i<=n;i++) scanf("%d%s",&x,s+1),val[i]=1<<(s[1]-'a'),add(x,i),add(i,x);
    memset(st,0xef,sizeof st);
    // update(1,1,0);
    dfs1(1,1); dfs2(1,1,1);
    for(int i=1;i<=n;i++) printf("%d ",ans[i]); puts("");
    return 0;
}

好,这两道例题讲完了。

相信各位对\(dsu\ on\ tree?\)有了一个大致的理解。

这东西有时候可以替代点分治。因为好写啊...
它的使用条件有些苛刻,但是一旦满足了这些条件还是比较水的。

而有些问题并不是直接告诉你询问子树,需要进行一些奇奇怪怪的转化,然后才能用这东西解决。

附上两道\(bz?\)的练习题吧:

bzoj 5457 城市

bzoj 4182 Shopping

加油哦~

原文地址:https://www.cnblogs.com/ShuraK/p/10731810.html

时间: 2024-11-09 22:24:18

dsu on tree:关于一类无修改询问子树可合并问题的相关文章

【学习笔记】dsu on tree

我也不知道为啥这要起这名,完完全全没看到并查集的影子啊-- 实际上原理就是一个树上的启发式合并. 特点是可以在$O(nlogn)$的时间复杂度内完成对无修改的子树的统计,复杂度优于莫队算法. 局限性也很明显:1.不能支持修改  2.只能支持子树统计,不能链上统计.(链上统计你不能直接树剖吗?) 那么它是怎么实现的呢?首先有一个例子:树上每个节点都有一个颜色(那么一定是蓝色), 求每个节点的子树上有多少颜色为k的节点.(每个节点的k不一定相同) $O(n^2)$的算法非常好想,以每个点为起点dfs

[探究] dsu on tree,一类树上离线问题的做法

dsu on tree. \(\rm 0x01\) 前言\(\&\)技术分析 \(\bold{dsu~on~tree}\),中文别称"树上启发式合并"(虽然我并不承认这种称谓),大概是一种优雅的暴力,并且跟\(dsu\)毫无关系.于是我打算叫他\(\bold{Elegantly~Direct~Counting~on~Tree}\),"优雅的树上暴力统计". 严格来说,\(\bold {EDCT}\)解决的问题范围并不广泛: 1.维护子树信息: 2.不能带修改

dsu on tree

osu on tree? dsu on tree! 这种操作可以在O(nlogn)的时间内解决一些无修改子树询问问题. 咱知道把一棵树轻重链剖分后,树的轻链,重链都只有O(logn)个. 这个算法就是利用了这一点,递归处理时保留重儿子的信息,轻儿子的则重新计算. 乍一看感觉很暴力,但是实际上是O(nlogn)的. 来看几道题吧. CodeForces - 600E 1 #include<cstdio> 2 #include<vector> 3 #include<bitset&

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

这个故事告诉我们,在做一个辣鸡出题人的比赛之前,最好先看看他发明了什么新姿势= =居然直接出了道裸题 参考链接: http://codeforces.com/blog/entry/44351(原文) http://blog.csdn.net/QAQ__QAQ/article/details/53455462 这种技巧可以在O(nlogn)的时间内解决绝大多数的无修改子树询问问题. 例1 子树颜色统计 有一棵n个点的有根树,根为1,每个点有一个1~n的颜色,对于每一个点给了一个数k,要询问这个子树

poj2104 主席树 区间K大 在线 无修改

关于主席树: 主席树(Chairman Tree)是一种离线数据结构,使用函数式线段树维护每一时刻离散之后的数字出现的次数,由于各历史版本的线段树结构一致,可以相减得出区间信息,即该区间内出现的数字和对应的数量,由于在线段树内,左子树代表的数字都小与右子树,便可像平衡树一样进行K大询问.新建一颗树是\(O(logn)\),查询一次也为\(O(logn)\). 比划分树好想&写多了,但是在POJ上比划分树慢一些. CODE: 1 #include <cstdio> 2 #include

DSU on tree浅谈

DSU on tree 在之前的一次比赛中,学长向我们讲了了这样一个神奇的思想:DSU on tree(树上启发式合并),看上去就非常厉害--但实际上是非常暴力的一种做法;不过暴力只是看上去暴力,它在处理不带修改的子树统计问题时有着优秀的时间复杂度\(O(Nlog N)\),显然在处理这一类问题上,它是优于我们常用的\(dfs\)序后莫队,更关键是它十分好写. 算法实现: 首先对所有轻儿子的子树信息进行统计,然后暴力擦除所有轻儿子的影响.再统计重儿子为根的子树信息,并将轻儿子的信息合并起来,加上

poj2104 划分树 区间K大 在线 无修改

博主sbit....对于高级数据结构深感无力,然后这些东西在OI竟然烂大街了,不搞就整个人都不好了呢. 于是我勇猛的跳进了这个大坑 ——sbit 区间K大的裸题,在线,无修改. 可以用归并树(\(O(nlog^3n)\)),也可用划分树(\(O(nlogn + mlogn)\)).果断划分树...(以后再来看归并树...再来看...来看..看..) 划分树是个什么东西呢?为什么可以做区间k大呢? 想想平衡树做k大时是如何搞的,其实内在原理是一样的. 划分树分两个步骤:建树与询问,这两个步骤相互关

dsu on tree总结

dsu on tree 树上启发式合并.我并不知道为什么要叫做这个名字... 干什么的 可以在\(O(n\log n)\)的时间内完成对子树信息的询问,可横向对比把树按\(dfs\)序转成序列问题的\(O(n\sqrt n)\)莫队算法. 怎么实现 当\(dfs\)到一个点\(u\),执行以下操作: 1.递归处理所有轻儿子; 2.递归处理重儿子; 3.计算整棵子树的贡献(在第2步中重儿子的贡献得以保留,所以不需要重复计算); 4.若点\(u\)不是其父亲的重儿子,删除整棵子树的贡献. 看上去像是

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

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