【长链剖分】【DP】BZOJ4543[POI2014]Hotel加强版

该博客介绍了如何解决一个图论问题:在具有n个节点的无根树中找到三个互不相同节点,使得它们两两之间的距离相等。博主通过动态规划(DP)策略来解决这个问题,定义了f(x,i)和g(x,i)两个状态,并详细解释了状态转移方程。最后,博主提出了一种利用最大儿子的DP值在O(n)时间内枚举轻儿子最大深度的方法,以实现整体O(n)的时间复杂度解决方案。" 130419932,8178746,搭建pikachu:快速指南,"['服务器', '运维', '数据库管理', 'Web应用']

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题意:

给出一棵 n 个点的无根树,请在这棵树上选三个互不相同的节点,使得这个三个
节点两两之间距离相等,输出方案数即可。


分析:

定义
f(x,i)f(x,i)f(x,i)表示在以x为根的子树中,与x距离为i的节点数
g(x,i)g(x,i)g(x,i)表示在以x为根的子树中选择了两个节点,最后一个点需满足与x的距离为i的方案数。
所以f(x,i)=∑uf(u,i−1)f(x,i)=\sum_u f(u,i-1)f(x,i)=uf(u,i1)
g(x,i)=(∑ug(u,i+1))+(∑u,vf(u,i)∗f(v,i))g(x,i)=(\sum_u g(u,i+1))+(\sum_{u,v}f(u,i)*f(v,i))g(x,i)=(ug(u,i+1))+(u,vf(u,i)f(v,i))

可以通过继承其最大儿子的DP值,使其最大儿子的转移速度为O(1),再O(n)暴力枚举每个轻儿子的最大深度。

继承的具体操作,可以通过预开内存的方式实现。
(即建立一个内存池,然后f(x)从iii位置开始,f(sonx)f(son_x)f(sonx)就从i+1i+1i+1位置开始,g(x)从i位置开始,g(sonx)g(son_x)g(sonx)就从i-1位置开始,详见代码中两个dfs)

总的复杂度为O(∑重链长度)=O(n)O(\sum 重链长度)=O(n)O()=O(n)

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#define SF scanf
#define PF printf
#define MAXN 100010
using namespace std;
typedef long long ll;
vector<int> a[MAXN];
ll Gpool[MAXN*2];
ll Fpool[MAXN];
ll *f[MAXN],*g[MAXN],*fcnt=Fpool,*gcnt=Gpool;
int maxl[MAXN],son[MAXN];
void prepare(int x,int fa=0){
	for(int i=0;i<int(a[x].size());i++){
		int u=a[x][i];
		if(u==fa)
			continue;
		prepare(u,x);
		maxl[x]=max(maxl[x],maxl[u]+1);
	}
	for(int i=0;i<int(a[x].size());i++){
		int u=a[x][i];
		if(u==fa)
			continue;
		if(son[x]==0||maxl[u]>maxl[son[x]])
			son[x]=u;
	}
}
void dfsf(int x,int fa=0){
	f[x]=++fcnt;
	if(son[x])
		dfsf(son[x],x);
	for(int i=0;i<int(a[x].size());i++){
		int u=a[x][i];
		if(u==fa||u==son[x])
			continue;
		dfsf(u,x);
	}
}
void dfsg(int x,int fa=0){
	for(int i=0;i<int(a[x].size());i++){
		int u=a[x][i];
		if(u==fa||u==son[x])
			continue;
		dfsg(u,x);
		gcnt+=maxl[u];
	}
	if(son[x])
		dfsg(son[x],x);
	g[x]=++gcnt;
}
ll ans;
void dp(int x,int fa=0){
	f[x][0]=1;
	if(son[x]==0)
		return ;
	dp(son[x],x);
	ans+=g[x][0];
	for(int i=0;i<int(a[x].size());i++){
		int u=a[x][i];
		if(u==fa||u==son[x])
			continue;
		dp(u,x);	
		for(int i=1;i<=maxl[u]+1;i++)
			ans+=g[x][i]*f[u][i-1];
		for(int i=0;i<=maxl[u]-1;i++)
			ans+=f[x][i]*g[u][i+1];
		for(int i=0;i<=maxl[u]-1;i++)
			g[x][i]+=g[u][i+1];
		for(int i=1;i<=maxl[u]+1;i++)
			g[x][i]+=(f[u][i-1]*f[x][i]);
		for(int i=1;i<=maxl[u]+1;i++)
			f[x][i]+=f[u][i-1];
	}
//	PF("%d %d(%lld):\n",x,maxl[x],ans);
//	for(int i=0;i<=maxl[x];i++)
//		PF("%d:[%lld %lld]\n",i,f[x][i],g[x][i]);
//	PF("-------------------\n");
}
int n,u,v;
void init(){
	fcnt=Fpool;
	gcnt=Gpool;
	memset(Fpool,0,sizeof Fpool);
	memset(Gpool,0,sizeof Gpool);
	memset(maxl,0,sizeof maxl);
	memset(son,0,sizeof son);
	for(int i=1;i<=n;i++)
		a[i].clear();
	ans=0;	
}
int main(){
//	freopen("three1-3.in","r",stdin);
	while(SF("%d",&n)!=EOF){
		if(n==0)
			break;
		init();
		for(int i=1;i<n;i++){
			SF("%d%d",&u,&v);
			a[u].push_back(v);
			a[v].push_back(u);	
		}
		prepare(1);
		dfsf(1);
		dfsg(1);
		dp(1);
		PF("%lld\n",ans);
	}
}
<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问题的复杂度优化到理论下限,是算法竞赛中形问题的核心优化手段之一。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值