点分治与点分树

点分治和点分树真的是各种意义上的好东西。不仅好玩,而且写完一看自己的代码5.几kb:“wc我今天搞了好多学习”。


在做关于树的题时,我们会遇到一类题型:题目跟路径有关,你找到两个点当端点求贡献,然后你发现这两个点当形成路径跨过根时,相比没跨过根,求贡献的方法不一样。

详情可参考洛谷模板题:点分治1

给定一棵有 \(n\) 个点的树,询问树上距离为 \(k\) 的点对是否存在。

在这道题中,没跨过根的路径要减去 LCA 的深度,跨过根的路径只要把深度一加就好了。后者显然比前者简单许多。

鲁迅说过:“偷懒是人类第一生产力”。在面对即将进行的宏大的分类讨论时,我们往往会选择回避掉这个问题。例如:换一个根,把原来与根的关系破坏掉。

很赞的一点是由于路径换根后不变,所以换一个根实际上不影响答案。

我们确定了算法的目的:我们要让每条路径都有一个“根”供其跨越与计算答案。那么我们试着写出算法的步骤

1:找到一个根

2:统计跨过根的路径的答案

3:递归处理哪些路径没跨过根

4:找到一个新根,回到第二步


算法的雏形有了,我们还需要处理几个问题:

1:到底哪些路径没跨过根?

两端点 LCA 等于根的路径都跨过了根,剩下的东西都是没跨过根的。画个图就知道他们的两端点属于根的同一子树。

这也意味着我们在处理完根以后,我们要处理的是根的每一个子树,并在每一个子树中选出一个新根。

2:选出新根后要遍历哪些节点?

根据问题 1 可以得知:只需要遍历老根的这个子树的节点就行了,如果你选一个子树内的点,选一个子树外的点组成路径,那么就跨过了根。如果你选了两个子树外的点且他们属于同一子树,那么就是其他新根的任务了。

发现这一套:选中间点→处理能处理的问题→划分子结构→选中间点 的步骤就是分治。这也是算法名字的由来。

3:这算法时间复杂度行不行?

我们发现一件事情:划分出的子树大小越小,算法所需时间越少。例如下图:

以 1 为根的划分,需要的划分次数比以 2 为根要少。因为划分时留下的没跨过根节点的路径越少。

怎么让划分出的子树大小最小?这是个经典且简单的问题,答案就是选重心。

以重心为根,每个子树的大小每次变为原来的 1/2。总共最多划分 \(log(n)\) 层,子树最多的一层最多有 \(n\) 个子树(每棵子树大小为 1)。总复杂度为 O(nlogn)。实际上由于我们还要对每个子树求重心,所以为 \(O(nlog^2n)\)
4:怎么统计跨过根的答案?

一般我们三种不同的方法:

第一种做法:我们可以先不管子树的性质,只管满足条件,然后再单独统计这个子树内部与内部的答案,容斥一下就好了。

第二种做法:给每个子树染个不同的颜色,统计颜色不同的答案。

第三种做法:在遍历子树时,我们先遍历,再求答案,再更新,这样可以保证每棵子树只会与前面的子树求答案。

以上题为例:

这道题说白了我们只要找出一条满足条件的路径就行,最简单的办法就是染色后把当前根管的点按深度排个序,让两个指针一个指头一个指尾,不断向中间靠近,直到两指针对应的深度之和等于 \(k\) 。如果此时颜色相同还要继续跳(优先选择跳完后深度不变的那一边)。

for(int i = 1;i <= m;i ++)
{
	int l = 1, r = tot;
	if(pd[i])
		continue;
	while(l < r)
	{
		if(dep[h[l]] + dep[h[r]] > que[i])
			r --;
		else if(dep[h[l]] + dep[h[r]] < que[i])
			l ++;
		else if(col[h[l]] == col[h[r]])
		{ 
			if(dep[h[r]] == dep[h[r - 1]])
				r --;
			else
				l ++;
		}
		else
		{
			pd[i] = 1;
			break;
		}
	}
}

处理完这四个问题,点分治的算法过程已经完成了。时间复杂度为 \(O(nlog^2n+nmlogn)\)。其中前面为分治复杂度,后面为处理询问复杂度。

推荐习题:

点分治1

IOI2011 race

聪聪可可

永恒


现在我们对于树上路径问题能够使用点分治解决了,但我们发现如果带上修改我们就得每次修改后再做一次点分治,复杂度就上去了。

例如模板点分树

在一片土地上有 \(n\) 个城市,通过 \(n-1\) 条无向边互相连接,形成一棵树的结构,相邻两个城市的距离为 \(1\),其中第 \(i\) 个城市的价值为 \(value_i\)

不幸的是,这片土地常常发生地震,并且随着时代的发展,城市的价值也往往会发生变动。

接下来你需要在线处理 \(m\) 次操作:

