树链剖分之长链剖分 详解 题目整理

树链剖分

题目中出现的树链剖分一般分为两种,重链剖分和长链剖分

  • 重链剖分:选择子树最大的儿子, 将其归入当前点所在 的同一条重链
  • 长链剖分:选择向下能达到的深 度最深的儿子,将其归 入当前点所在的同一 条长链

重剖主要用于维护子树信息和链信息长剖主要用于维护子树中只与深度有关的信息

长剖

从根开始对树进行深度优先搜索,同时优先搜索子树深度最深的儿子

优先搜索子树深度最深的儿子 (以下以重儿子表示) 使得 每条长链在 dfs 序上是连续的

  • 树被切分为多条长链
  • 一条长链顶端点的父亲节点所在长链一定长于这条长链,一个点 k 级祖先所在长链一定长于 k
  • 任一点到根最多经过 n \sqrt{n} n 条长链,所有长链 总长度为 O ( n ) O(n) O(n)
  • 能够在线性时间维护 子树中只与深度有关 的信息

k级祖先

重剖

重链剖分,还是根据 O ( l o g n ) O(logn) O(logn)条轻链的性质,如果k级组先就在当前重链上则直接找到,否则往上一条重链跳。复杂度 O ( l o g n ) O(logn) O(logn)

长剖

  1. O ( n l o g n ) O(nlogn) O(nlogn) 求祖先 ST 表
  2. O(n) 求出每个长链顶端 1 到 len(x) 级祖先和重儿子
  3. O(n) 预处理每个数字最高位 1 的位数 b[i]
  4. 对于 k 级祖先,我们先求 ST 表 2b[k] 级祖先
  5. 其长链信息一定大于 k−2b[k],直接 O(1) 查表即可
详解链接

长链剖分优化树上DP

O(n)统计每个点子树中以深度为下标的可合并信息

长链剖分

我们首先要预处理以下内容

  • 节点的深度
  • 节点的重儿子(子树深度最深的儿子)
  • 长链的长度
int deep[maxn], h_size[maxn], son[maxn];
//deep记录深度,h_size记录重链长度,son记录重儿子
void dfs1(int now, int f) {
	deep[now] = deep[f] + 1;
	h_size[now] = deep[now];
	for (int i = head[now]; i; i = edge[i].next) {
		if (f == edge[i].v)continue;
		dfs1(edge[i].v, now);
		h_size[now] = max(h_size[now], h_size[edge[i].v]);
		if (h_size[edge[i].v] > h_size[son[now]])
			son[now] = edge[i].v;		//长度最大的为重儿子
	}
}

树上DP

  • 数组大小
    数组大小只需要开 t m p [ m a x n ] tmp[maxn] tmp[maxn],即为所有长链的点的总数
    状态数组 ∗ s u m [ m a x n ] *sum[maxn] sum[maxn],指向 t m p tmp tmp上,长链所在区间
    ∗ i d *id id用于维护长链区间的移动

  • 数组维护
    s u m [ i ] [ d ] sum[i][d] sum[i][d]表示节点 i i i的深度为 d d d的状态

  • 重链的继承
    s u m [ i ] [ j ] = s u m [ i ] [ j + 1 ] sum[i][j]=sum[i][j+1] sum[i][j]=sum[i][j+1]为某节点的深度x的状态可以直接继承其长链深度为x+1的
    s u m [ i ] = s u m [ j ] + 1 sum[i]=sum[j]+1 sum[i]=sum[j]+1

  • 轻链的合并
    直接将轻链合并到重链即可
    由于每个点只会合并一次,复杂度为O(n)

特别注意,这样实现也有相应的缺点,即若动规的数组高于一维,那么数组是一次性的,不能在完成转移后查询中间过程的值。
原因是部分内存被共用掉了

int* sum[maxn], tmp[maxn], * id = tmp;
//sum为数组指针,数组总大小为maxn(所有长链加起来为n个点),id为指针
void dfs(int now, int f) {
	sum[now][0] = 1;
	if (son[now]) {
		sum[son[now]] = sum[now] + 1;	//继承其长链,sum[fx][i+1]=sum[x][i]
		dfs(son[now], now);
	}
	for (int i = head[now]; i; i = edge[i].next) {
		if (edge[i].v == f || edge[i].v == son[now])continue;
		sum[edge[i].v] = id;		//数组开始点
		id += h_size[edge[i].v] - deep[edge[i].v] + 1;	//开数组长度为长链长度
		dfs(edge[i].v, now);
		for (int j = 1; j <= h_size[edge[i].v] - deep[edge[i].v] + 1; j++) {
			//DP() ,dp方程
		}
	}
	
}

