dsu on tree:关于一类无修改询问子树可合并问题
dsu on tree:关于一类无修改询问子树可合并问题
开始学长讲课的时候听懂了但是后来忘掉了....最近又重新学了一遍
所谓\(dsu\ on\ tree\)就是处理本文标题:无修改询问子树可合并问题。
\(dsu\)是并查集,\(dsu\ on\ tree\)是树上启发式合并,基于树剖(轻重链剖分)。
无修改好理解,询问子树也好理解,啥是可合并啊?
举个简单的例子,集合的\(gcd\)就是可以合并的,就是两个集合\(gcd\)的\(gcd\);桶也是能合并的,对应位置相加就好了,诸如此类。
怎么处理呢?
用一个例题来讲吧(网上几乎所有博客都是这两个例题,我也就不例外了
我们考虑这样的一个暴力:对于第\(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\)的练习题吧:
加油哦~
dsu on tree:关于一类无修改询问子树可合并问题的更多相关文章
- 【学习笔记】dsu on tree
我也不知道为啥这要起这名,完完全全没看到并查集的影子啊…… 实际上原理就是一个树上的启发式合并. 特点是可以在$O(nlogn)$的时间复杂度内完成对无修改的子树的统计,复杂度优于莫队算法. 局限性也 ...
- 【CF600E】Lomsat gelral(dsu on tree)
[CF600E]Lomsat gelral(dsu on tree) 题面 洛谷 CF题面自己去找找吧. 题解 \(dsu\ on\ tree\)板子题 其实就是做子树询问的一个较快的方法. 对于子树 ...
- dsu on tree详解
这个算法还是挺人性化的,没有什么难度 就是可能看起来有点晕什么的. 大体 思想是 利用重链刨分来优化子树内部的查询. 考虑一个问题要对每个子树都要询问一次.我们暴力显然是\(n^2\)的. 考虑一下优 ...
- [探究] dsu on tree,一类树上离线问题的做法
dsu on tree. \(\rm 0x01\) 前言\(\&\)技术分析 \(\bold{dsu~on~tree}\),中文别称"树上启发式合并"(虽然我并不承认这种称 ...
- dsu on tree题表
dsu on tree,又名树上启发式合并.重链剖分,是一类十分实用的trick,它常常可以作为一些正解的替代算法: 1.DFS序+线段树/主席树/线段树合并 2.对DFS序分块的树上莫队 3.长链剖 ...
- dsu on tree学习笔记
前言 一次模拟赛的\(T3\):传送门 只会\(O(n^2)\)的我就\(gg\)了,并且对于题解提供的\(\text{dsu on tree}\)的做法一脸懵逼. 看网上的其他大佬写的笔记,我自己画 ...
- dsu on tree入门
先瞎扯几句 说起来我跟这个算法好像还有很深的渊源呢qwq.当时在学业水平考试的考场上,题目都做完了不会做,于是开始xjb出题.突然我想到这么一个题 看起来好像很可做的样子,然而直到考试完我都只想出来一 ...
- Dsu on Tree
这个属于一种技巧,可以解决类似于子树询问无修改可离线的问题,一些点分治的问题也可以用Dsu on Tree解决,并且常数较小,代码复杂度低,很具有可写性. 整体上的意思就是继承重儿子的信息,暴力修改轻 ...
- [算法学习] dsu on tree
简介 dsu on tree跟dsu没有关系,但是dsu on tree借鉴了dsu的启发式合并的思想. 它是用来解决一类树上的询问问题,一般这种问题有以下特征: \(1.\)只有对子树的查询: \( ...
随机推荐
- php配置之include_path
在php.ini中配置include_path,可在引入文件时直接引入配置目录下的文件. 项目中就可以直接 引入/var/www/phpxwlib及/var/www/huicuiserver/libs ...
- BZOJ 2157: 旅游 (2017.7.21 6:30-2017.7.21 15:38 今日第一题。。)
Time Limit: 10 Sec Memory Limit: 259 MBSubmit: 1754 Solved: 765 Description Ray 乐忠于旅游,这次他来到了T 城.T ...
- HttpClient 接口调用
String url = "http://127.0.0.1:8080/api"; //然后根据表名获取公司信息 HttpPost httppost = new HttpPost( ...
- break,continue,return的区别
break,continue,return的区别 break 当break语句用于循环语句时,会终止执行循环,并执行循环后代码(如果有的话). function main() { for(var i ...
- dropdb - 删除一个现有 PostgreSQL 数据库
SYNOPSIS dropdb [ option...] dbname DESCRIPTION 描述 dropdb 删除一个现有 PostgreSQL 数据库. 执行这条命令的人必须是数据库超级用户, ...
- nodejs 安装 淘宝镜像
临时使用 npm --registry https://registry.npm.taobao.org install express 2.持久使用 npm config set registry h ...
- QT_3
1.QT中命名的规范和常用的快捷键 1.1 命名规范: 类名:首字母大写 多个单词时单词与单词之间首 字母大写 函数名:变量名称 首字母小写 多个单词时,单词和单词之间首字母大写 1. ...
- gcc编译问题
gcc avl.o hash.o list.o rb.o example.o -o 123.exe 多个.o输出 exe -c和-o都是gcc编译器的可选参数.-c表示只编译(compile)源文件但 ...
- android开发链接
http://blog.csdn.net/zz2043191420/article/details/47338591
- js事件默认行为
事件默认行为: 当一个事件发生的时候浏览器自己默认做的事情 怎么阻止? 当前这个行为是什么事件触发的,然后在这个事件的处理函数中使用 return false; 但是return false 阻止的是 ...