长链剖分总结

这篇博客介绍了树剖和长链剖分两种数据结构在解决树上动态规划问题中的优化作用。通过实例解析了如何使用这两种方法来提升算法效率,包括在处理点对查询和求解子树距离分布问题中的应用。文章详细阐述了算法的思路、分析和代码实现,展示了如何利用这些技巧降低复杂度并节省空间。

【算法简介】

长链剖分的思想很简单,和树剖基本一致,就是把维护的子树节点最多改成了子树深度最大

这种数据结构一般用于优化动态规划(一般涉及了长度的dp)

【习题】

1.P3899 [湖南集训]谈笑风生

【题意】

求树上的三个点构成的点对(a,b,c),给定a求满足a和b是c的祖先,a和b的距离不超过给定的k

【分析】

分类讨论

1.当b比a高时,c点的可能取值有siz[a]-1个,b的取值有min(dep[b],dep[a],k)种,相乘即可

2.当b比a低时,c点的可能取值有siz[b]-1个,b的取值为min(dep[叶子]-dep[a],k),也是相乘

第一部分的贡献很好计算

计算第二部分贡献,发现每个节点的贡献可以赋给长儿子很多,所以用到了长链剖分来优化

维护一个后缀和,计算每个点的后缀和贡献

从链的最深点开始的点,第二部分贡献加上差分的和即可

注意实现过程中,因为要从父亲节点继承信息,所以开的数组要用指针版,方便赋值

【代码】

#include<bits/stdc++.h>
using namespace std;
const int maxn=3e5+5;
typedef long long ll;
int head[maxn],n,m,tot;
ll siz[maxn],ans[maxn],tmp[maxn],*s[maxn],*temp=tmp;
struct edge
{
	int to,nxt;
}e[maxn<<1];
void add(int x,int y)
{
	e[++tot].to=y; e[tot].nxt=head[x]; head[x]=tot;
}
struct query
{
	int no,z;
};
vector <query> q[maxn];
int dep[maxn],son[maxn],md[maxn];
void dfs1(int x,int fa)
{
	dep[x]=md[x]=dep[fa]+1; siz[x]=1;
	for(int i=head[x];i;i=e[i].nxt)
	{
		int to=e[i].to;
		if(to==fa) continue;
		dfs1(to,x);
		siz[x]+=siz[to];
		if(md[to]>md[son[x]]) son[x]=to;
	}
	if(son[x]) md[x]=md[son[x]];
}
void dfs(int x,int fa)
{
	s[x][0]=siz[x]-1;
	if(son[x]) s[son[x]]=s[x]+1,dfs(son[x],x),s[x][0]+=s[son[x]][0];
	for(int i=head[x];i;i=e[i].nxt)
	{
		int to=e[i].to;
		if(to==fa || to==son[x]) continue;
		s[to]=temp; temp+=md[to]-dep[to]+1;
		dfs(to,x);
		for(int j=0;j<=md[to]-dep[to];j++) s[x][j+1]+=s[to][j];
		s[x][0]+=s[to][0];
	}
	for(int i=0;i<q[x].size();i++)
	{
		int no=q[x][i].no,y=q[x][i].z;
		ans[no]+=1LL*(siz[x]-1)*min(dep[x]-1,y);
		ans[no]+=s[x][0]-siz[x]+1;
		if(y<md[x]-dep[x])  ans[no]-=s[x][y+1];
	}
}
int main()
{
	freopen("longchain.in","r",stdin);
	freopen("longchain.out","w",stdout);
	scanf("%d%d",&n,&m);
	int x,y;
	for(int i=1;i<n;i++)
	{
		scanf("%d%d",&x,&y);
		add(x,y); add(y,x);
	}
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&x,&y);
		q[x].push_back((query){i,y});
	}
	dfs1(1,0);
	s[1]=temp; temp+=md[1];
	dfs(1,0); 
	for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
	return 0;
}

2.CF1009F Dominant Indices

【题意】

设 d(u,x) 为 u 子树中到 u 距离为 x 的节点数。对于每个点,求一个最小的 k,使得 d(u,k)最大。

