浅谈点分治

简介

点分治就是在一棵树上,对具有某些限定条件的路径静态地进行统计的算法。 ---- 摘自《算法竞赛:进阶指南》

参考资料

洛谷日报

b站视频

感谢以上大佬帮助我学会点分治,如果我这篇blog讲得不够详细也可以参考上述资料。

Problem

先来看一道点分治模板题 -> 洛谷题目地址

  • 给定一棵有 \(n\) 个点的无根树,询问树上距离为 \(k\) 的点对是否存在。\(n \le 10^4,k \le 10^7\)

Solution

设这棵树的根为 \(u\),那么所以的路径可以分为两种,一种是经过 \(u\),另外一种是不经过 \(u\)(这种路径就是在 \(u\) 的子树里面)。

例如:

其中红色、蓝色路径就是第一种,绿色路径就是第二种。

进而我们可以发现如果我们以 \(x\) 点为根,那么绿色路径就变为第一种路径了。所以我们关键在于解决第一种情况路径的统计。


在讲解点分治算法之前,我们先来讲述一下如何统计第一种情况的路径:

先对以 \(u\) 为根的树分别对每一个子树进行一遍 \(Dfs\),处理出 \(dist[]\) 数组。并且把所有的路径长度记录下来

建立 \(bool\) 数组 \(judge[]\),其中 \(judge[i]\) 表示前面子树里面是否出现长度为 \(i\) 的路径。每遍历一个子树,我们循环遍历记录下来的每一个路径长度,看一下 \(judge[K-dist]\) 是否为 \(true\) 就好了。


点分治算法就是(初步):

  • \(1.\) 找到一个点为根,统计所有经过这个点的路径(符合限定条件的)。

  • \(2.\) 删除这个点,递归到下一层,重复 \(1,2\)

\(Q:\) 点分治算法就这么简单?这样就没有了??

\(A:\) 确实。但是我们来理解一下黑体字“递归到下一层”。理解了这一步你就应该了解点分治算法了。

如下图:

可以发现递归一层的时间复杂度大概是 \(O(N)\)。理解?


点分治至此还没有结束,因为我们遇上了难题!!!

设递归层数为 \(T\),那么时间复杂度就是 \(O(TN)\),然而时间复杂度总是能那么优美吗??我们来看一下下面这个例子:

求重心的复杂度是 \(O(N)\),和处理一层的复杂度一样,因此不影响总复杂度

事实上,有些大佬对点分治的复杂度提出过质疑-->传送门。由于我也没看懂,但是点分治的复杂度应该是不会高于 \(O(NlogN)\)


最后我们来看一下求重心,点分治你就学会了:

先来看几个概念:

  • \(sum,rt\) :当前这棵树的大小,当前这棵树的根(我们需要求)。

  • \(sz[u]\) :记录当前这棵树上,以u为根的子树大小。

  • \(son[u]\)\(u\) 的重儿子的大小。重儿子:\(sz[]\) 最大的儿子。

重心:重儿子最小的点

所以求重心也很简单了,我们来看一下求重心的代码

void Getrt(int u,int fa) {
	sz[u] = 1, son[u] = 0;
	for(int i=head[u];i;i=edge[i].next) {
		int v = edge[i].to;
		if(v == fa || vis[v]) continue;    //如果 v 已经处理过(删除),也不用继续访问。
		Getrt(v,u);
		sz[u] += sz[v];
		son[u] = max(son[u],sz[v]);
	}
	son[u] = max(son[u],sum-sz[u]);    //因为是无根树,子树外也是这棵树的一个子树。
	if(son[u] < son[rt]) rt = u;       //更新重心。
}

一些细节

我们在处理完一个点后,\(judge[]\) 数组也应该要清空。发现值域是 \(10^7\),不能用 \(memset\)。可以用一个队列把处理这个点的时候,修改过的大小记录起来,然后再把修改过的地方重新置为 \(0\)


最后我们来总结一下点分治的过程:

  • \(1.\) 找到当前树的重心作为根,统计所有经过这个点的路径(符合限定条件的)。

  • \(2.\) 删除这个点,递归到下一层,重复 \(1,2\)

Code

Talk is cheap.Show me the code.

