关于点分治的常见写法及时间复杂度

3 篇文章 0 订阅
3 篇文章 0 订阅

关于点分治的常见写法及时间复杂度

0x01 概述

点分治是一种基于树的重心,统计树上路径的优秀算法。将树上的路径分为经过树的重心和不经过树的重心两种,同时利用树的重心性质,使得递归深度不超过 l o g n logn logn次。

总时间复杂取决于每次递归统计答案的时间复杂度。若每次统计是 O ( n ) O(n) O(n)的,那么总时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)。若统计的时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)的,那么总时间复杂度为 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)。这两种时间复杂度均为可也接受的。

0x02 写法

点分治有2种写法:

  1. 对于某个重心 u u u,统计以 u u u为根的所有路径,然后计算出所有组合情况。递归子树时,首先删除全部在一颗子树的路径,然后再进入子树递归求解。这样可以保证路径全部合法且不重不漏。

  2. 对于某个重心 u u u,先进入子树 v 1 v_1 v1,求解出 u u u到子树 v 1 v_1 v1所有节点的路径,然后进入子树 v 2 v_2 v2,进入时先统计答案,然后再统计相关值……每次进入新的子树时,先统计答案,这样每次计算的路径一定是和之前统计过子树的相连而成的,没有不合法的答案,所以不用删除。最后,依次递归进子树,找出重心递归求解。

点分治

图1表示直接统计以重心为根的子树上的路径,递归进入子树之前,首先删除子树中的不合法路径。

图2表示按子树依次递归进入,进入后先统计答案,然后再统计相关值。统计答案是利用到了先前子树的信息。

一般地,写法2常数更小。

0x03 分析

下面来探讨两种写法的时间复杂度。

两种写法首先在“分治”过程中,每次都是中心划分,树的层数(递归深度)是严格 l o g n logn logn的。也就是说总体框架上都是带一个log。

对当前层答案统计、每次问题规模减半,可看作 T ( n ) = T ( n / 2 ) + O ( n ) T(n)=T(n/2)+O(n) T(n)=T(n/2)+O(n)

问题就出在两种写法在”统计计算”这块的时间。

对于写法1,一般会将所有子树的dis全部排个序或者二分,这样就躲不掉的一个log的时间,总时间复杂度是 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)

对于写法2,采用的桶来记录前面子树的dis信息,或占用一定量的空间(当然在范围允许的情况下),或用map增加一个log的时间,总时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)

下给出两份题目的代码,供读者阅读体会:

Luogu P4178 Tree

//写法1实现
#include <bits/stdc++.h>
using namespace std;
inline int read() {
	int x = 0; bool f = 0; char ch = getchar();
	while (!isdigit(ch)) f |= ch == '-', ch = getchar();
	while (isdigit(ch)) x = (x << 1) + (x << 3) + (ch ^ 48), ch = getchar();
	return f ? -x : x;
}
const int MAXN = 4e4 + 6;
int n, k, root, cnte, cntd, ans;
int mxp[MAXN], vis[MAXN], sz[MAXN], hd[MAXN], dis[MAXN];
struct tEdge { int to, nxt, wei; } e[MAXN << 1];
inline void link(int u, int v, int w) {
	e[++cnte] = {v, hd[u], w};
	hd[u] = cnte;
}
void getroot(int u, int p, int n) {
	sz[u] = 1; mxp[u] = 0;
	for (int i = hd[u]; i; i = e[i].nxt) {
		int v = e[i].to;
		if (v == p || vis[v]) continue;
		getroot(v, u, n);
		sz[u] += sz[v];
		mxp[u] = max(mxp[u], sz[v]);
	}
	mxp[u] = max(mxp[u], n - sz[u]);
	if (mxp[u] < mxp[root]) root = u;
	// mxp[u] < mxp[root]    ...YES
	// mxp[u] <= n / 2       ...NO
}
void getdis(int u, int d, int p) {
	dis[++cntd] = d;
	for (int i = hd[u]; i; i = e[i].nxt) {
		int v = e[i].to, w = e[i].wei;
		if (v == p || vis[v]) continue;
		getdis(v, d + w, u);
	}
}
int calc(int u, int d) {
	cntd = 0; getdis(u, d, 0);
	sort(dis + 1, dis + cntd + 1);
	int sum = 0;
	for (int l = 1, r = cntd; ; ++l) {
		while (r && dis[l] + dis[r] > k) --r;
		if (r < l) break;
		sum += r - l;
	}
	return sum;
}
void solve(int u) {
	ans += calc(u, 0); vis[u] = 1;
	for (int i = hd[u]; i; i = e[i].nxt) {
		int v = e[i].to, w = e[i].wei;
		if (vis[v]) continue;
		ans -= calc(v, w);
		mxp[root = 0] = INT_MAX;
		getroot(v, 0, sz[v]); solve(root);
	}
}
int main() {
	n = read();
	for (int i = 1; i < n; ++i) {
		int u = read(), v = read(), w = read();
		link(u, v, w); link(v, u, w);
	}
	k = read();
	mxp[root = 0] = INT_MAX;
	getroot(1, 0, n); solve(root);
	printf("%d\n", ans);
	return 0;
}

