树上操作(启发式合并、淀粉质、边分治、树套树)
树上性感操作
序言
好的又来写序言了,其实我这一块学的不是很扎实,所以这篇文章就是自己的一个整理,写的丑勿喷。
树上启发式合并(Dsu on Tree)
em学这个首先要学会启发式合并,就是说当两个集合需要合并到一起的时候,把大小较小的集合合并到较大的集合里,这个操作的复杂度仅为 \(O(\log n)\),其实我觉得比较神奇,证明不会证,其实也没有必要学证明(谁出题靠你启发式合并的证明啊?)。
现在考虑树上启发式合并,主要过程就是说,当我们需要处理一些关于子树操作的题目时,一般需要把子树的贡献合并一下,这个时候就是树上启发式合并的作用了。
我们考虑这样一个问题:
- 有一棵 \(n\) 个结点的以 \(1\) 号结点为根的有根树。
- 每个结点都有一个颜色,颜色是以编号表示的, \(i\) 号结点的颜色编号为 \(c_i\)。
- 如果一种颜色在以 \(x\) 为根的子树内出现次数最多,称其在以 \(x\) 为根的子树中占主导地位。显然,同一子树中可能有多种颜色占主导地位。
- 你的任务是对于每一个 \(i\in[1,n]\),求出以 \(i\) 为根的子树中,占主导地位的颜色的编号和。
- \(n\le 10^5,c_i\le n\)
那其实就是要求出子树 \(size\) 最大的编号和。
其实可以用 \(O(n^2)\) 暴力硬搞一下,然后就直接T飞。
然后我们发现其实每个子树的贡献是互相独立的,那么其实就可以单独搞一下。
具体过程如下:
- 首先 \(O(n)\) 遍历一下每个节点,求出重儿子(啊没错就是树剖的那个)。
- 然后对于每个 \(u\) 的子树先处理一下轻儿子答案,再处理一下重儿子(注意,这里轻儿子只计算答案,重儿子不仅要计算答案,还要计算对应的贡献)。
- 再处理一下轻儿子贡献合并一下。
这样看似没什么变化,但是复杂度神奇地变成了 \(O(n \log n)\),别问我,我不会证。
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define PII pair<int,int>
#define mk(a,b) make_pair(a,b)
using namespace std;
template<typename P>
inline void read(P &x){
P res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
res=(res<<3)+(res<<1)+(ch^48);
ch=getchar();
}
x=res*f;
}
template<typename PP>
inline void write(PP x){
if(x<0) putchar('-'),x=-x;
if(x>=10) write(x/10);
putchar('0'+x%10);
}
const int N=1e5+10;
int n;
int col[N],cnt[N];
int siz[N],son[N];
struct edge{
int to,nxt;
}e[N<<1];
int head[N],tot=0;
ll ans[N],sum;
int flag,maxc;
void add(int u,int v){
e[++tot].to=v;
e[tot].nxt=head[u];
head[u]=tot;
}
void dfs1(int u,int fa){
siz[u]=1;
for(int i=head[u];i;i=e[i].nxt){
int to=e[i].to;
if(to==fa) continue;
dfs1(to,u);
siz[u]+=siz[to];
if(siz[to]>siz[son[u]]) son[u]=to;
}
}
void count(int u,int fa,int val){
cnt[col[u]]+=val;
if(cnt[col[u]]>maxc) maxc=cnt[col[u]],sum=col[u];
else if(cnt[col[u]]==maxc) sum+=col[u];
for(int i=head[u];i;i=e[i].nxt){
int to=e[i].to;
if(to==fa || to==flag) continue;
count(to,u,val);
}
}
void dfs2(int u,int fa,bool kep){
for(int i=head[u];i;i=e[i].nxt){
int to=e[i].to;
if(to==fa || to==son[u]) continue;
dfs2(to,u,0);
}
if(son[u]) dfs2(son[u],u,1),flag=son[u];
count(u,fa,1);
flag=0;
ans[u]=sum;
if(!kep) count(u,fa,-1),sum=maxc=0;
}
signed main(){
read(n);
for(int i=1;i<=n;++i) read(col[i]);
for(int i=1;i<n;++i){
int u,v;
read(u),read(v);
add(u,v);
add(v,u);
}
dfs1(1,0);
dfs2(1,0,0);
for(int i=1;i<=n;++i) cout<<ans[i]<<' ';
cout<<endl;
return 0;
}
剩下的以后再补上吧,现在还是不太会。
点分治
当我们处理树上问题时,有没有一种算法能够把复杂度为 \(O(n^2)\) 变成 \(O(n\log n)\) 呢。\(\log n\) 让我们联想到分治,由此出现了点分治。
点分治就是说把某个点去掉后,树会被分成两部分,分别处理两部分后,再分别对两部分分别找一个点继续上面的步骤。
但是我们发现如果只找普通的点就不优,怎么办。
这时我们想到了树的重心,每个子树的大小都趋近于相等,现在我们发现如果去掉重心,每次的规模最少减半,所以可以保证复杂度了。
具体的有如下题目:
给定一棵有 $n$ 个点的树,询问树上距离为 $k$ 的点对是否存在。
我们分成两种路径,第一种是经过重心 \(rt\) 的路径,第二种在子树内不经过重心的路径。
然后对于一个新找出的重心,我们先讨论子树之间的贡献,然后对每个子树的贡献进行递归统计。
其实还是很好理解的。这里不再赘述。
#include<bits/stdc++.h>
#define PII pair<int,int>
#define mk(a,b) make_pair(a,b)
using namespace std;
template<typename P>
inline void read(P &x){
P res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
res=res*10+ch-'0';
ch=getchar();
}
x=res*f;
}
int T=1;
const int N=100010,inf=10000010;
struct edge{
int to,nxt,w;
}e[N<<1];
int n,m,cnt=0,head[N];
int maxp[N],siz[N],dis[N],rem[N];
int vis[N],tet[inf],judge[inf],q[N];
int query[1010];
int sum,rt;
void add(int u,int v,int w){
e[++cnt].w=w;
e[cnt].nxt=head[u];
e[cnt].to=v;
head[u]=cnt;
}
void getrt(int u,int fa){
siz[u]=1;maxp[u]=0;
for(int i=head[u];i;i=e[i].nxt){
int to=e[i].to;
if(to==fa || vis[to]) continue;
getrt(to,u);
siz[u]+=siz[to];
maxp[u]=max(maxp[u],siz[to]);
}
maxp[u]=max(maxp[u],sum-siz[u]);
if(maxp[u]<maxp[rt]) rt=u;
}
void getdis(int u,int fa){
rem[++rem[0]]=dis[u];
for(int i=head[u];i;i=e[i].nxt){
int to=e[i].to;
if(to==fa || vis[to]) continue;
dis[to]=dis[u]+e[i].w;
getdis(to,u);
}
}
void calc(int u){
int p=0;
for(int i=head[u];i;i=e[i].nxt){
int to=e[i].to;
if(vis[to]) continue;
rem[0]=0,dis[to]=e[i].w;
getdis(to,u);
for(int j=rem[0];j;--j){
for(int k=1;k<=m;++k){
if(query[k]>=rem[j]){
tet[k]|=judge[query[k]-rem[j]];
}
}
}
for(int j=rem[0];j;--j) q[++p]=rem[j],judge[rem[j]]=1;
}
for(int i=1;i<=p;++i) judge[q[i]]=0;
}
void sol(int u){
vis[u]=judge[0]=1;calc(u);
for(int i=head[u];i;i=e[i].nxt){
int to=e[i].to;
if(vis[to]) continue;
sum=siz[to];maxp[rt=0]=inf;
getrt(to,0),sol(rt);
}
}
signed main(){
read(n),read(m);
for(int i=1;i<n;++i){
int u,v,w;
read(u),read(v),read(w);
add(u,v,w),add(v,u,w);
}
for(int i=1;i<=m;++i) read(query[i]);
maxp[rt]=sum=n;
getrt(1,0);
sol(rt);
for(int i=1;i<=m;++i){
if(tet[i]) printf("AYE\n");
else printf("NAY\n");
}
return 0;
}
树套树
字面意思,就是某种特殊树的结点还是某颗特殊树。
常见搭配:线段树套平衡树、线段树套主席树、主席树套树状数组...
其实就是说一个树解决不了问题就再套一颗树。
一般来说