长链剖分学习笔记

什么是长链剖分?

轻重链剖分就是选择子树最大的儿子与当前点在同一条重链里,而长链剖分就是选择向下能达到的深度最深的儿子(也就是到叶子的链长度最长的儿子)与其在同一条长链中。

从任何一个点往上跳到根,最多经过 n \sqrt{n} n 条不同的长链。

灵魂画手重出江湖

根据长链的性质,这条长链链顶的父亲所在的长链,一定不会比它短,然后如图,最坏情况每条长链长度 n \sqrt{n} n ,一共 n \sqrt{n} n 条。

应用

1.求k次祖先

问题:给定一棵树,每次询问一共点的 k k k次祖先。

我会倍增! O ( n log ⁡ n ) + O ( Q log ⁡ n ) O(n \log n)+O(Q \log n) O(nlogn)+O(Qlogn)

而长链剖分可以做到 O ( n log ⁡ n ) + O ( Q ) O(n \log n)+O(Q) O(nlogn)+O(Q)

首先,因为长链剖分中每个点是在它往下延伸的最长链中,所以一个点的 k k k次祖先所在的长链长度一定大于等于 k k k

在每条长链的链顶,记录下整条链,和它的链长那么多次的祖先。这个记录是 O ( n ) O(n) O(n)的。

假如我们先跳到 x x x的第 r r r次祖先 y y y,并保证 2 r ≥ k 2r \geq k 2rk,那么 y y y的第 k − r k-r kr次祖先一定已经被记录在了它所在长链的链顶。

合理设 r r r的取值的话, r r r就设为 k k k在二进制下的最高位。于是只需要预处理每个 x x x的第 2 i 2^i 2i次祖先,就可以做到 O ( n log ⁡ n ) O(n \log n) O(nlogn)预处理, O ( 1 ) O(1) O(1)询问了。

题目链接

#include<bits/stdc++.h>
using namespace std;
#define RI register int
int read() {
	int q=0;char ch=' ';
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
	return q;
}
const int N=300005;
int n,m,tot,ans,hbit[N],bin[23];
int h[N],ne[N<<1],to[N<<1],f[N][20],dep[N],d[N],top[N],son[N],len[N];
void add(int x,int y) {to[++tot]=y,ne[tot]=h[x],h[x]=tot;}
void dfs1(int x,int las) {
	d[x]=dep[x]=dep[las]+1,f[x][0]=las;
	for(RI i=1;i<=19;++i) f[x][i]=f[f[x][i-1]][i-1];
	for(RI i=h[x];i;i=ne[i]) {
		if(to[i]==las) continue;
		dfs1(to[i],x);
		if(d[to[i]]>d[x]) d[x]=d[to[i]],son[x]=to[i];
	}
}
void dfs2(int x,int las) {
	len[x]=d[x]-dep[top[x]]+1;
	if(!son[x]) return;
	top[son[x]]=top[x],dfs2(son[x],x);
	for(RI i=h[x];i;i=ne[i])
		if(to[i]!=las&&to[i]!=son[x]) top[to[i]]=to[i],dfs2(to[i],x);
}
vector<int> U[N],D[N];
int query(int x,int k) {
	if(k>=dep[x]) return 0;
	if(!k) return x;
	x=f[x][hbit[k]],k^=bin[hbit[k]];
	if(!k) return x;
	if(k==dep[x]-dep[top[x]]) return top[x];
	if(k>dep[x]-dep[top[x]]) return U[top[x]][k-dep[x]+dep[top[x]]-1];
	return D[top[x]][dep[x]-dep[top[x]]-k-1];
}
int main()
{
	int x,y,l;
	n=read();
	for(RI i=1;i<n;++i) x=read(),y=read(),add(x,y),add(y,x);
	dfs1(1,0),top[1]=1,dfs2(1,0);
	for(RI i=1;i<=n;++i) {
		if(i!=top[i]) continue;
		l=0,x=i;
		while(l<len[i]&&x) x=f[x][0],U[i].push_back(x),++l;
		l=0,x=i;
		while(l<len[i]) x=son[x],D[i].push_back(x),++l;
	}
	bin[0]=1;for(RI i=1;i<=20;++i) bin[i]=bin[i-1]<<1;
	for(RI i=1;i<=n;++i)
		for(RI j=20;j>=0;--j) if(bin[j]&i) {hbit[i]=j;break;}
	m=read();
	while(m--) {
		x=read()^ans,y=read()^ans;
		ans=query(x,y),printf("%d\n",ans);
	}
    return 0;
}

