学习笔记点分治

Posted 繁凡さん

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学习笔记点分治相关的知识,希望对你有一定的参考价值。

整理的算法模板合集: ACM模板

点我看算法全家桶系列!!!

实际上是一个全新的精炼模板整合计划


点分治

点分治适合处理大规模的树上路径信息问题。

点分治,即基于树上结点的分治,显然我们在处理路径信息问题的时候,普通结点到根节点的路径信息比较容易计算和维护,因此我们考虑将一棵树分治拆分成若干棵子树,将问题从任意结点到结点间的问题转变为结点与根节点之间的问题递归分治解决。

例如在处理树上任意两点间的最短路径距离问题,显然有两种情况

  1. 经过根节点的路径。
  2. 不经过根节点的路径。

对于第一种路径,有 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 1n1041m1001k1071u,vn1w104

Solution

显然进行点分治。

具体的,我们先找到整棵树的重心作为分治点,开始分治。
每次找到当前分治点的所有子树的重心,作为新的分治点进行递归分治
每次分治的时候,对于分治点,我们处理的都是以该分治点为根的子树的信息
我们 O ( n ) O(n) O(n) 遍历整颗子树进行统计信息即可。
我们统计的时候,计算以根节点直连的点为根的子树的点距分治点的距离,用 rem 存下来,然后进行查询即可。
ext[dis] 表示以 x 为根的子树中路径长度为 dis 是否存在
rem[dis] 表示以 x 的子结点 y 为根的子树遍历到的路径长度 dis 是否存在
所以我们寻找长度为 query[k] 的路径的时候,我们只需要对于当前的 remext 是否存在即可。然后把当前的 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 1n4×104 1 ≤ u , v ≤ n 1\\leq u,v\\leq n 1u,vn 0 ≤ w ≤ 1 0 3 0\\leq w\\leq 10^3 0w103 0 ≤ k ≤ 2 × 1 0 4 0\\leq k\\leq 2\\times 10^4 0k2×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) 也是一组答案, 并且这个时候才有资格对 ansquery(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

以上是关于学习笔记点分治的主要内容,如果未能解决你的问题,请参考以下文章

学习笔记:树分治

POJ1741Tree [点分治]学习笔记

CDQ分治学习笔记

学习笔记分治FFT

[ZJOI2007]捉迷藏 解题报告 (动态点分治)

学习笔记:python3,代码片段(2017)