0 x k 表示发生了一次地震,震中城市为 \(x\),影响范围为 \(k\),所有与 \(x\) 距离不超过 \(k\) 的城市都将受到影响,该次地震造成的经济损失为所有受影响城市的价值和。

1 x y 表示第 \(x\) 个城市的价值变成了 \(y\)

为了体现程序的在线性,操作中的 \(x\)\(y\)\(k\) 都需要异或你程序上一次的输出来解密,如果之前没有输出,则默认上一次的输出为 \(0\)

虽然每次的值会更改,但我们点分治经过的点的顺序是不变的,因为树的形态不变。那么我们是不是可以提前把点分治要走的点存好,然后在上面修改呢?

如果我们把根同每个子树的重心连接起来,就会发现最终这也形成了一棵树。

变为了:

这个树跟原树有联系,每个点的子树就是点分治时以这个点为根管的节点。但好像不是那么有联系。我们甚至不能看出两个点之间的路径关系。

这个树有个可爱的性质,这是根据点分治的递归关系建出来的,点分治最多递归 \(logn\) 次,这个树最高就是 \(logn\) 层。如果我们把这棵树所有子树大小相加,也只是 \(nlogn\) 级别的。

最后一句话如果不理解,可以算算贡献,每一个点的贡献就是她的深度,最多 \(logn\),所以 \(n\) 个点就是 \(nlogn\)

这样的话,一些常规可能卡空间的做法:例如每个子树动态开点建线段树,也变得可行了起来。

继续观察,根据点分治的过程,这棵树的不同子树在原树中也属于不同子树。那么我们就找到了一个中转点:这个树上的两点 \(u,v\) 的 LCA 必定在原树从 \(u\)\(v\) 的路径上。因为 \(u\)\(v\) 在原树上属于 LCA 的不同子树。

总结一下两条性质:

  1. 点分树最高 \(logn\) 层。如果我们把树所有子树大小相加,总和为 \(nlogn\) 级别。
  2. 这个树上的两点 \(u,v\) 的 LCA 必定在原树从 \(u\)\(v\) 的路径上。

为了表示方便,我们设点分树上 \(u\)\(v\) 的 LCA 为 \(l\),原树上为 \(lca\)

根据性质 2,我们可以把两点的距离 \(dis(u, v)\) 变为 \(dis(u,l)+dis(v,l)\)

询问为我们固定了节点 \(u\) 与距离 \(k\),我们只需求出对于 \(u\)\(u\) 的祖先 \(z\)\(dis = k - dis(u,z)\) 的点点权之和为多少就行了。这个可以用动态开点线段树解决,将距离作为下标,将价值作为权值即可。

那么对于询问的节点 \(u\) ,我们统计她自身子树中的答案,然后暴力一个个往上跳,依次统计即可。

等等,好像有问题。在跳到父亲 \(fa\) 后, \(u\) 及其子树有可能会被算到第二遍。要减去才行。

怎么减呢?我们要的是减去 \(u\) 子树中与 \(fa\) 距离 \(k - dis(u, fa)\) 的点的点权,很 naive 的想法是减去 \(u\) 子树中与 \(u\) 距离 \(k - dis(u,fa) - 1\) 的点的点权。点分树上的距离和原树没有一点关系,所以这个 \(k - dis(u, fa) - 1\) 中的 \(1\) 意义不明。

所以我们忽略点分树中的距离关系,直接用另一棵动态开点线段树求解。

再次强调点分树上的距离没有任何意义,所以如果祖先的距离超过了 \(k\) 只要忽略当前祖先,上面的祖先还是要进行统计的。

修改也是如此,我们修改完 \(u\) 本身后,暴力往上跳,依次进行修改即可。

