点分治

点分治

简介

点分治,是用于树上的统计路径各种信息的算法

它的思想从名字就能看出来,是分治的思想


流程

具体的,每条路径一定有一个 lca

那么我们想把路径按 lca 分类处理,这样两段路径就可以拼接

有时也不能说是拼接,可能会用各种数据结构来辅助统计

统计时注意从前往后处理每棵子树,先与前面的拼接,再把当前子树合并到总共的信息中,这样就不重不漏

那么当前 lca 即为分治中心,把它作为根

剩下的路径,只需把当前的分治中心从树上删除,那么树就分为了几棵小的树,形成若干子问题

可以发现,剩下的路径均不会跨过小树,此时递归进入小树中,重复上述过程统计


实现

每层中由于加起来遍历一遍全树,复杂度为 \(O(n/n\log n)\)

这里分治中心取哪个点呢?

肯定不能随便取,否则在一条链的情况下会退化为 \(O(n^2)\)

这里就想让分出的小树尽量均衡——取树的重心

根据重心的定义,每层小树的 \(\max size\) 大小至少减半(否则调整一下,\(\max size\) 肯定更小,与重心矛盾)

这样总共只有 \(\log n\) 层,复杂度为 \(O(n\log n/n\log^2n)\)

P3806 【模板】点分治1

这题统计有没有长为 \(k\) 的路径

把询问离线,由于不好离散化可以开个 \(set\) 记录有哪些路径的长度,然后采用上面的方法,枚举当前子树中的路径长度,看看前面有没有能对应上的

inline void dfsroot(int x, int fa)
{
	siz[x] = 1;	int res = 0;
	for(reg pii y : edge[x])
	{
		if(y.fi == fa || vis[y.fi])	continue; // 注意,如果一个点被删除了一定要 continue!
		dfsroot(y.fi, x), siz[x] += siz[y.fi];
		res = max(res, siz[y.fi]);
	}
	res = max(res, tot - siz[x]);
	if(res < mn)	root = x, mn = res;
}
inline void getroot(int x)
{
	root = 0, mn = inf, dfsroot(x, x);
}
inline void dfs(int x, int fa, int dist) // 进入子树中,找路径长度
{
	siz[x] = 1, disb.insert(dist); 
	for(reg pii y : edge[x])
	{
		if(y.fi == fa || vis[y.fi])	continue;
		dfs(y.fi, x, dist + y.se);
		siz[x] += siz[y.fi];
	}
}
inline void solve(int x)
{
	disa.clear(), disa.insert(0), vis[x] = 1;
	for(reg pii y : edge[x])
	{
		if(vis[y.fi])	continue;
		disb.clear(), dfs(y.fi, y.fi, y.se); // 注意初始加上到根的距离
		for(iter i = disb.begin(); i != disb.end(); ++i)
			for(reg int j = ans._Find_first(); j <= m; j = ans._Find_next(j))	
				if(disa.find(q[j] - (*i)) != disa.end())	ans[j] = 0; // 找到了
		disa.insert(disb.begin(), disb.end()); // set 可以很方便的合并
	}
	for(reg pii y : edge[x])
	{
		if(vis[y.fi])	continue; // 重新找子树的重心,递归
		tot = siz[y.fi], getroot(y.fi), solve(root);
	}
}

例题

1. P4178 Tree

上一题的变式,要找长度 \(\le k\) 的路径条数

\(k\) 不大,可以开个桶存长度 \(=x(x\le k)\) 的路径数量,当前枚举到长度为 \(y\) 的路径时,用前缀和求长度 \(\le k-y\) 的路径数量,再把 \(y\) 对应位置 \(+1\)

动态的求前缀和?树状数组!

注意长度可能为 \(0\),下标整体 \(+1\) 即可

inline void dfs(int x, int fa, int dist)
{
	siz[x] = 1;
	if(dist <= k)	lsh.pb(dist);
	for(reg pii y : edge[x])
	{
		if(y.fi == fa || vis[y.fi])	continue;
		dfs(y.fi, x, dist + y.se), siz[x] += siz[y.fi];
	}
}
inline void solve(int x)
{
	memset(tree, 0, sizeof(tree)), add(1, 1), vis[x] = 1;
	for(reg pii y : edge[x])
	{
		if(vis[y.fi])	continue;
		lsh.clear(), dfs(y.fi, y.fi, y.se);
		for(reg int i : lsh)	ans += query(k + 1 - i);
		for(reg int i : lsh)	add(i + 1, 1);
	}
	for(reg pii y : edge[x])
	{
		if(vis[y.fi])	continue;
		tot = siz[y.fi], getroot(y.fi), solve(root);
	}
}

2. P4149 [IOI2011]Race

这里在模板的基础上要求边数最小

沿用上题思路,\(k\) 不大,可以开桶记录长为 \(x\) 的路径中的边数最小值

枚举当前路径长度,直接用桶里的值更新答案和用当前边数更新桶

时间复杂度 \(O(n\log n)\)

思路二:

