学习笔记点分治
Posted 繁凡さん
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学习笔记点分治相关的知识,希望对你有一定的参考价值。
整理的算法模板合集: ACM模板
实际上是一个全新的精炼模板整合计划
点分治
点分治适合处理大规模的树上路径信息问题。
点分治,即基于树上结点的分治,显然我们在处理路径信息问题的时候,普通结点到根节点的路径信息比较容易计算和维护,因此我们考虑将一棵树分治拆分成若干棵子树,将问题从任意结点到结点间的问题转变为结点与根节点之间的问题递归分治解决。
例如在处理树上任意两点间的最短路径距离问题,显然有两种情况
- 经过根节点的路径。
- 不经过根节点的路径。
对于第一种路径,有 dis[u->v] = dis[u->rt] + dis[v->rt]
。
对于第二种路径,我们需要减去 dis[LCA(u,v)]
。较难维护,因此我们只需要使用点分治将树拆分开成若干个子树,显然所有第二种路径问题均可转化为第一种路径问题。
根据题目实际需求我们也可以套用数据结构进行维护。
复杂度
我们发现这样分治递归下去的复杂度是和深度有关的,若深度为 h h h ,则点分治将会递归 h h h 层,总时间复杂度为 O ( n h ) O(nh) O(nh)。因此我们需要尽量的减小深度来优化复杂度。
对于每一棵树来说,都有一个树的重心,由重心的性质可知,我们删掉重心后剩下的子树深度是最小的。且有重心的性质可知我们每次选择重心作为分治点将树分为若干个子树,最多会有 log n \\log n logn 层。若我们进行的是 O ( 1 ) O(1) O(1) 的维护操作,点分治的总时间复杂度为 O ( n log n ) O(n\\log n) O(nlogn)。
模板 luogu P3806【模板】点分治 1
Weblink
https://www.luogu.com.cn/problem/P3806
Problem
给定一棵有 n n n 个点的树,询问树上距离为 k k k 的点对是否存在。
保证 1 ≤ n ≤ 1 0 4 , 1 ≤ m ≤ 100 , 1 ≤ k ≤ 1 0 7 , 1 ≤ u , v ≤ n , 1 ≤ w ≤ 1 0 4 1 \\leq n\\leq 10^4,1 \\leq m\\leq 100,1 \\leq k \\leq 10^7,1 \\leq u, v \\leq n,1 \\leq w \\leq 10^4 1≤n≤104,1≤m≤100,1≤k≤107,1≤u,v≤n,1≤w≤104。
Solution
显然进行点分治。
具体的,我们先找到整棵树的重心作为分治点,开始分治。
每次找到当前分治点的所有子树的重心,作为新的分治点进行递归分治
每次分治的时候,对于分治点,我们处理的都是以该分治点为根的子树的信息
我们
O
(
n
)
O(n)
O(n) 遍历整颗子树进行统计信息即可。
我们统计的时候,计算以根节点直连的点为根的子树的点距分治点的距离,用 rem 存下来,然后进行查询即可。
ext[dis]
表示以 x
为根的子树中路径长度为 dis
是否存在
rem[dis]
表示以 x
的子结点 y
为根的子树遍历到的路径长度 dis
是否存在
所以我们寻找长度为 query[k]
的路径的时候,我们只需要对于当前的 rem
找 ext
是否存在即可。然后把当前的 rem
归为 ext
,这样我们最后遍历完的时候,有一半,最后另一半一定能找到,一定能匹配。
最后我们需要把 ext
清空,如果使用 memset
O
(
n
)
O(n)
O(n) 清空的话,时间复杂度就会暴涨成
O
(
n
2
)
O(n^2)
O(n2) ,所以我们把所有的 ext
存在 used
数组里,清空的时候只需要枚举 used[i]
数组来清空 ext[used[i]]
即可。
若左右链匹配成功则标记 ans[k] = 1
,
O
(
m
)
O(m)
O(m) 输出即可。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 2e4 + 7, M = 2e4 + 7, Q = 2e7 + 7, INF = 0x3f3f3f3f;
int n, m, s, t;
int head[N], ver[M], nex[M], edge[M], tot;
bool vis[N];
int rem[N], num1;
int used[N], num2;
int ans[Q], ext[Q];
int query[N];
int root;
int siz[N], max_size;
int dis[N];
void add(int x, int y, int z)
{
ver[tot] = y;
nex[tot] = head[x];
edge[tot] = z;
head[x] = tot ++ ;
}
void get_rt(int x, int fa, int n)
{
siz[x] = 1;
int tmp = 0;
for (int i = head[x]; ~i; i = nex[i]) {
int y = ver[i];
if(y == fa || vis[y]) continue;
get_rt(y, x, n);
siz[x] += siz[y];
tmp = max(tmp, siz[y]);
}
tmp = max(tmp, n - siz[x]);
if(tmp < max_size) {
max_size = tmp;
root = x;
}
}
void get_vertex_root_dis(int x, int fa)
{
rem[ ++ num1] = dis[x];
for (int i = head[x]; ~i; i = nex[i]) {
int y = ver[i], z = edge[i];
if(y == fa || vis[y]) continue;//这里仅往下遍历子树,防止上去需要判断 vis[y]
dis[y] = dis[x] + z;
get_vertex_root_dis(y, x);
}
}
void cal_exist(int x)
{
num2 = 0;
for (int i = head[x]; ~i; i = nex[i]) {
int y = ver[i], z = edge[i];
if(vis[y]) continue;
num1 = 0, dis[y] = z;
get_vertex_root_dis(y, x);
for (int j = 1; j <= num1; ++ j) {
for (int k = 1; k <= m; ++ k) {
int query_len = query[k] - rem[j];
if(query_len >= 0 &&
query_len <= 1e7 &&
ext[query_len])
ans[k] = 1;
}
}
for (int j = 1; j <= num1; ++ j) {
if(rem[j] <= 1e7) {
used[ ++ num2] = rem[j];
ext[rem[j]] = 1;
}
}
}
for (int i = 1; i <= num2; ++ i)
ext[used[i]] = 0;
}
// divide and conquer
void divide(int x)
{
vis[x] = 1;
ext[0] = 1;
cal_exist(x);
for (int i = head[x]; ~i; i = nex[i]) {
int y = ver[i];
if(vis[y]) continue;
max_size = INF;
get_rt(y, x, siz[y]);
divide(root);
}
}
int main()
{
memset(head, -1, sizeof head);
scanf("%d%d", &n, &m);
for (int i = 1; i <= n - 1; ++ i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
add(u, v, w);
add(v, u, w);
}
for (int i = 1; i <= m; ++ i)
scanf("%d", &query[i]);
max_size = INF;
get_rt(1, 0, n);
divide(root);
for (int i = 1; i <= m; ++ i) {
if(ans[i])
puts("AYE");
else puts("NAY");
}
return 0;
}
Luogu P4178 Tree (点分治 + 树状数组)
Weblink
https://www.luogu.com.cn/problem/P4178
Problem
给定一棵 n n n 个节点的树,每条边有边权,求出树上两点距离小于等于 k k k 的点对数量。
1 ≤ n ≤ 4 × 1 0 4 1≤n≤4×10^4 1≤n≤4×104, 1 ≤ u , v ≤ n 1\\leq u,v\\leq n 1≤u,v≤n, 0 ≤ w ≤ 1 0 3 0\\leq w\\leq 10^3 0≤w≤103 , 0 ≤ k ≤ 2 × 1 0 4 0\\leq k\\leq 2\\times 10^4 0≤k≤2×104。
Solution
树上路径信息问题显然用点分治。
这道题统计的是树上两点距离小于等于 k k k 的点对数量,我们当然可以先算出距离等于 k k k 的点对数量,然后容斥即可。
但我不想动脑子(
我们显然可以开一个桶 bit[i]
表示距离小于等于 i
的点对的数量。
我们使用树状数组维护即可。
时间复杂度 O ( n log n log k ) O(n\\log n\\log k) O(nlognlogk)
具体的,我们先找到分治点
r
o
o
t
root
root,然后计算与
r
o
o
t
root
root 直连的结点
u
u
u 为根的子树距
r
o
o
t
root
root 的距离, 与已经计算好了的其他子树距离去匹配,这棵子树一半,另一颗子树一半,拼起来 dis <= k
即可。因此我们需要先计算对 ans
的贡献,再更新树状数组。 最后因为我们统计完以
u
u
u 为根的子树的贡献之后,会再去统计与
r
o
o
t
root
root 直连的其他结点的贡献,所以我们需要把这棵子树的更新删除,我们记录该子树所有的点,
O
(
n
)
O(n)
O(n) 删除即可。
最后画图可知,所有直连的结点
u
u
u 到根节点
r
o
o
t
root
root 的距离为 dis[u] = edge[root -> u]
,若 dis[u] <= k
,显然点对
(
u
,
r
o
o
t
)
(u, root)
(u,root) 也是一组答案, 并且这个时候才有资格对 ans
有 query(k - dis[u])
的贡献,所以最后的答案 ans += query(k - dis[v]) + 1
,要加上点对
(
u
,
r
o
o
t
)
(u, root)
(u,root) 的贡献
1
1
1。
Code
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 5e5 + 6, M = 3e6 + 7, INF = 0x3f3f3f3f;
int n, m, s, t, k;
int head[N], ver[M], nex[M], edge[M], tot;
bool vis[N];
ll ans;
int dis[N];
int sta[N], num1;
int seq[N], num2;
int root, siz[N], min_son_size;
ll bit[N << 1];
#define lowbit(x) (x & (-x))
void update(int x, int val)
{
for (int i = x; i <= N - 7; i += lowbit(i))
bit以上是关于学习笔记点分治的主要内容,如果未能解决你的问题,请参考以下文章