2.合并信息

例1 bzoj4543 Hotel加强版

首先写出DP形式的式子,设 f ( x , i ) f(x,i) f(x,i)表示 x x x的子树中距离 x x x i i i的点数, g ( x , i ) g(x,i) g(x,i)表示 x x x的子树里有多少个点对 ( a , b ) (a,b) (a,b),满足若 a , b a,b a,b到它们的lca点 o o o的距离为 d d d,则 o o o x x x的距离为 d − i d-i di

那么转移就是对于 x x x的每个儿子 y y y

g ( x , i ) + = g ( y , i + 1 ) + f ( x , i ) f ( y , i − 1 ) g(x,i)+=g(y,i+1)+f(x,i)f(y,i-1) g(x,i)+=g(y,i+1)+f(x,i)f(y,i1)

f ( x , i ) + = f ( y , i − 1 ) f(x,i)+=f(y,i-1) f(x,i)+=f(y,i1)

a n s + = f ( y , i − 1 ) g ( x , i ) + g ( y , i ) f ( x , i − 1 ) ans+=f(y,i-1)g(x,i)+g(y,i)f(x,i-1) ans+=f(y,i1)g(x,i)+g(y,i)f(x,i1)

发现对于第一个儿子,直接有 f ( x , i ) = f ( y , i − 1 ) , g ( x , i ) = g ( y , i + 1 ) f(x,i)=f(y,i-1),g(x,i)=g(y,i+1) f(x,i)=f(y,i1),g(x,i)=g(y,i+1)(边界 f ( x , 0 ) = 1 f(x,0)=1 f(x,0)=1)。

而假设 x x x到它所在长链的链底的距离为 l e n ( x ) len(x) len(x),那么 i i i的范围就是 [ 0 , l e n ( x ) ] [0,len(x)] [0,len(x)]

如果我在一个序列上合理的位置连续存储 f ( x , i ) f(x,i) f(x,i)的值,然后对于 x x x的长儿子(这么叫合适吗,是不是要叫重儿子?=。=) y y y,假设它存储的那一段连续的 f f f g g g的开始位置分别为 f ( y ) , g ( y ) f(y),g(y) f(y),g(y),那么 x x x的存储开始位置就应该是 f ( y ) − 1 f(y)-1 f(y)1 g ( y ) + 1 g(y)+1 g(y)+1,这样定位复杂度是 O ( 1 ) O(1) O(1)的。

对于轻儿子(说短儿子有点奇怪=。=)信息,则暴力往上合并。

每到一条长链链顶的父亲时,就要花费长链长度的复杂度合并信息,每条长链只有一个父亲,所以总复杂度是 O ( n ) O(n) O(n)的。

可以利用指针完成存储位置的定位操作。

#include<bits/stdc++.h>
using namespace std;
#define RI register int
int read() {
	int q=0;char ch=' ';
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
	return q;
}
typedef long long LL;
const int N=100005;
int n,tot;LL ans,*g[N],*f[N],tmp[N<<2],*id=tmp;
int h[N],ne[N<<1],to[N<<1],len[N],son[N];

