长链剖分

1 :长链剖分的构造

推荐先行学习重链剖分

与重链剖分类似,我们设一个节点中深度最深的子节点为长节点,设该节点到长节点的边为重边,其他边为轻边

然后我们把首尾相连的重边组成长链,落单的一个节点也被视作一条长链

我们就把树分成了若干条互不相交的长链

2:长链剖分的性质

  1. 树上所有长链长度和为n
  • 互不相交,很好理解
  1. 节点x的k级祖先y所在长链长度必定大于等于k
  • 如果小于k,节点x到节点y的长度比长链大,不符合长链定义
  1. 从节点x沿长链跳到根节点,跳跃次数最多为 O(n)
  • 每跳一次长链,调到的长链次数会越来越大。最坏情况为1,2,...,n

3:树上k级祖先

如题,求x节点的树上k级祖先

显然可以倍增,也可以树剖+LCA。但用长链剖分可以做到询问O(1)

我们预处理出每一个节点的2k级祖先(倍增), 再处理出每一个长链顶端d的len[d]级儿子和len[d]级祖先(len[d]为以d为顶端的长链长度)

对于每一个询问,我们首先找出一个数t,使2t<k<2t+1,然后把x往上跳2t。我们的目标就变为了求现在x的k2t级祖先

根据性质2,原来x的2t级祖先y(也就是现在的x)所在长链长度必定大于等于2t

我们又能知道k2t肯定小于2t。因此k2t小于当前x所在链的长度

因此把x跳到当前链的顶端,就可以用预处理出的数组O(1)解决

洛谷模板

这道题输出记得要开long long,不然就会像某个憨批一样调半个小时:)

#include <bits/stdc++.h>
using namespace std;

const int maxn = 5e5 + 5;

int n, q;
unsigned s;
unsigned get(unsigned x)
{
	x ^= x << 13, x ^= x >> 17, x ^= x << 5;
	return s = x;
}

int st[maxn], nx[maxn << 1], to[maxn << 1], tot;
void add(int u, int v)
{
	nx[++tot] = st[u];
	st[u] = tot;
	to[tot] = v;
}

int dep[maxn], mx[maxn], son[maxn], f[maxn], dp[maxn][23];
void dfs1(int u, int fa)
{
	mx[u] = dep[u] = dep[fa] + 1;
	dp[u][0] = f[u] = fa;
	for (int i = 1; i <= 22; i++)
		dp[u][i] = dp[dp[u][i - 1]][i - 1];
	for (int i = st[u]; i; i = nx[i])
	{
		int v = to[i];
		if (v == fa)
			continue;
		dfs1(v, u);
		if (mx[v] > mx[u])
			son[u] = v, mx[u] = mx[v];
	}
}
int top[maxn], num[maxn], cnt, len[maxn];
void dfs2(int u, int topu)
{
	top[u] = topu;
	num[u] = ++cnt;
	len[u] = mx[u] - dep[topu];
	if (son[u])
		dfs2(son[u], topu);
	for (int i = st[u]; i; i = nx[i])
	{
		int v = to[i];
		if (v == f[u] || v == son[u])
			continue;
		dfs2(v, v);
	}
}
//以上为长链剖分的构造
vector<int> df[maxn];
vector<int> ds[maxn];
int query(int x, int d)
{
	if (d == 0)
		return x;
	int i = log(d) / log(2);
	int u = x;
	u = dp[u][i];
	d -= (1 << i);
	d -= (dep[u] - dep[top[u]]);
	u = top[u];
	if (d > 0)
		return df[u][d];
	else
		return ds[u][-d];
}
int main()
{
	scanf("%d%d%u", &n, &q, &s);
	int f, rt;
	for (int i = 1; i <= n; i++)
	{
		scanf("%d", &f);
		if (f == 0)
			rt = i;
		else
			add(f, i), add(i, f);
	}
	for (int i = 0; i <= 22; i++)
		dp[rt][i] = rt;
	dfs1(rt, rt);
	dfs2(rt, rt);
	for (int i = 1; i <= n; i++)
	{
		if (top[i] != i)
			continue;
		int u = i; //预处理顶点数据
		for (int j = 0; j <= len[i]; j++)
		{
			df[i].push_back(u);
			u = dp[u][0];
		}
		u = i;
		for (int j = 0; j <= len[i]; j++)
		{
			ds[i].push_back(u);
			u = son[u];
			// cout << ds[i][j] << ' ';
		}
		// cout << endl;
	}
	// cout << endl;
	long long x, k, res = 0, ans = 0;
	for (int i = 1; i <= q; i++)
	{
		x = (get(s) ^ res) % n + 1;
		k = (get(s) ^ res) % dep[x];
		res = query(x, k);
		// cout << x << ' ' << k << endl;
		// cout << res << endl;
		ans ^= i * res;
		//
	}
	printf("%lld\n", ans);
	return 0;
}

4:用长链剖分优化dp

例题

