Martian148's blog Martian148's blog
首页
  • ICPC 算法笔记
  • ICPC 算法题解
  • 体系结构
  • 高等数学
  • 线性代数
  • 概率论与数理统计
  • 具体数学
  • Martian148的奇思妙想
  • 游记
  • 通识课笔记
关于
  • useful 网站
  • 友情链接
  • 分类
  • 归档

Martian148

一只热爱文科的理科生
首页
  • ICPC 算法笔记
  • ICPC 算法题解
  • 体系结构
  • 高等数学
  • 线性代数
  • 概率论与数理统计
  • 具体数学
  • Martian148的奇思妙想
  • 游记
  • 通识课笔记
关于
  • useful 网站
  • 友情链接
  • 分类
  • 归档
  • 线上赛板子(实时更新)
  • 数据结构

    • 堆
    • 并查集
    • 树状数组
    • 分块
    • 树相关
      • 树的重心
        • 性质
        • 实现
      • 树分块
        • 引言
        • 概述
        • 分块方法
        • 法一
        • 法二
        • 法三
      • 树哈希
        • 算法实现
        • 代码实现
      • 树上启发式合并
        • 算法实现
    • 线段树
    • 平衡树
    • 树链剖分
    • LCT
    • 最近公共祖先
    • 虚树
    • 树分治
    • K-D Tree
    • 笛卡尔树
    • 珂朵莉树
  • 数学

  • 计算几何

  • 动态规划

  • 图论

  • 字符串

  • 杂项

  • 算法笔记
  • 数据结构
martian148
2024-09-03
目录

树相关

# 树相关

# 树的重心

如果在树中选择某个节点并删除,这棵树将分为若干棵子树,统计子树节点数并记录最大值。取遍树上所有节点,使此最大值取到最小的节点被称为整个树的重心。

(这里以及下文中的「子树」若无特殊说明都是指无根树的子树,即包括「向上」的那棵子树,并且不包括整棵树自身。)

# 性质

  • 树的重心如果不唯一,则至多有两个,且这两个重心相邻
  • 以树的重心为根时,所有子树的大小都不超过整棵树大小的一半
  • 树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么到它们的距离和一样
  • 把两棵树通过一条边相连得到一棵新的树,那么新的树的重心在连接原来两棵树的重心的路径上
  • 在一棵树上添加或删除一个叶子,那么它的重心最多只移动一条边的距离

# 实现

在 DFS 中计算每个子树的大小,记录「向下」的子树的最大大小,利用总点数 - 当前子树(这里的子树指有根树的子树)的大小得到「向上」的子树的大小,然后让向上的子树和向下最大的子树比较一下得出最大的 "子树"

然后根据定义,最大 "子树" 最小来求出答案

// 这份代码默认节点编号从 1 开始,即 i ∈ [1,n]
int size[MAXN],  // 这个节点的「大小」(所有子树上节点数 + 该节点)
    weight[MAXN],  // 这个节点的「重量」,即所有子树「大小」的最大值
    centroid[2];  // 用于记录树的重心(存的是节点编号)

