长链剖分优化 dp

思维含量很低实现含量很高的东西。

对于一个存在深度维或者深度大小维的树形 dp,可以考虑把深度维换到最前面。然后重儿子直接继承,轻儿子暴力转移

我们先转移长剖重儿子,尝试 \(\mathcal O(1)\) 继承。通常来说这是一个可以整体 dp 进行的操作。一般需要进行移位操作。

比较精妙的写法是利用指针。我们把所有链的 dp 数组放在一起,开一个内存池 Memory[],我们用一个指针给所有链分配内存。

一切开始之前我们先给根节点的那条链分配内存。

先考虑重儿子继承。例如 \(f_{u,j}\gets f_{v,j-1}\),那么我们进行 \(ad_v\gets ad_u+1\) 即可,其中 \(ad_u\) 表示 \(f_u\) 的起始地址,也就是 *f[u]。因为我们不能开 \(n\times n\) 的数组,所以我们干脆直接就开一堆指针,直接 f[v]=f[u]+1 就好。

这样做的道理是 \(v\) 的 dp 数组将直接存在 \(ad_u+1\) 的位置。那么等到回溯回来 \(u\) 使用的时候,就相当于 \(f_v\) 全部后移一位,也就是 \(f_{u,j}\) 用上了 \(f_{v,j-1}\)。当然,如果还有其他整体 dp 操作,使用全局变量维护一下就好。

然后考虑轻儿子暴力。显然我们暴力转移轻儿子是正确的。因为轻儿子的最大深度只有它的长链长度,均摊下来总和就是所有长链的长度,也就是 \(\mathcal O(n)\)。所以直接转移就好。

实现上我们要先遍历轻儿子计算轻儿子链的 dp 数组。移动一下分配内存的指针把内存分配下去就可以了。

这些东西都可以用 vectordeque 之类的东西写,但是有很 dirty 的边界问题(0-index 和空 vector 将非常恶心)。所以还是写指针吧。

一个写法:

vector <int> p[N];
int hson[N],len[N];
int memo[N],*f[N],*pt;
void calc(int x,int fa){
	if(hson[x]) f[hson[x]]=f[x]+1,calc(hson[x],x);
	for(auto y:p[x]){
		if(y==fa||y==hson[x]) continue;
		f[y]=pt;
		pt+=len[y];
		calc(y,x);
		fr1(j,1,len[y]){
			//use f[y][j-1] to transfer
		}
	}
}
int main(){
	f[1]=memo;
	pt=memo+len[1];//f[1] can only use [0,len[1])
	calc(1,0);
	ET;
}

其中 \(len\) 表示每条长链的长度。需要注意分配内存时边界处的 \(len_y\) 是否应当取得。

这样做之后显然时间复杂度变成了 \(\mathcal O(n)\),由于我们精细的实现,空间复杂度也是 \(\mathcal O(n)\),可以看成我们到最后只对每条长链链顶维护答案,因为我们需要精确值的位置只有链顶,中途我们在转移的过程中直接继承了长链上的点的答案把一般的点转移乱了。

所以深度维总和是 \(\mathcal O(n)\) 的。

同时也因此,如果试图查询非长链链顶的 dp 值,需要离线在中间做。

P3899 [湖南集训] 更为厉害

考虑我们最后比较麻烦的是求这个:

\[\sum\limits_{1\le dep_b-dep_a\le k,b\in \operatorname{subtree}(a)} siz_b-1 \]

显然可以二维数点,此处不题。

考虑 dp,\(f_{i,p}\) 表示 \(i\) 子树内到 \(i\) 的距离 \(\le p\) 的点的 \(siz-1\) 之和,那么我们只需离线查询 \(f_{a,k}\),抠掉 \(siz_a-1\) 即可。

转移非常简单。

\[f_{u,p}\gets siz_{u}-1+\sum\limits_{v\in \operatorname{son}(u)} f_{v,p-1} \]

显然可以使用长链剖分优化。我们先进行第一步转移,然后维护一个此位置的整体加标记。有了整体加标记之后,转移轻儿子链时需要加上轻儿子链链顶的整体加标记,并且继承重儿子时也要把整体加标记继承上来。

总时间复杂度 \(\mathcal O(n)\)

有必要给出一个参考实现:

int n,q;
vector <int> p[N];
ll memo[N],*dp[N],*ptr=memo;
ll add[N];
ll ans[N];
int len[N],hson[N],mxdep[N],dep[N],siz[N];
vector <pii> ofl[N];
void dfs(int x,int fa){
	dep[x]=dep[fa]+1;
	mxdep[x]=dep[x];
	len[x]=1,siz[x]=1;
	int idx=0;
	for(auto y:p[x]){
		if(y==fa) continue;
		dfs(y,x);
		siz[x]+=siz[y];
		if(mxdep[y]>mxdep[idx]) idx=y;
		mxdep[x]=max(mxdep[x],mxdep[y]);
	}
	hson[x]=idx;
	len[x]+=len[hson[x]];
}
void calc(int x,int fa){
	if(hson[x]){
		dp[hson[x]]=dp[x]+1;
		calc(hson[x],x);
		add[x]+=add[hson[x]];
		dp[x][0]=-add[x];
	}
	for(auto y:p[x]){
		if(y==hson[x]||y==fa) continue;
		dp[y]=ptr;
		ptr+=len[y];
		calc(y,x);
		add[x]+=dp[y][len[y]-1]+add[y];
		dp[x][0]-=dp[y][len[y]-1]+add[y];
		fr1(i,1,len[y]){
			(dp[x][i]+=dp[y][i-1]-dp[y][len[y]-1]);
		}
	}
	for(auto y:ofl[x]) ans[y.fi]+=dp[x][min(y.se,len[x]-1)]+add[x];
	add[x]+=siz[x]-1;
}
int main(){
	ios::sync_with_stdio(false);
	cin>>n>>q;
	fr1(i,2,n){
		int u,v;
		cin>>u>>v;
		p[u].pb(v);
		p[v].pb(u);
	}
	dfs(1,0);
	fr1(i,1,q){
		int p,k;
		cin>>p>>k;
		ans[i]=1ll*(siz[p]-1)*min(dep[p]-1,k);
		ofl[p].pb(mp(i,k));
	}
	dp[1]=ptr;
	ptr=memo+len[1];
	calc(1,0);
	fr1(i,1,q) cout<<ans[i]<<'\n';
	ET;
}
//ALL FOR Zhang Junhao.

重链剖分也可以类似完成第二维大小只有子树大小且容易继承和整体 dp 的题目。例如 [THUWC2018] 城市规划,先用点分转化连通块问题,钦定一种颜色,然后在点分的树内 dp 另一种颜色,就可以使用这个技巧。因为第二维只是大小上与子树大小相同,所以大约可能还要 dsu 一下。

posted @ 2025-01-07 11:14  Shunpower  阅读(109)  评论(0)    收藏  举报