题目大意:给定一棵以 1 为根,n 个节点的树。设 d(u,x)d(u,x) 为 u 子树中到 u 距离为 x 的节点数。对于每个点,求一个最小的 k,使得 d(u,k) 最大。

实际上就是把题面复制了一遍

暴力的dp很容易想,dp[i][j]为i子树中距离为j的节点数,转移dp[i][j]时将i的所有儿子的dp[son][j - 1]加到一起就行了。但这样是O(n2)

发现dp的转移只与深度有关,考虑用长链剖分优化

dfs到节点x时,我们先dfs x的长儿子,然后直接继承长儿子的dp数组

注意长儿子的深度比x多1,因此继承时要错开一位

然后dfs其他儿子,暴力dp更新

由于每一条链只会在链顶暴力dp一次,所以理论复杂度为O(n)

但可以想象,这么做常数巨大,有时可以比肩启发式合并

接下来稍微讲一下如何继承长儿子dp数组

显然直接暴力搞空间会炸

有一种方法是开vector,这种方法在有些题里很好用,但在另外的题里细节爆炸

这里主要讲述用指针的方法

容易看出,如果我们先遍历长儿子,那么每一条长链DFS序是连续的。因此我们可以在每一个链的顶端动态申请空间。就像这样:

int *dp[maxn], tmp[maxn], *id = tmp;
dp[y] = id;
id += len[y];

(每一次申请的空间要能容纳这一条长链。因此每次要申请链的长度这么大的空间)
空间会炸的问题解决了。由于dp数组被我们搞成了一堆指针,所以重儿子的继承只需要这样就行:

dp[son[u]] = dp[u] + 1;

这样u节点的重儿子的dp值会直接错位存入u节点中。

完整代码如下;

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const ll maxn = 1e6 + 5;
ll n, x, y;
ll tot, st[maxn], to[maxn << 1], nx[maxn << 1];
void add(ll x, ll y)
{
	to[++tot] = y;
	nx[tot] = st[x];
	st[x] = tot;
}
ll top[maxn], dep[maxn], mx[maxn], len[maxn], son[maxn], f[maxn];
void dfs1(ll u, ll fa)
{
	f[u] = fa;
	mx[u] = dep[u] = dep[fa] + 1;
	for (ll i = st[u]; i; i = nx[i])
	{
		ll v = to[i];
		if (v == fa)
			continue;
		dfs1(v, u);
		if (mx[v] > mx[u])
			son[u] = v, mx[u] = mx[v];
	}
}
ll ans[maxn], *dp[maxn], tmp[maxn], *id = tmp;
void dfs2(ll u, ll topu)
{
	top[u] = topu;
	len[u] = mx[u] - dep[topu] + 1;
	dp[u][0] = 1;
	if (son[u])
	{
		dp[son[u]] = dp[u] + 1;
		dfs2(son[u], u);
		ans[u] = ans[son[u]] + 1;
	}
	for (ll i = st[u]; i; i = nx[i])
	{
		ll y = to[i];
		if (y == f[u] || y == son[u])
			continue;
		dp[y] = id;
		id += len[y];
		dfs2(y, u);
		for (ll j = 1; j <= len[y]; j++)
		{
			dp[u][j] += dp[y][j - 1];
			if (dp[u][j] > dp[u][ans[u]] || (dp[u][j] == dp[u][ans[u]] && j < ans[u]))
				ans[u] = j;
		}
	}
	if (dp[u][ans[u]] == 1)
		ans[u] = 0;
}
int main()
{
	scanf("%lld", &n);
	for (ll i = 1; i < n; i++)
	{
		scanf("%lld%lld", &x, &y);
		add(x, y), add(y, x);
	}
	dfs1(1, 0);
	dp[1] = id;
	id += len[1];
	dfs2(1, 0);
	for (ll i = 1; i <= n; i++)
		printf("%lld\n", ans[i]);
	return 0;
}

容易看出,如果维护的dp值只与深度有关,我们就可以试着用长链剖分优化

(这里原本还有一道题目,但被咕掉了。)

5:用长链剖分优化贪心

这一部分很神仙,题好像也比较少。如果有好题推荐请洛谷私信~

所以只放了一道例题:给定一棵树,每个点有点权,选定k个叶子,满足根到k个叶子的所有路径所覆盖的点权和最大。每个节点的权值只被计算一次

(题目来源:BZOJ3252 攻略)

考虑一个贪心:每次选择一条权值和最大的路径,统计后将路径上所有点权清零

这个贪心正确,但似乎不太好做

忽然我们发现:长链剖分是可以统计路径长度的。只要把深度改为点权和就行了

长链剖分有一个显而易见的性质:每一条长链总是会到一个叶子节点。如果不到,这条链就不是最长链了。

所以我们要做的就是选k条权值和最长的链相加就行了。
代码很简单,就不放了

6:结尾

虽然前面提到了树上启发式合并,但作者对启发式合并并不熟悉

所以有可能会再开一篇博客?但作者是个鸽子,咕了也很正常。。。

posted @   _maze  阅读(434)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
点击右上角即可分享
微信分享提示