【动态规划】长链剖分优化树形 dp

我们在树形 dp 中经常会遇到这样一个模型:

fx,i 表示节点 x 的子树中深度为 i 的答案...有递推式: fx,i=sonfson,i1/i+1

这样直接做是 Θ(n2) 的,我们考虑去优化这个 dp。

有一个小优化,就是我们想让 fx 直接继承一个儿子 fson 的答案,这时不需要暴力合并,直接过继信息即可。这一点我们可以用分配空间做到,以 fx,i=sonfson,i1 为例,就是将 fson 的头指针设在 fx 的头指针后一位,就可以直接得到答案。

考虑直接继承哪一个儿子呢?fx,ii 的个数取决于子树 x 的最大深度,所以我们将子树深度最大的儿子过继即可,容易发现这个就是长链剖分的重儿子。

对于其他儿子,我们直接循环 i[0,maxdepson] 来暴力合并答案。

看似没有优化多少,这时你会发现程序很快,事实上,时间已经优化到了 Θ(n) ,下面做分析:

考虑一个点什么时候会被暴力合并,即它是其父亲的轻儿子时,所以它就是一条长链的链顶。然而它被暴力合并需要的次数就是这条长链的长度,而这条长链下面的点都直接过继给父亲,没有暴力合并,相当于每个点在暴力合并中贡献了一次,所以只会合并 Θ(n) 次。

模板题 CF1009F Dominant Indices

给定一棵 1 为根,有 n 个节点的树,对于每一个节点 ud(u,x)u 子树中距离 ux 的节点个数,对于每个 u ,求最小的 kd(u,k) 最大。

1n106

考虑设状态,我们设 fx,ix 子树中距离 xi 的有多少个,有转移:

fx,i=sonfson,i1fx,0=1

与上面说的相符,直接长剖转移即可,对于每个节点记录最大转移点,过继重儿子的时候将转移点判断一下,再继承过来。

对于空间分配,分析上述证明过程可以发现总空间占用也是 Θ(n) 的,实现细节见代码。

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
vector <int> G[N];
struct Tree{
	int dfn,dep,mxd,siz,fa,top,son;
}t[N];
int val[N * 2],*dp[N],*tp = val,tot = 0,mxp[N],n;
inline void dfs1(int x,int last)
{
	t[x].fa = last;
	t[x].dep = t[last].dep + 1;
	t[x].siz = 1;
	t[x].mxd = 0;
	for(auto to : G[x])
	{
		if(to == last) continue;
		dfs1(to,x);
		t[x].siz += t[to].siz;
		if(t[to].mxd + 1 > t[x].mxd)
		{
			t[x].mxd = t[to].mxd + 1;
			t[x].son = to;
		}
	}
}
inline void dfs2(int x,int last)
{
	t[x].dfn = ++tot;
	if(t[x].son) {dp[t[x].son] = dp[x] + 1; t[t[x].son].top = t[x].top; dfs2(t[x].son,x);}
	for(auto to : G[x])
	{
		if(to == last || to == t[x].son) continue;
		t[to].top = to;
		dp[to] = tp; tp += t[to].mxd + 1;
		dfs2(to,x);
	}
}
inline void dfs3(int x,int last)
{
	dp[x][0] = 1; mxp[x] = 0;
	if(t[x].son) 
	{
		dfs3(t[x].son,x);
		if(dp[x][mxp[t[x].son] + 1] > dp[x][mxp[x]]) mxp[x] = mxp[t[x].son] + 1;
	}
	for(auto to : G[x])
	{
		if(to == last || to == t[x].son) continue;
		dfs3(to,x);
		for(int i = 0;i <= t[to].mxd;i++) 
		{
			dp[x][i + 1] += dp[to][i];
			if(dp[x][i + 1] > dp[x][mxp[x]]) mxp[x] = i + 1;
			else if(dp[x][i + 1] == dp[x][mxp[x]] && mxp[x] > i + 1) mxp[x] = i + 1;
		}
	}
}
int main()
{
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	cin>>n;
	for(int i = 1,x,y;i <= n - 1;i++)
	{
		cin>>x>>y;
		G[x].push_back(y);
		G[y].push_back(x);
	}
	dfs1(1,0);
	t[1].top = 1; dp[1] = tp; tp += t[1].mxd;
	dfs2(1,0);
	dfs3(1,0);
	for(int i = 1;i <= n;i++) cout<<mxp[i]<< '\n';
	return 0;
}

[POI2014] HOT-Hotels

给一棵有 n 个节点的树,求有多少组点 (i,j,k) 满足两两之间的距离相等。

1n105

容易发现这样是三元组一定形如一个中心点到三个点的距离相等。这个中心点可以是三个点的 lca ,也可以在某一个点的子树内。

我们考虑统计下面的一对点,设 gx,i 表示 x 的子树内的点对,并且在 x 上接上一条长为 i 的链就可以形成贡献的点对数。

也可以定义为: 点对 (a,b) 满足 a,blca 距离为 d ,都在子树内,且 lcax 距离为 di 的 点对数。