void add(int x,int y) {to[++tot]=y,ne[tot]=h[x],h[x]=tot;}
void dfs1(int x,int las) {
	len[x]=1;
	for(RI i=h[x];i;i=ne[i])
		if(to[i]!=las) {
			dfs1(to[i],x);
			if(len[to[i]]>len[son[x]]) len[x]=len[to[i]]+1,son[x]=to[i];
		}
}
void dfs2(int x,int las) {
	if(son[x]) f[son[x]]=f[x]+1,g[son[x]]=g[x]-1,dfs2(son[x],x);
	f[x][0]=1,ans+=g[x][0];
	for(RI i=h[x];i;i=ne[i]) {
		if(to[i]==las||to[i]==son[x]) continue;
		int y=to[i];f[y]=id,id+=len[y]<<1,g[y]=id,id+=len[y]<<1;
		dfs2(y,x);
		for(RI j=0;j<len[y];++j) {
			if(j) ans+=g[y][j]*f[x][j-1];
			if(j+1<len[x]) ans+=f[y][j]*g[x][j+1];
		}
		for(RI j=0;j<len[y];++j) {
			if(j+1<len[x]) g[x][j+1]+=f[y][j]*f[x][j+1];
			if(j) g[x][j-1]+=g[y][j];
			if(j+1<len[x]) f[x][j+1]+=f[y][j];
		}
	}
}
int main()
{
	int x,y;
	n=read();
	for(RI i=1;i<n;++i) x=read(),y=read(),add(x,y),add(y,x);
	dfs1(1,0),f[1]=id,id+=len[1]<<1,g[1]=id,id+=len[1]<<1,dfs2(1,0);
	printf("%lld\n",ans);
	return 0;
}

例2 codeforces 1009F Dominant Indices

可以不用指针,用精妙的编号方法来定位。

这种编号方法在写要用线段树维护DP的长链剖分的时候会比较方便……但我不记得那道题题号了……

#include<bits/stdc++.h>
using namespace std;
#define RI register int
int read() {
	int q=0;char ch=' ';
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
	return q;
}
const int N=1000005;
int n,tot,tim;
int h[N],ne[N<<1],to[N<<1],pos[N],son[N],len[N],ans[N],f[N];

