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

加油哦~

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

  1. 【学习笔记】dsu on tree

    我也不知道为啥这要起这名,完完全全没看到并查集的影子啊…… 实际上原理就是一个树上的启发式合并. 特点是可以在$O(nlogn)$的时间复杂度内完成对无修改的子树的统计,复杂度优于莫队算法. 局限性也 ...

  2. 【CF600E】Lomsat gelral(dsu on tree)

    [CF600E]Lomsat gelral(dsu on tree) 题面 洛谷 CF题面自己去找找吧. 题解 \(dsu\ on\ tree\)板子题 其实就是做子树询问的一个较快的方法. 对于子树 ...

  3. dsu on tree详解

    这个算法还是挺人性化的,没有什么难度 就是可能看起来有点晕什么的. 大体 思想是 利用重链刨分来优化子树内部的查询. 考虑一个问题要对每个子树都要询问一次.我们暴力显然是\(n^2\)的. 考虑一下优 ...

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

    dsu on tree. \(\rm 0x01\) 前言\(\&\)技术分析 \(\bold{dsu~on~tree}\),中文别称"树上启发式合并"(虽然我并不承认这种称 ...

  5. dsu on tree题表

    dsu on tree,又名树上启发式合并.重链剖分,是一类十分实用的trick,它常常可以作为一些正解的替代算法: 1.DFS序+线段树/主席树/线段树合并 2.对DFS序分块的树上莫队 3.长链剖 ...

  6. dsu on tree学习笔记

    前言 一次模拟赛的\(T3\):传送门 只会\(O(n^2)\)的我就\(gg\)了,并且对于题解提供的\(\text{dsu on tree}\)的做法一脸懵逼. 看网上的其他大佬写的笔记,我自己画 ...

  7. dsu on tree入门

    先瞎扯几句 说起来我跟这个算法好像还有很深的渊源呢qwq.当时在学业水平考试的考场上,题目都做完了不会做,于是开始xjb出题.突然我想到这么一个题 看起来好像很可做的样子,然而直到考试完我都只想出来一 ...

  8. Dsu on Tree

    这个属于一种技巧,可以解决类似于子树询问无修改可离线的问题,一些点分治的问题也可以用Dsu on Tree解决,并且常数较小,代码复杂度低,很具有可写性. 整体上的意思就是继承重儿子的信息,暴力修改轻 ...

  9. [算法学习] dsu on tree

    简介 dsu on tree跟dsu没有关系,但是dsu on tree借鉴了dsu的启发式合并的思想. 它是用来解决一类树上的询问问题,一般这种问题有以下特征: \(1.\)只有对子树的查询: \( ...

随机推荐

  1. sql server 2000备份还原数据库

    转载请注明出处:http://blog.csdn.net/neochan1108/article/details/79248017 备份: -- Create the backup device fo ...

  2. (转)Spring4.2.5+Hibernate4.3.11+Struts1.3.8集成方案一

    http://blog.csdn.net/yerenyuan_pku/article/details/52888808 前面我们已经集成了Spring4.2.5+Hibernate4.3.11这两个框 ...

  3. Android(java)学习笔记167:横竖屏切换时Activity的生命周期

    1.横竖屏切换的生命周期     默认情况下横竖屏切换,先销毁再创建 2.有的时候,默认情况下的横竖屏切换(先销毁再创建),对应用户体验是不好的,比如是手机游戏横竖屏切换对游戏体验非常不好,下面两种方 ...

  4. Vue 2.0 右键菜单组件 Vue Context Menu

    Vue 2.0 右键菜单组件 Vue Context Menu https://juejin.im/entry/5976d14751882507db6e839c

  5. js将时间戳装换成日期格式

    13位时间戳改为yyyy-MM-dd HH-mm-ss 格式 目标时间戳:1516324500000 formatDateTime (unix) { // 转换时间戳 var date = new D ...

  6. No-3.Linux 终端命令格式

    Linux 终端命令格式 01. 终端命令格式 command [-options] [parameter] 说明: command:命令名,相应功能的英文单词或单词的缩写 [-options]:选项 ...

  7. Oracle中的for和while循环

    实例: beginfor i in 51..500 loop delete from test t where t.date=to_date('2016-07-01', 'yyyy-MM-dd') a ...

  8. (3) openssl genrsa(生成rsa私钥)

    genrsa用于生成RSA私钥,不会生成公钥,因为公钥提取自私钥,如果需要查看公钥或生成公钥,可以使用openssl  rsa命令. 使用man genrsa查询其用法. openssl genrsa ...

  9. 用tkinter写出you-get下载器界面,并用pyinstaller打包成exe文件

    本文为原创文章,转载请标明出处 一.you-get介绍 you-get是一个基于 python 3 的下载工具,使用 you-get 可以很轻松的下载到网络上的视频.图片及音乐.目前支持网易云音乐.A ...

  10. mysql启动问题

    /usr/local/mysql/bin/mysqld: Can't find file: './mysql/plugin.frm' (errno: 13 - Permission denied) - ...