我们发现每次 gx,i 只需要加上在 x 点汇聚的点对数即可,记 fx,ix 子树中距离 xi 的点个数,用 f 可以更新 g

对于每一个儿子 son ,转移为:

gx,i+=fx,ifson,i1

fx,i+=fson,i1

gx,i+=gson,i+1

我们考虑同样地长剖优化,将 fson 的头指针设在 fx 后一位,gson 的头指针设在 gx 前一位,所以每次留空间要留两倍。

考虑第一个转移必须要枚举一边,通过重复空间不能转移。

但是我们发现, fx,i 没有值的情况下是不存在第一种转移的,所以我们可以直接过继重儿子信息,无视第一种转移。

那么我们如何统计答案呢?

考虑 “中心点” 头上的链穿过 x 的答案,分为两种不重复的情况:

  1. 一个点在前面的子树中,两个点在子树 son 中:fx,i1gson,i 。意思就是 son 中的点对还需要 i 的长度,算上 sonx 的边,前面子树中到 x 长度为 i1 的链个数就是答案。

  2. 两个点在前面的子树中,一个点在 son 中:gx,ifson,i1 。同理。

我们可以发现,合并第一个儿子时答案只有一种贡献:就是中心点到 x 的距离正好等于下面的点对到中心点的距离,我声称这就是 gson,1 (代码里写的是只有一个儿子时的 gx,0 ,是等价的)。

所以过继重儿子的时候也不用统计答案,保证了复杂度为 Θ(n)

#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
int n,a[N],b[N],tot = 0;
vector <int> G[N];
typedef long long ll;
ll v1[N * 5],*f[N],*g[N],*tp = v1,ans = 0;
struct Node{
    int dep,dfn,son,siz,mxd,fa,top;
}t[N];
inline void dfs1(int x,int last)
{
    t[x].dep = t[last].dep + 1;
    t[x].fa = last;
    t[x].siz = 1;
    t[x].mxd = 1;
    for(auto to : G[x])
    {
        if(to == last) continue;
        dfs1(to,x);
        t[x].siz += t[to].siz;
        if(t[to].mxd + 1 > t[x].mxd) t[x].mxd = t[to].mxd + 1,t[x].son = to;
    }
}
inline void dfs2(int x,int last)
{
    t[x].dfn = ++tot;
    if(t[x].son) {t[t[x].son].top = t[x].top; f[t[x].son] = f[x] + 1; g[t[x].son] = g[x] - 1; dfs2(t[x].son,x); }
    for(auto to : G[x])
    {
        if(to == last || to == t[x].son) continue;
        f[to] = tp; tp += 2 * (t[to].mxd + 1);
        g[to] = tp; tp += 2 * (t[to].mxd + 1);
        t[to].top = to;
        dfs2(to,x);
    }
}
inline void dfs3(int x,int last)
{
    f[x][0] = 1; g[x][0] = 0;
    if(t[x].son) {dfs3(t[x].son,x);}
    ans += g[x][0];
    for(auto to : G[x])
    {
        if(to == last || to == t[x].son) continue;
        dfs3(to,x);
        for(int i = 1;i <= t[to].mxd;i++) ans += f[x][i - 1] * g[to][i] + g[x][i] * f[to][i - 1];
        for(int i = 0;i <= t[to].mxd;i++) g[x][i + 1] += f[x][i + 1] * f[to][i];
        for(int i = 0;i <= t[to].mxd;i++) f[x][i + 1] += f[to][i];
        for(int i = 1;i <= t[to].mxd;i++) g[x][i - 1] += g[to][i];
    }
}
int main()
{
    cin>>n;
    for(int i = 1,x,y;i <= n - 1;i++)
    {
        cin>>x>>y;
        G[x].push_back(y);
        G[y].push_back(x);
    }
    dfs1(1,0);
    t[1].top = 1; 
    f[1] = tp; tp += 2 * (t[1].mxd + 1); 
    g[1] = tp; tp += 2 * (t[1].mxd + 1);
    dfs2(1,0);
    dfs3(1,0); 
    cout<<ans;
    return 0;
}

[WC2010] 重建计划

给定 n 个点的树,L,U ,求一条边数在 [L,U] 之间且边权平均值最大的路径,输出最大值。

1n105

求平均值最大,考虑 0/1 分数规划,二分这个答案,将每条边权减去 mid ,找一条 [L,U] 之间的非负权路径。

我会点分!Θ(nlog3n) 直接带走(详见题解区)。

考虑一个点 x 子树内所有到 x 的路径,我们发现长度相同时我们只关注权值最大的一条,所以设 fx,i 表示 x 子树中到 x 的长度为 i 路径最大权值。

转移:

fx,i=maxfson,i1+wx,son

我们发现这个不能直接继承,需要加一个边,这当然可以线段树区间加,但是更简便的方法是将这个 “最大权值” 改为 “点的最大深度”。

转移变为:

fx,i=maxfson,i1

这就可以用长剖直接继承。

答案怎么求呢?

ans=maxi{fson,i1+maxj[max(1,Li),min(maxdx,Ui)]{fx,j}}2disx