Luogu P3806【模板】点分治 1

//写法2实现
#include <bits/stdc++.h>
using namespace std;
inline int read() {
	int x = 0; bool f = 0; char ch = getchar();
	while (!isdigit(ch)) f |= ch == '-', ch = getchar();
	while (isdigit(ch)) x = (x << 1) + (x << 3) + (ch ^ 48), ch = getchar();
	return f ? -x : x;
}
const int MAXN = 1e4 + 6, INF = 1e8 + 6;
int n, m, cnte, cntd, root;
int hd[MAXN], sz[MAXN], mxp[MAXN], vis[MAXN];
int q[MAXN], f[MAXN], dic[INF], dis[MAXN], tf[MAXN];
struct tEdge { int to, nxt, wei; } e[MAXN << 1];
inline void link(int u, int v, int w) {
	e[++cnte] = {v, hd[u], w};
	hd[u] = cnte;
}
void getroot(int u, int p, int n) {
	sz[u] = 1, mxp[u] = 0;
	for (int i = hd[u]; i; i = e[i].nxt) {
		int v = e[i].to;
		if (vis[v] || v == p) continue;
		getroot(v, u, n);
		sz[u] += sz[v];
		mxp[u] = max(mxp[u], sz[v]);
	}
	mxp[u] = max(mxp[u], n - sz[u]);
	if (mxp[u] < mxp[root]) root = u;
}
void getdis(int u, int d, int p) {
	dis[++cntd] = d;
	for (int i = hd[u]; i; i = e[i].nxt) {
		int v = e[i].to, w = e[i].wei;
		if (v == p || vis[v]) continue;
		getdis(v, d + w, u);
	}
}
void calc(int u) {
	int tot = 0;
	for (int i = hd[u]; i; i = e[i].nxt) {
		int v = e[i].to, w = e[i].wei;
		if (vis[v]) continue;
		cntd = 0; getdis(v, w, u);
		for (int j = cntd; j; --j)
			for (int k = 1; k <= m; ++k) {
				if (q[k] < dis[j]) continue;
				f[k] |= dic[q[k] - dis[j]];
			}
		for (int j = cntd; j; --j)
			tf[++tot] = dis[j], dic[dis[j]] = 1;
	}
	for (int i = 1; i <= tot; ++i)
		dic[tf[i]] = 0;
}
void solve(int u) {
	vis[u] = dic[0] = 1; calc(u);
	for (int i = hd[u]; i; i = e[i].nxt) {
		int v = e[i].to;
		if (vis[v]) continue;
		mxp[root = 0] = INT_MAX;
		getroot(v, 0, sz[v]); solve(root);
	}
}
int main() {
	n = read(), m = read();
	for (int i = 1; i < n; ++i) {
		int u = read(), v = read(), d = read();
		link(u, v, d); link(v, u, d);
	}
	for (int i = 1; i <= m; ++i)
		q[i] = read();
	mxp[root = 0] = INT_MAX;
	getroot(1, 0, n); solve(root);
	for (int i = 1; i <= m; ++i) {
		if (f[i]) puts("AYE");
		else puts("NAY");
	}
	return 0;
}

0x04 相关

点分治常见题型有:

  • 路径和等于或小于等于 k k k的点对(路径条数)。
  • 路径和为某个数的倍数。
  • 路径和为 k k k且路径的边数最少。
  • 路径和 m o d M mod M modM后为某个值。
  • 路径上经过不允许点的个数不超过某个值,且路径和最大。
  • ……

若使用写法1,一般开一个栈,保存路径上的距离等相关信息,排序后利用单调性或者二分找答案。

若使用写法2,则一般处理这些问题时,都是开一个桶 m p i mp_i mpi表示距离为i的相关信息。

0x05 总结

个人感觉写法2更好,效率更高、适用面广、调试与思维难度低。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值