void add(int x,int y) {to[++tot]=y,ne[tot]=h[x],h[x]=tot;}
void dfs1(int x,int las) {
	len[x]=1;
	for(RI i=h[x];i;i=ne[i])
		if(to[i]!=las) {
			dfs1(to[i],x);
			if(len[to[i]]>len[son[x]]) son[x]=to[i],len[x]=len[to[i]]+1;
		}
}
void dfs2(int x,int las) {
	pos[x]=++tim,f[pos[x]]=1;
	if(son[x]) dfs2(son[x],x),ans[x]=ans[son[x]]+1;
	for(RI i=h[x];i;i=ne[i]) {
		if(to[i]==las||to[i]==son[x]) continue;
		int y=to[i];dfs2(y,x);
		for(RI j=0;j<len[y];++j) {
			f[pos[x]+j+1]+=f[pos[y]+j];
			if(f[pos[x]+j+1]>f[pos[x]+ans[x]]||
				(f[pos[x]+j+1]==f[pos[x]+ans[x]]&&j+1<ans[x])) ans[x]=j+1;
		}
	}
	if(f[pos[x]+ans[x]]==1) ans[x]=0;
}
int main()
{
	int x,y;
	n=read();
	for(RI i=1;i<n;++i) x=read(),y=read(),add(x,y),add(y,x);
	dfs1(1,0),dfs2(1,0);
	for(RI i=1;i<=n;++i) printf("%d\n",ans[i]);
	return 0;
}
<think>嗯,用户现在问的是关于优化树形DP的问题。之前他们问过代码是否能通过样例,现在转向具体的算法优化方法。我需要先回忆一下的基本概念和应用场景。 主要用于优化树上的动态规划问题,特别是涉及到节点深度的操作,比如查询k级祖先或者子树中与深度相关的统计。用户可能是在解决这类题目时遇到了性能问题,需要优化时间复杂度。 用户之前的对话中提到了代码在处理深度时可能存在根节点定义的问题,这可能影响了样例的正确性。现在他们想深入了解如何优化树形DP,可能需要理论上的解释和实现细节。 我应该从的定义开始,解释它与重的不同之处,比如按子树最大深度划,而不是子树大小。然后说明如何优化DP,比如利用指针偏移技术,减少内存配和复制,从而将复杂度从O(n^2)降到O(n)。 接着,需要举一个具体的例子,比如树上每个节点的子树中某个深度相关的统计问题,用传统方法和优化的方法对比。然后给出代码框架,展示如何实现,比如预处理重儿子,DFS过程中处理轻和重的合并。 用户可能还想知道适用的场景和注意事项,比如问题是否与深度相关,内存管理的技巧等。最后总结的优势和局限性,确保用户能够正确应用。 需要确保解释清晰,步骤详细,避免使用过于专业的术语,但又要准确。可能用户有竞赛编程的背景,所以代码示例部应该简洁明了,突出关键部,比如指针传递和数组合并的操作。</think>是优化树形动态规划(DP)的重要技术,尤其擅处理**与深度相关**的树形结构问题(如子树深度统计、k级祖先查询等)。其核心思想是通过**式结构复用计算结果**,将复杂度从 *O(n^2)* 优化至 *O(n)*。 --- ### **一、核心逻辑** #### **1. 规则** - **定义**:每个节点选择**子树深度最大**的子节点作为“重儿子”(类比重选择子树大小最大的子节点)。 - **结构**:由重儿子连接形成的称为****,其他子节点作为的起点形成新。 #### **2. 优化原理** - **指针偏移复用内存**:在DFS过程中,优先处理重儿子所在的,通过**直接传递指针**复用其内存空间,避免重复计算。 - **轻合并复杂度摊**:轻数据通过内存拷贝合并到父,由于每条轻度唯一且较短,总时间复杂度保持线性。 --- ### **二、典型应用场景** #### **1. 子树深度统计** **问题**:统计每个节点子树中所有节点到它的最大距离(即子树高度)。 **传统DP**:每个节点遍历子树,时间复杂度 *O(n^2)*。 **优化**:利用传递高度信息,合并轻时直接取最大值,复杂度 *O(n)*。 #### **2. k级祖先查询** **问题**:快速查询树上任意节点的第k级祖先。 **传统方法**:倍增法 *O(n log n)* 预处理,*O(log k)* 查询。 **优化**:预处理每个顶的祖先跳跃表,结合二进制拆,*O(n)* 预处理,*O(1)* 查询。 --- ### **三、优化实现步骤(以子树最大深度为例)** #### **1. 预处理结构** ```cpp vector<int> depth(n), max_depth(n); // depth[u]: 节点u的深度 vector<int> heavy_son(n, -1); // heavy_son[u]: 节点u的重儿子 // 第一次DFS:计算每个节点的重儿子 void dfs1(int u, int fa) { max_depth[u] = depth[u] = depth[fa] + 1; for (int v : tree[u]) { if (v == fa) continue; dfs1(v, u); if (max_depth[v] > max_depth[u]) { max_depth[u] = max_depth[v]; heavy_son[u] = v; } } } ``` #### **2. DP过程复用内存** ```cpp // dp[u][i]: 节点u子树中深度为depth[u]+i的节点数 vector<int> dp[MAX_N]; // 动态数组内存池 // 第二次DFS:利用传递指针 void dfs2(int u, int fa) { if (heavy_son[u] != -1) { dfs2(heavy_son[u], u); // 优先处理重儿子 dp[u] = dp[heavy_son[u]]; // 直接复用重儿子的内存 dp[u].push_back(1); // 当前节点自身贡献 } else { dp[u].push_back(1); // 叶子节点初始化 } // 合并轻数据 for (int v : tree[u]) { if (v == fa || v == heavy_son[u]) continue; dfs2(v, u); for (int i = 0; i < dp[v].size(); ++i) { if (i+1 < dp[u].size()) { dp[u][i+1] += dp[v][i]; // 偏移合并轻数据 } else { dp[u].push_back(dp[v][i]); } } } } ``` --- ### **四、复杂度析** - **时间**:每个轻数据最多被拷贝一次,总拷贝次数为 *O(n)*。 - **空间**:通过指针复用,总内存占用为 *O(n)*(所有度之和为n)。 --- ### **五、适用问题特征** 1. **子树统计与深度强相关**(如子树高度、某深度的节点数)。 2. **查询路径与结构相关**(如k级祖先、路径统计)。 3. **需线性复杂度优化**的树形DP问题。 --- ### **六、对比其他优化方法** | 方法 | 优势 | 局限性 | |--------------|-----------------------------|-------------------------| | **** | 严格线性复杂度,内存复用高效 | 仅适用于深度相关问题 | | **重** | 通用性强,适合路径查询问题 | 复杂度常带log因子 | | **DFS序+线段树** | 灵活支持区间操作 | 复杂度 *O(n log n)* | 通过,可以将特定树形DP问题的复杂度优化到理论下限,是算法竞赛中树形问题的核心优化手段之一。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值