其中 maxdxxx 所在长链链底的距离。对于 fx,ii 就被限定在 [0,maxdx] 中。disxx 的带权深度。

我们发现这实在需要一个区间 max ,而且还带修,所以考虑用线段树维护,单点修改和区间查询最值。将这个 f 数组套在一个线段树上,原来的指针就变成了下标,剩下的没有变化。

注意这个 fx,j 指的是合并儿子 son 之前的 fx,j ,所以先用临时变量将这个儿子的值存起来,取完 ans 再更新。

同理,转移重儿子时仍然不需要更新 ans

最后 ans 可能是某个点到 x 的祖孙链,再取一下 maxi[L,min(U,maxdx)]fx,idisx 即可。

由于内层要用线段树维护,时间复杂度 Θ(nlog2n) 。常数巨大。

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5 + 5;
const double eps = 1e-5,inf = LONG_LONG_MAX;
struct Edge{
	int v,next;
	double w;
}e[N * 2];
int head[N],n,L,U,tot = 0;
double mid,ans,tmp[N];
struct Segment_Tree{
	double a[N << 3];
	inline void modify(int l,int r,int x,double k,int pos)
	{
		a[pos] = max(a[pos],k);
		if(l == r) return;
		int mid = (l + r) >> 1;
		if(x <= mid) modify(l,mid,x,k,pos << 1);
		else modify(mid + 1,r,x,k,pos << 1 | 1);
	}
	inline double query(int l,int r,int L,int R,int pos)
	{
		if(L > R) return -inf;
		if(L <= l && r <= R) return a[pos];
		int mid = (l + r) >> 1; double ret = -inf;
		if(L <= mid) ret = max(ret,query(l,mid,L,R,pos << 1));
		if(R > mid) ret = max(ret,query(mid + 1,r,L,R,pos << 1 | 1));
		return ret;
	}
	inline void clear() {fill(a,a + (N << 3),-inf);}
}t;
int f[N]; 
struct Node{
	int dfn,siz,dep,mxd,son,fa,top;
	double dis;
}a[N];
inline void dfs1(int x,int last)
{
	a[x].dep = a[last].dep + 1;
	a[x].siz = 1;
	a[x].fa = last;
	a[x].mxd = 0;
	for(int i = head[x];i;i = e[i].next)
	{
		int to = e[i].v;
		if(to == last) continue;
		a[to].dis = a[x].dis + e[i].w;
		dfs1(to,x);  
		a[x].siz += a[to].siz;
		if(a[to].mxd + 1 > a[x].mxd) a[x].mxd = a[to].mxd + 1,a[x].son = to;
	}
} 
inline void dfs2(int x,int last)
{
	a[x].dfn = ++tot;
	f[x] = a[x].dfn;
	if(a[x].son) {a[a[x].son].top = a[x].top; dfs2(a[x].son,x);}
	for(int i = head[x];i;i = e[i].next)
	{
		int to = e[i].v;
		if(to == last || to == a[x].son) continue;
		a[to].top = to;
		dfs2(to,x);
	}
}
inline void add(int x,int y,int z)
{
	++tot;
	e[tot].v = y;
	e[tot].w = z;
	e[tot].next = head[x];
	head[x] = tot;
	++tot;
	e[tot].v = x;
	e[tot].w = z;
	e[tot].next = head[y];
	head[y] = tot;
}
inline void dfs3(int x,int last)
{
	t.modify(1,N,f[x],a[x].dis - mid * a[x].dep,1);
	if(a[x].son) dfs3(a[x].son,x);
	for(int i = head[x];i;i = e[i].next)
	{
		int to = e[i].v;
		if(to == last || to == a[x].son) continue;
		dfs3(to,x);
		for(int j = 1;j <= a[to].mxd + 1;j++)
		{
			double vto = t.query(1,N,f[to] + j - 1,f[to] + j - 1,1);
			if(j <= U)
				ans = max(ans,vto + t.query(1,N,f[x] + max(1ll,L - j),f[x] + min(U - j,a[x].mxd),1) - 2 * (a[x].dis - mid * a[x].dep));
			tmp[j] = vto;
		}
		for(int j = 1;j <= a[to].mxd + 1;j++) t.modify(1,N,f[x] + j,tmp[j],1);
	}
	ans = max(ans,t.query(1,N,f[x] + L,f[x] + min(U,a[x].mxd),1) - (a[x].dis - mid * a[x].dep));
}
inline bool ck()
{
	t.clear();
	ans = -inf;
	dfs3(1,0);
	return ans >= 0;
}
signed main()
{
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	cin>>n;
	cin>>L>>U;
	for(int i = 1,x,y,z;i <= n - 1;i++)
	{
		cin>>x>>y>>z;
		add(x,y,z);
	}
	tot = 0;
	a[0].dep = -1; a[0].dis = 0;
	dfs1(1,0);
	dfs2(1,0);
	double l = 0,r = 1e6;
	while(l + eps < r)
	{
		mid = (l + r) / 2;
		if(ck()) l = mid;
		else r = mid;
	}
	cout<<fixed<<setprecision(3)<<l;
	return 0;
}
posted @   The_Last_Candy  阅读(100)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示