沿用模板的实现思路,我们还是开 \(set\),但存第一关键字为路径长,第二关键字为边数,\(set\) 内从小到大排序

查找 \(\le (k-x,0)\) 的最小 \(pair\),判断长度是否 \(=k-x\),如果是,则找到了长度为 \(k\) 路径,此时 \(k-x\) 的边数最小,也符合

时间复杂度 \(O(n\log^2n)\)

inline void dfs(ll x, ll fa, ll dist, ll depth)
{
	siz[x] = 1, disb.insert(mp(dist, depth));
	for(reg pll y : edge[x])
	{
		if(y.fi == fa || vis[y.fi])	continue;
		dfs(y.fi, x, dist + y.se, depth + 1), siz[x] += siz[y.fi];
	}
}
inline void solve(ll x)
{
	disa.clear(), disa.insert(mp(0, 0)), vis[x] = 1;
	for(reg pll y : edge[x])
	{
		if(vis[y.fi])	continue;
		disb.clear(), dfs(y.fi, y.fi, y.se, 1);
		for(iter it = disb.begin(); it != disb.end(); ++it)
		{
			if(it -> fi > k)	break;
			iter p = disa.lower_bound(mp(k - (it -> fi), 0));
			if(p != disa.end() && (p -> fi) == k - (it -> fi))	ans = min(ans, (p -> se) + (it -> se));
		}
		disa.insert(disb.begin(), disb.end());
	}
	for(reg pll y : edge[x])
	{
		if(vis[y.fi])	continue;
		tot = siz[y.fi], getroot(y.fi), solve(root);
	}
}

3. P2664 树上游戏

进阶题,隐蔽一点点的点分治

颜色数量没有可加性,不好把两条路径拼接

换个思路,计算每种颜色对答案的贡献

用正常的点分治,我们可以求出当前的树中,分治中心为一端点的答案

\(dfs\) 过程中,进入点时把它的颜色层数 \(+1\),退出时 \(-1\),那么某种颜色第一次出现时,对根为端点的路径的贡献即为这个点的子树大小,把这个贡献计入对应颜色的总贡献的桶内,同时更新总答案

但是每个点的路径除了另一端点在当前树内,还可以在树外,怎么统计呢?

这里就要在每次计算时不仅算分治中心的,还要统计树内其它点

考虑一个点到当前分治中心的路径,设这个点在分治中心 \(x\) 的子节点 \(d\) 的子树中

  • 若某个颜色在路径上出现过,则它对这个点的贡献为 \(siz_x-siz_d\)

  • 若某个颜色在这条路径上没出现过,则可以看作是对根的贡献去掉 \(d\) 子树的,统计时清空 \(d\) 子树内贡献

这样做,对路径贡献的统计相当于点分治时统计了所有以分治中心为 \(lca\) 的路径,就可以不重不漏的计算答案

实现细节会多一些,注意清空已经顺序!

inline void clear(ll x, ll fa) // 清空
{
	++book[a[x]];
	for(reg ll y : edge[x])
		if(y != fa && !vis[y])	clear(y, x);
	--book[a[x]];
	if(!book[a[x]])	cnt[a[x]] -= siz[x], ans -= siz[x];
}
inline void dfs(ll x, ll fa)
{
	siz[x] = 1, ++book[a[x]];
	for(reg ll y : edge[x])
	{
		if(y == fa || vis[y])	continue;
		dfs(y, x), siz[x] += siz[y];
	}
	--book[a[x]];
	if(!book[a[x]])	cnt[a[x]] += siz[x], ans += siz[x];
}
inline void dfs2(ll x, ll fa, ll fr, ll rt) // 统计答案
{
	if(!book[a[x]])	ans -= cnt[a[x]], ++num;
	++book[a[x]], sum[x] += ans + num * (siz[rt] - siz[fr]); // 注意单独算已经出现过的颜色
	for(reg ll y : edge[x])
	{
		if(y == fa || vis[y])	continue;
		dfs2(y, x, fr, rt);
	}
	--book[a[x]];
	if(!book[a[x]])	ans += cnt[a[x]], --num;
}
inline void solve(ll x)
{
	vis[x] = 1, book[a[x]] = siz[x] = 1, ans = 0;
	for(reg ll y : edge[x])
	{
		if(vis[y])	continue;
		dfs(y, y), siz[x] += siz[y];
	}
	sum[x] += ans + siz[x]; // 以 x 为端点路径
	for(reg ll y : edge[x])
	{
		if(vis[y])	continue;
		clear(y, y); // 先去掉当前子树贡献
		num = 1, dfs2(y, y, y, x);
		dfs(y, y); // 记得加回来
	}
	for(reg ll y : edge[x])
		if(!vis[y])	clear(y, y); // 记得清空
	book[a[x]] = siz[x] = 0; // 一定写在清空下方!不然清空  a[x] 颜色会出问题!
	for(reg ll y : edge[x])
	{
		if(vis[y])	continue;
		tot = siz[y], getroot(y), solve(root);
	}
}
posted @ 2023-04-09 22:55  KellyWLJ  阅读(4)  评论(0编辑  收藏  举报  来源