【分析】

设计dp:

f[i][j]表示i子树中到i 的距离为j 的点的个数

在dp转移的时候,先做长儿子,每次处理完之后,直接把长儿子的信息向后错一位放到当前的f数组中,然后再把其他子树与当前暴力计算

我们在做dp的时候只对链的顶点申请内存,这样节省了很多空间,只需要开能放下一条链的长度即可O(N)

对于u的长儿子v,给v 的内存分配到f[u]+1上 ,即f[v=son[u]]=f[u]+1

然后去计算其他儿子的答案,对于u的非长儿子,为他申请内存f[v]=now , now+=dep[u],大小为v为顶端的长链长度

在dfs(v)之后,暴力合并信息即可

void dfs2(int u,int fa)
{
    f[u][0]=1; // 先算上自己
    if (son[u])
    {
        f[son[u]]=f[u]+1; // 共享内存,这样一步之后,f[son[u]][dep]会被自动保存到f[u][dep-1]
        dfs2(son[u],u);
    }
    for (int e=head[u];e;e=nex[e])
    {
        int v=tail[e];
        if (v==son[u] || v==fa) continue;
        f[v]=now;now+=dep[v]; // 为 v 节点申请内存,大小等于以 v 为顶端的长链的长度
        dfs2(v,u);
        for (int i=1;i<=dep[v];i++)
        {
            f[u][i]+=f[v][i-1]; //暴力合并
        }
    }
}

【代码】

#include<bits/stdc++.h>
using namespace std;
const int maxn=3e5+5;
typedef long long ll;
int head[maxn],n,m,tot;
ll siz[maxn],ans[maxn],tmp[maxn],*s[maxn],*temp=tmp;
struct edge
{
	int to,nxt;
}e[maxn<<1];
void add(int x,int y)
{
	e[++tot].to=y; e[tot].nxt=head[x]; head[x]=tot;
}
struct query
{
	int no,z;
};
vector <query> q[maxn];
int dep[maxn],son[maxn],md[maxn];
void dfs1(int x,int fa)
{
	dep[x]=md[x]=dep[fa]+1; siz[x]=1;
	for(int i=head[x];i;i=e[i].nxt)
	{
		int to=e[i].to;
		if(to==fa) continue;
		dfs1(to,x);
		siz[x]+=siz[to];
		if(md[to]>md[son[x]]) son[x]=to;
	}
	if(son[x]) md[x]=md[son[x]];
}
void dfs(int x,int fa)
{
	s[x][0]=siz[x]-1;
	if(son[x]) s[son[x]]=s[x]+1,dfs(son[x],x),s[x][0]+=s[son[x]][0];
	for(int i=head[x];i;i=e[i].nxt)
	{
		int to=e[i].to;
		if(to==fa || to==son[x]) continue;
		s[to]=temp; temp+=md[to]-dep[to]+1;
		dfs(to,x);
		for(int j=0;j<=md[to]-dep[to];j++) s[x][j+1]+=s[to][j];
		s[x][0]+=s[to][0];
	}
	for(int i=0;i<q[x].size();i++)
	{
		int no=q[x][i].no,y=q[x][i].z;
		ans[no]+=1LL*(siz[x]-1)*min(dep[x]-1,y);
		ans[no]+=s[x][0]-siz[x]+1;
		if(y<md[x]-dep[x])  ans[no]-=s[x][y+1];
	}
}
int main()
{
	freopen("longchain.in","r",stdin);
	freopen("longchain.out","w",stdout);
	scanf("%d%d",&n,&m);
	int x,y;
	for(int i=1;i<n;i++)
	{
		scanf("%d%d",&x,&y);
		add(x,y); add(y,x);
	}
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&x,&y);
		q[x].push_back((query){i,y});
	}
	dfs1(1,0);
	s[1]=temp; temp+=md[1];
	dfs(1,0); 
	for(int i=1;i<=m;i++) printf("%lld\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、付费专栏及课程。

余额充值