CF1009F Dominant Indices

题意

给定一棵根为 1 的树, 对于每个节点,求子树 中,哪个距离下的节点 数量最多
当数量相同时,取较小 的那个距离值

思路

我们对树进行长链剖分,记录 s u m [ x ] sum[x] sum[x]数组表 示节点x不同距离的点的数量
同一条长链中 s u m [ f x ] [ i + 1 ] = s u m [ x ] [ i ] sum[fx][i+1]=sum[x][i] sum[fx][i+1]=sum[x][i], 直接继承长链,暴力轻 儿子即可

CF570D Tree Requests

题意:

给定一棵树,树的边长均为1,每个节点上标着一个字母
多次询问,每次给出一个v和一个d,要求查询v的子树 中深度为 d 的节点所标字母能否通过合理排列形成回文串

思路:

我们发现能组合成回文串的字母集包含奇数个字母最多只有一种
因此我们对字母集进行状压,1和0分别表示 该字母出现奇数次或者偶数次
a [ x ] [ i ] a[x][i] a[x][i] 表示 x 为子树,距离 x 深度为 i 的字母集
对树进行长链剖分,在同一条长链上有 a [ f x ] [ i + 1 ] = a [ x ] [ i ] a[fx][i+1]=a[x][i] a[fx][i+1]=a[x][i]长链继承
短链对应深度暴力 xor 即可询问需保存在点上,在对应的点统计答案

BZOJ4543 Hotel加强版

题意

求一棵树上三点距离 两两相等的三元组数

思路

f [ i ] [ j ] f[i][j] f[i][j]表示i子树距离 i 为 j 的点数量
g [ i ] [ j ] g[i][j] g[i][j] 表示 i 子树两点 l c a lca lca距离彼此为d,且 该 l c a lca lca距离i点为d-j的 点对数

  • g [ x ] [ j + 1 ] + = f [ x ] [ j + 1 ] ∗ f [ y ] [ j ] g[x][j+1]+=f[x][j+1]*f[y][j] g[x][j+1]+=f[x][j+1]f[y][j]
  • g [ x ] [ j − 1 ] + = g [ y ] [ j ] g[x][j-1]+=g[y][j] g[x][j1]+=g[y][j]
  • f [ x ] [ j + 1 ] + = f [ y ] [ j ] f[x][j+1]+=f[y][j] f[x][j+1]+=f[y][j]
  • a n s = f [ x ] [ j ] ∗ g [ y ] [ j + 1 ] + g [ x ] [ j ] ∗ f [ y ] [ j − 1 ] ans=f[x][j]*g[y][j+1]+g[x][j]*f[y][j-1] ans=f[x][j]g[y][j+1]+g[x][j]f[y][j1]
    我们发现状态转移只跟节点深度有关,因此可以长链剖 分优化
    同一条长链上对于 f 数组有 f [ f x ] [ i + 1 ] = f [ x ] [ i ] f[fx][i+1]=f[x][i] f[fx][i+1]=f[x][i],对于g数组有 g [ f x ] [ i − 1 ] = g [ x ] [ i ] g[fx][i-1]=g[x][i] g[fx][i1]=g[x][i]
    继承重儿子的 g 和 f 函数, 暴力统计轻链,同时计算 ans 即可

例题代码

Dominant Indices
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
#pragma warning (disable:4996)
typedef long long LL;
const int maxn = 1000005;
const int maxm = 1000005;
int n, res[maxn];

int head[maxn], tot;
struct Edge
{
	int v;
	int next;
}edge[maxm << 1];
void init() {
	memset(head, 0, sizeof(head));
	tot = 0;
}
inline void AddEdge(int u, int v) {
	edge[++tot].v = v;
	edge[tot].next = head[u];
	head[u] = tot;
}