#include <bits/stdc++.h>
#define ll long long
#define For(i,a,b) for( int i=(a); i<=(b); ++i)
#define Rep(i,a,b) for( int i=(a); i>=(b); --i)
using namespace std;
void init()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
}
const int maxn = 100105;
int n, m;
int a[maxn];
int root;
vector<int> e[maxn], t[maxn];
bool vis[maxn];
int rt, mx[maxn], si[maxn], trTot, dfn[maxn], dep[maxn], num[maxn << 1];
int st[maxn << 1][23], f[maxn];
// find LCA
void findDeep(int u, int fa)
{
	dfn[u] = ++ trTot;
	num[trTot] = u;
	for(auto v : e[u])
	{
		if(v == fa) continue;
		f[v] = f[u];
		dep[v] = dep[u] + 1;
		findDeep(v, u);
		num[++ trTot] = u;
	}
}
void ST()
{
	memset(st, 0x3f, sizeof(st));
	for(int i = 1;i <= trTot;i ++)
		st[i][0] = num[i];
	for(int j = 1;j <= 20;j ++)
		for(int i = 1;i + (1 << j) <= trTot;i ++)
		{
			if(dep[st[i][j - 1]] < dep[st[i + (1 << (j - 1))][j - 1]])
				st[i][j] = st[i][j - 1];
			else
				st[i][j] = st[i + (1 << (j - 1))][j - 1];
		}
}
int LCA(int u, int v)
{
	if(dfn[u] > dfn[v])
		swap(u,v);
	int l1 = dfn[u],l2 = dfn[v];
	int len=log2(l2 - l1 + 1);
	if(dep[st[l1][len]] < dep[st[l2-(1 << len) + 1][len]])
		return st[l1][len];
	else 
		return st[l2 - (1 << len) + 1][len];
}
int dis(int u, int v)
{
	return dep[u] + dep[v] - (2 * dep[LCA(u, v)]);
}
// find zhong
int Tot;
void findRt(int u, int fa)
{
	mx[u] = 0;
	si[u] = 1;
	for(auto v : e[u])
	{
		if(vis[v] || v == fa) 
			continue;
		findRt(v, u);
		si[u] += si[v];
		if(si[v] > mx[u]) mx[u] = si[v];
	}
	mx[u] = max(mx[u], Tot - si[u]);
	if(mx[rt] > mx[u]) rt = u;
}
void build(int u)
{
	vis[u] = 1;
	for(auto v : e[u])
	{
		if(vis[v] == 1) 
			continue;
		rt = 0;
		Tot = si[v];
		findRt(v, u);
		t[u].push_back(rt);
		t[rt].push_back(u);
		build(rt);
	}
}
void read()
{
	cin >> n >> m;
	int u, v;
	for(int i = 1;i <= n;i ++) cin >> a[i];
	For(i, 1, n - 1)
	{
		cin >> u >> v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
}
struct linetree
{
	int tot, rt[maxn];
	struct point
	{
		int val, ls, rs;
	}tr[5000006];
	void push_up(int u)
	{
		tr[u].val = tr[tr[u].ls].val + tr[tr[u].rs].val;
	}
	void ins(int &p, int l, int r, int fl, int k)
	{
		if(l > fl || r < fl) 
			return ;
		if(p == 0) 
			p = ++ tot;
		if(l == r) 
		{
			tr[p].val += k;
			return ;
		}
		int mid = l + r >> 1;
		ins(tr[p].ls, l, mid, fl, k);
		ins(tr[p].rs, mid + 1, r, fl, k);
		push_up(p);
	}
	int query(int p, int l, int r, int fl, int fr)
	{
		if(l > fr || r < fl || p == 0) 
			return 0;
		if(l >= fl && r <= fr) 
			return tr[p].val;
		int mid = l + r >> 1, ans = 0;
		ans += query(tr[p].ls, l, mid, fl, fr);
		ans += query(tr[p].rs, mid + 1, r, fl, fr);
		return ans;
	}
}zi, fu;//zi 以距离当前点的距离为下标,fu 以距离当前点父亲的距离为下标
void Build()
{
	rt = 0;
	mx[0] = 2147483647;
	findDeep(1, -1);
	ST();
	Tot = n;
	findRt(1, -1);
	root = rt;
	build(rt);
}
int fa[maxn];
void dfs(int u, int f)
{
	for(auto v : t[u])
	{
		if(v == f) continue;
		fa[v] = u;
		dfs(v, u);
	}
}
void change(int u, int k, int r)
{
	if(u == 0) return ;
	zi.ins(zi.rt[u], 0, n, dis(r, u), k);
	if(fa[u]) 
		fu.ins(fu.rt[u], 0, n, dis(r, fa[u]), k);
	change(fa[u], k, r);
}
void chu()
{
	for(int i = 1;i <= n;i ++)
		change(i, a[i], i);
}
int jump(int u, int fr, int k, int from)
{
	if(u == 0) return 0;
	if(dis(u, fr) > k) return jump(fa[u], fr, k, u);
	int nope = (from == 0 ? 0 : fu.query(fu.rt[from], 0, n, 0, k - dis(u, fr)));
	return zi.query(zi.rt[u], 0, n, 0, k - dis(u, fr)) + jump(fa[u], fr, k, u) - nope;
}
void que()
{
	ll q, x, k, ans = 0;
	For(_, 1, m)
	{
		cin >> q >> x >> k;
		x ^= ans;k ^= ans;
		if(q == 0)
		{
			ans = jump(x, x, k, 0);
			cout << ans << endl;
		}
		else
		{
			change(x, k-a[x], x);
			a[x] = k;
		}
	}
}
int main()
{
	init();
	read();
	Build();
	dfs(root, -1);
	chu();
	que();
	return 0;
}

推荐习题:

洛谷上有题单。

posted @ 2023-01-07 10:04  _maze  阅读(68)  评论(0编辑  收藏  举报