#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
using namespace std;
inline int read() {
	int x=0,f=1; char ch=getchar();
	while(ch<'0' || ch>'9') { if(ch=='-') f=-1; ch=getchar(); }
	while(ch>='0'&&ch<='9') { x=(x<<3)+(x<<1)+(ch^48); ch=getchar(); }
	return x * f;
}
const int N = 1e4+7, M = 107, MAXN = 1e7+7;
int n,m,cnt,sum,rt;
int head[N],K[M],sz[N],son[N],dis[N],mem[N];
bool vis[N],judge[MAXN],ans[M];
struct Edge {
	int next,to,w;
}edge[N<<1];
stack<int> q;
inline void add(int u,int v,int w) {
	edge[++cnt] = (Edge)<%head[u],v,w%>;
	head[u] = cnt;
}
void Getrt(int u,int fa) {
	sz[u] = 1, son[u] = 0;
	for(int i=head[u];i;i=edge[i].next) {
		int v = edge[i].to;
		if(v == fa || vis[v]) continue;
		Getrt(v,u);
		sz[u] += sz[v];
		son[u] = max(son[u],sz[v]);
	}
	son[u] = max(son[u],sum-sz[u]);
	if(son[u] < son[rt]) rt = u;
}
void Getdis(int u,int fa) {
	mem[++mem[0]] = dis[u];
	for(int i=head[u];i;i=edge[i].next) {
		int v = edge[i].to, w = edge[i].w;
		if(v == fa || vis[v]) continue;
		dis[v] = dis[u] + w;
		Getdis(v,u);
	}
}
void Calc(int u) {
	vis[u] = judge[0] = 1;
	for(int i=head[u];i;i=edge[i].next) {
		int v = edge[i].to, w = edge[i].w;
		if(vis[v]) continue; 
		dis[v] = w; Getdis(v,u);
		for(int j=1;j<=mem[0];++j)
			for(int l=1;l<=m;++l) {
				if(K[l] - mem[j] >= 0)
					ans[l] |= judge[K[l] - mem[j]];
			}
		for(int j=1;j<=mem[0];++j) {
			judge[mem[j]] = 1;
			q.push(mem[j]);
		}
		mem[0] = 0;
	}
	while(!q.empty()) {
		judge[q.top()] = 0;
		q.pop();
	}
}
void Divide(int u) {
	Calc(u);
	for(int i=head[u];i;i=edge[i].next) {
		int v = edge[i].to;
		if(vis[v]) continue;
		sum = sz[v]; son[rt = 0] = sz[v];
		Getrt(v,0); Divide(v);
	}
}
int main()
{
	//freopen("test.out","w",stdout);
	n = read(), m = read();
	for(int i=1,u,v,w;i<=n-1;++i) {
		u = read(), v = read(), w = read();
		add(u,v,w), add(v,u,w);
	}
	for(int i=1;i<=m;++i) K[i] = read();
	son[rt] = sum = n;
	Getrt(1,0);
	Divide(rt);
	for(int i=1;i<=m;++i) {
		if(ans[i]) puts("AYE");
		else puts("NAY");
	}
	return 0;
}

题外话:我这份代码在洛谷上只有 \(60pts\),其中有一点 \(RE\),另外一个点 \(TLE\)\(RE\) 可以见洛谷的讨论贴;\(TLE\) 是因为我用的是 \(STL\) 的队列,用手写队列就不会了。这份代码的思路是没有问题的。

题目

一些简单的点分治题目:

P4178 Tree 洛谷题目地址

  • 给定一棵 \(n\) 个节点的树,每条边有边权,求出树上两点距离小于等于 \(k\) 的点对数量\(n \le 4 \times 10^4,k \le 2 \times 10^4\)

做法:

关键在于如何统计第一种情况的路径(以 \(u\) 为根,经过 \(u\) 的小于等于 \(k\) 的路径):假设我们用一个数组 \(d[]\)\(d[i]\) 表示前面的子树中等于 \(i\) 的路径的数量,当前遍历到的路径长度是 \(dist\),那么答案就要加上 \(\sum_{i=0}^{K-dist} d[i]\)。发现这是一个需要支持单点修改,区间查询的数据结构,所以我们可以用树状数组。(由于树状数组不能统计 \(0\) 下标,实际做的时候还要注意一些细节。)

P4149 [IOI2011]Race 洛谷题目地址

  • 给一棵树,每条边有权。求一条简单路径,权值和等于 \(k\),且边的数量最小。\(n \le 10^4,k \le 10^7\)

做法:

用一个数组 \(d[]\)\(d[i]\) 不但表示前面子树中长度为 \(i\) 的路径是否存在,还记录一下前面子树中长度为 \(i\) 的路径最少由几条边组成。

posted @ 2020-04-18 10:24  基地AI  阅读(272)  评论(0编辑  收藏  举报