int deep[maxn], h_size[maxn], son[maxn];
//deep记录深度,h_size记录重链长度,son记录重儿子
void dfs1(int now, int f) {
	deep[now] = deep[f] + 1;	
	h_size[now] = deep[now];
	for (int i = head[now]; i; i = edge[i].next) {
		if (f == edge[i].v)continue;
		dfs1(edge[i].v, now);
		h_size[now] = max(h_size[now], h_size[edge[i].v]);
		if (h_size[edge[i].v] > h_size[son[now]])
			son[now] = edge[i].v;		//长度最大的为重儿子
	}
}

int* sum[maxn], tmp[maxn], * id = tmp;
//sum为数组指针,数组总大小为maxn(所有长链加起来为n个点),id为指针
void dfs(int now, int f) {
	sum[now][0] = 1;
	if (son[now]) {
		sum[son[now]] = sum[now] + 1;	//继承其长链,sum[fx][i+1]=sum[x][i]
		dfs(son[now], now);
		res[now] = res[son[now]] + 1;	//同上,继承
	}
	for (int i = head[now]; i; i = edge[i].next) {
		if (edge[i].v == f || edge[i].v == son[now])continue;
		sum[edge[i].v] = id;		//数组开始点
		id += h_size[edge[i].v] - deep[edge[i].v] + 1;	//开数组长度为长链长度
		dfs(edge[i].v, now);
		for (int j = 1; j <= h_size[edge[i].v] - deep[edge[i].v] + 1; j++) {
			sum[now][j] += sum[edge[i].v][j - 1];	//对于轻链dp
			if (sum[now][j] > sum[now][res[now]] || sum[now][j] == sum[now][res[now]] && j < res[now])
				res[now] = j;
		}
	}
	if (sum[now][res[now]] == 1)res[now] = 0;	//特判,若为1个,那么0是最小的
}

int main() {
	int n; scanf("%d", &n);
	int u, v;
	for (int i = 1; i < n; i++) {
		scanf("%d%d", &u, &v);
		AddEdge(u, v);
		AddEdge(v, u);
	}
	dfs1(1, 0);	//长链剖分
	sum[1] = id; id += h_size[1];
	dfs(1, 0);		//树上dp
	for (int i = 1; i <= n; i++)
		printf("%d\n", res[i]);
}


Tree Requests
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
#pragma warning (disable:4996)
typedef pair<int, int> pii;
typedef long long LL;
const int maxn = 500005;
const int maxm = 500005;
int n, m, x;
bool res[maxn];

int head[maxn], tot;
struct Edge
{
	int v;
	int next;
}edge[maxm << 1];
void init() {
	memset(head, 0, sizeof(head));
	tot = 0;
}
inline void AddEdge(int u, int v) {
	edge[++tot].v = v;
	edge[tot].next = head[u];
	head[u] = tot;
}

int deep[maxn], h_size[maxn], son[maxn];
//deep记录深度,h_size记录重链长度,son记录重儿子
void dfs1(int now, int f) {
	deep[now] = deep[f] + 1;	
	h_size[now] = deep[now];
	for (int i = head[now]; i; i = edge[i].next) {
		if (f == edge[i].v)continue;
		dfs1(edge[i].v, now);
		h_size[now] = max(h_size[now], h_size[edge[i].v]);
		if (h_size[edge[i].v] > h_size[son[now]])
			son[now] = edge[i].v;		//长度最大的为重儿子
	}
}