void GetCentroid(int cur, int fa) {  // cur 表示当前节点 (current)
  size[cur] = 1;
  weight[cur] = 0;
  for (int i = head[cur]; i != -1; i = e[i].nxt) {
    if (e[i].to != fa) {  // e[i].to 表示这条有向边所通向的节点。
      GetCentroid(e[i].to, cur);
      size[cur] += size[e[i].to];
      weight[cur] = max(weight[cur], size[e[i].to]);
    }
  }
  weight[cur] = max(weight[cur], n - size[cur]);
  if (weight[cur] <= n / 2) {  // 依照树的重心的定义统计
    centroid[centroid[0] != 0] = cur;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 树分块

# 引言

在大多数时候,树上问题可以使用树剖、动态树和树分治进行解决,但在一些的特殊的情况下,或者在数据范围较为宽松的情况下,树分块也是一种不错的选择

# 概述

树分块是一种暴力算法, 大致思想就是将树上的点每次划分进 N\sqrt{N}N​ (或者基于题目性质的更优块大小)个块中,对与块内信息进行维护的算法

# 分块方法

# 法一

按照 DFS 序进行划分,不能保证直径长度和块联通,一般用于处理子树信息,在处理其它信息时较为乏力

# 法二

检查当前节点的父亲所在块的大小,如果小于 N\sqrt{N}N​ 就把当前的节点加进去,如何实现呢,用一个栈存下每个dfs的点,如果和父节点的距离超过块的大小,那么就把 这些点弹出作为一个块

是比较常用的分块方法,缺点就是不能确保块的数量

# 法三

没看懂,一种随机化算法???

# 树哈希

判断一些树是否同构的时,我们常常把这些树转成哈希值储存起来,以降低复杂度

树哈希的方法有很多,但是有些方法容易被卡掉,下面介绍一种不容易被卡的哈希方法

# 算法实现

我们需要一个多重集的哈希函数,是一个集合哈希出一个值。以某个结点为根的子树的哈希值,就是以它的所有儿子为根的子树的哈希值构成的多重集的哈希值,即:

hx=f({hi∣i∈son(x)}) h_x=f(\{h_i|i\in son(x)\}) hx​=f({hi​∣i∈son(x)})

其中 hxh_xhx​ 表示以 xxx 为根的子树哈希值, fff 是多重集哈希函数。

以代码中使用的哈希函数为例:

f(S)=(c+∑x∈Sg(x))mod⁡m f(S)=(c+\sum\limits_{x\in S}g(x))\operatorname{mod} m f(S)=(c+x∈S∑​g(x))modm

其中 ccc 为常数,一般使用 111 即可。mmm 为模数,一般使用 2322^{32}232 或 2642^{64}264 进行自然溢出,或者使用大素数。ggg 为整数到整数的映射,代码中使用 xor shift,也可以选用其他的函数,但是不建议使用多项式。为了预防出题人对着 xor hash 卡,还可以在映射前后异或一个随机常数。

这就是一段 xor shift 函数

ull shift(ull x){
    x^=mask;
    x^=x<<13;
    x^=x>>7;
    x^=x<<17;
    x^=mask;
    return x;
}
1
2
3
4
5
6
7
8

如果需要换根,第二次 DP 时只需把子树哈希减掉即可

# 代码实现

#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const ull mask=std::chrono::steady_clock::now().time_since_epoch().count();

ull shift(ull x){
    x^=mask;
    x^=x<<13;
    x^=x>>7;
    x^=x<<17;
    x^=mask;
    return x;
}

const int maxn=1e6+5;
int n;
ull hsh[maxn];
vector<int> E[maxn];
set<ull> trees;

void get_hash(int u,int fa){
    hsh[u]=1;
    for(int& v:E[u]){
        if(v==fa) continue;
        get_hash(v,u);
        hsh[u]+=shift(hsh[v]);
    }
    trees.insert(hsh[u]);
}

int main(){
    scanf("%d",&n);
    for(int i=1;i<n;i++){
        int u,v;
        scanf("%d%d",&u,&v);
        E[u].push_back(v);
        E[v].push_back(u);
    }
    get_hash(1,0);
    printf("%lu",trees.size());
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 树上启发式合并

启发式算法是基于人类的经验和直觉感觉,对一些算法的优化

在并查集的按秩合并中,我们把小的集合与大的集合合并,代码如下

void merge(int x, int y) {
  int xx = find(x), yy = find(y);
  if (size[xx] < size[yy]) swap(xx, yy);
  fa[yy] = xx;
  size[xx] += size[yy];
}
1
2
3
4
5
6

在树中,我们把高度小的树成为高度较大的树的子树,这个优化可以称为启发式合并算法

# 算法实现

来看一个问题

给出一个 nnn 个节点以 111 为根的树,节点 uuu 的颜色为 cnc_ncn​。现在对每个结点 uuu 子树里一共出现了多少种不同的颜色,n≤2×105n\le 2 \times 10^5n≤2×105

image.png

这个问题有很多解决方法,比如树套树,当然如果可以离线,树上莫队也可以,但是树上莫队带根号,可以用 log 实现

既然支持离线,考虑预处理后 O(1)O(1)O(1) 输出答案,直接暴力预处理的时间复杂度为 O(n2)O(n^2)O(n2) ,即对每个子节点进行一次遍历,每次遍历的复杂度显然与 nnn 同阶,有 nnn 个结点,故总时间复杂度为 O(n2)O(n^2)O(n2)

可以发现,每个节点的答案由其子树和其本身得到,考虑利用这个性质处理问题

我们可以处理好重儿子,然后想象让轻儿子 "加" 到重儿子上面

我们用 cnt[i]cnt[i]cnt[i] 表示颜色 iii 出现的次数

对于一个节点 uuu:

  1. 我们先遍历轻儿子,求出轻儿子的答案,但不保留遍历后它对 cntcntcnt 数组的影响
  2. 遍历它的重儿子,求出重儿子的答案,保留它对 cntcntcnt 数组的影响
  3. 再次遍历 uuu 的轻儿子,在重儿子 cntcntcnt 数组上加上轻儿子的贡献,求出 uuu 的答案

image.png

注意,除了重儿子,每次遍历完 cntcntcnt 要清零

上次更新: 2025/04/08, 18:03:31
分块
线段树

← 分块 线段树→

最近更新
01
Java基础语法
05-26
02
开发环境配置
05-26
03
pink 老师 JavaScript 学习笔记
05-26
更多文章>
Theme by Vdoing | Copyright © 2024-2025 Martian148 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式