int w[maxn];
char s[maxn];
vector<pii> E[maxn];
int* sum[maxn], tmp[maxn], * id = tmp;
void dfs(int now, int f) {
	sum[now][0] = w[now];
	if (son[now]) {
		sum[son[now]] = sum[now] + 1;
		dfs(son[now], now);
	}
	for (int i = head[now]; i; i = edge[i].next) {
		if (edge[i].v == f || edge[i].v == son[now])continue;
		sum[edge[i].v] = id;
		id += h_size[edge[i].v] - deep[edge[i].v] + 1;
		dfs(edge[i].v, now);
		for (int j = 1; j <= h_size[edge[i].v] - deep[edge[i].v] + 1; j++)
			sum[now][j] ^= sum[edge[i].v][j - 1];
	}
	int depth = deep[now], son_depth;
	for (int i = 0; i < E[now].size(); i++) {
		son_depth = E[now][i].first - depth;
		if (son_depth <= 0 || E[now][i].first > h_size[now]) {
			res[E[now][i].second] = true;
			continue;
		}
		if (sum[now][son_depth] == 0) {
			res[E[now][i].second] = true;
			continue;
		}
		int cnt = 0, j;
		for (j = 0; j < 26; j++) {
			if (sum[now][son_depth] & (1 << j))
				cnt++;
			if (cnt == 2)break;
		}
		if (j < 26)res[E[now][i].second] = false;
		else res[E[now][i].second] = true;
	}
}

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 2; i <= n; i++) {
		scanf("%d", &x);
		AddEdge(i, x);
		AddEdge(x, i);
	}
	scanf("%s", s + 1);
	for (int i = 1; i <= n; i++) {
		w[i] = 1 << (s[i] - 'a');
	}
	int d;
	for (int i = 1; i <= m; i++) {
		scanf("%d%d", &x, &d);
		E[x].push_back(pii(d, i));
	}
	dfs1(1, 0);
	sum[1] = id; id += h_size[1];
	dfs(1, 0);
	for (int i = 1; i <= m; i++)
		if (res[i])printf("Yes\n");
		else printf("No\n");

}

Hotel加强版

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
#pragma warning (disable:4996)
typedef pair<int, int> pii;
typedef long long LL;
const int maxn = 100005;
const int maxm = 100005;
int n;

int head[maxn], tot;
struct Edge
{
	int v;
	int next;
}edge[maxm << 1];
void init() {
	memset(head, 0, sizeof(head));
	tot = 0;
}
inline void AddEdge(int u, int v) {
	edge[++tot].v = v;
	edge[tot].next = head[u];
	head[u] = tot;
}

int deep[maxn], h_size[maxn], son[maxn];
//deep记录深度,h_size记录重链长度,son记录重儿子
void dfs1(int now, int f) {
	deep[now] = deep[f] + 1;
	h_size[now] = deep[now];
	for (int i = head[now]; i; i = edge[i].next) {
		if (f == edge[i].v)continue;
		dfs1(edge[i].v, now);
		h_size[now] = max(h_size[now], h_size[edge[i].v]);
		if (h_size[edge[i].v] > h_size[son[now]])
			son[now] = edge[i].v;		//长度最大的为重儿子
	}
}

LL* f[maxn], * g[maxn], tmp[maxn << 2], * id = tmp;
LL ans;
void dfs(int now, int fa) {
	if (son[now]) {
		f[son[now]] = f[now] + 1;
		g[son[now]] = g[now] - 1;
		dfs(son[now], now);
	}
	f[now][0] = 1;
	ans += g[now][0];
	for (int i = head[now]; i; i = edge[i].next) {
		if (edge[i].v == fa || edge[i].v == son[now])continue;
		f[edge[i].v] = id; id += ((h_size[edge[i].v] - deep[edge[i].v]) << 1) + 2;
		g[edge[i].v] = id; id += (h_size[edge[i].v] - deep[edge[i].v]) + 1;
		dfs(edge[i].v, now);
		for (int j = 0; j <= h_size[edge[i].v] - deep[edge[i].v]; j++) {
			if (j)ans += g[edge[i].v][j] * f[now][j - 1];	//长链上距离(j-1)的点数量
			ans += f[edge[i].v][j] * g[now][j + 1];			//短链上的点*长链上的点对
		}
		for (int j = 0; j <= h_size[edge[i].v] - deep[edge[i].v]; j++) {
			g[now][j + 1] += f[edge[i].v][j] * f[now][j + 1];	//短链一个点+长链一个点的合并
			if (j)g[now][j - 1] += g[edge[i].v][j];				//短链点对加入
			f[now][j + 1] += f[edge[i].v][j];
		}
	}	
}

int main() {
	scanf("%d", &n);
	int u, v;
	for (int i = 2; i <= n; i++) {
		scanf("%d%d", &u, &v);
		AddEdge(u, v);
		AddEdge(v, u);
	}
	dfs1(1, 0);
	f[1] = id; id += (h_size[1] << 1) + 2;	//给g数组预留向前移动的空间
	g[1] = id; id += h_size[1] + 1;
	dfs(1, 0);
	printf("%lld\n", ans);
}
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值