P4383 林克卡特树 题解

题意:给定带边权的树,要切掉 k 条边,再任意连上 k 条边权为 0 的边。问最优策略下得到的树的边权最大值。n,k3×105

参考

【问题转化】

切掉 k 条边后会变成 k+1 个连通块,之后的连边一定会把这 k+1 个连通块的直径连起来。所以相当于问把原树分成恰好 k+1 个连通块后所有直径的和最大。
再转化一下,等价于在原树里找 k+1 条不相交的路径,要求和最大。

下文中直接用 k 指代原本的 k+1

处理树上的最优,考虑树形 DP。

按照套路,dpi,j 表示从 i 的子树内选恰好 j 条不相交的路径,和最大是多少。

然鹅发现这个玩意 dpfadpson 之间不能很好的建立联系。按理想中的思路,DP 方程应该根据父结点是否加入子结点的路径里面分类。但是现在没法分类,究其原因是我们的状态描述过于宽泛,我们无法通过状态描述得知子结点处的路径的状态。

对于状态描述信息太少的情况,我们一如既往地升维。(这么朴素的想法,是否能想得到呢?)

新增一维描述子树根处路径的状态。dpi,j,0/1/2 表示从 i 的子树内选恰好 j 条不相交的路径,且 i "不属于任何一条路径"/"属于一条路径且 i 在这条路径的端点处"/"属于一条路径且 i 不在这条路径的端点处"。为了方便更新,令 dpi,j,3=max(dpi,j,0/1/2)

我们允许一个点作为一个 "退化链" 存在。

【状态转移】

(转移方程和细节在参考题解中写了而且写的很好)

dpi,j,k 表示在 i 的子树内选了 j 条不相交的路径,且 i 不属于任何路径/是路径的一端/是路径的中间结点。

认为一个单个结点构成的退化链既可以作为路径的一端,也可以作为路径的中间结点。

设当前结点为 u,当前循环到的子结点为 v

先不考虑让 u 自己独立做一个退化链的情况,原因后文有提到。也就是先只考虑 v 的方案合并到 u 的情况。

  1. 更新 dp[u][i][0]v 的路径也不可能上到 u 这里。(否则 u 属于了 v 的那条路径)让 v 的子树自治即可。

    dp[u][i][0]=maxj=0i(dp[u][i][0],dp[u][ij][0]+dp[v][j][3]),i=0k

  2. 更新 dp[u][i][1]。有两种可能:u 之前就属于路径的一端,v 不干涉 uu 之前不属于任何路径,现在属于 v 延长上来的路径。第二种情况要求 v 能延长上来,自然要求 v 是路径的一端。

    对于这种情况,显然应有 i1

    dp[u][i][1]=maxj=0i1(dp[u][i][1],dp[u][ij][1]+dp[v][j][3])

    这是第一种情况的转移方程。注意 ji1,因为要之前就有路径。

    dp[u][i][1]=maxj=1i(dp[u][i][1],dp[u][ij][0]+dp[v][j][1]+wu,v)

    这是第二种情况的转移方程。注意 j1,因为要 v 有路径能延长。

    这里本还有一种让 u 自己作为一条退化链的情况,但是我们上面说了先不考虑这种情况。

  3. 更新 dp[u][i][2]。有两种可能:u 之前就属于一条路径的中间结点;u 之前属于一条路径的一端,现在 v 又延长上来。

    dp[u][i][2]=maxj=0i1(dp[u][i][2],dp[u][ij][2]+dp[v][j][3])

    这是第一种情况的转移方程。

    dp[u][i][2]=maxj=1i(dp[u][i][2],dp[u][i+1j][1]+dp[v][j][1]+wu,v)

    这是第二种情况的转移方程。注意观察:dp[u][i+1j][1],为什么是 i+1j?因为最终是 i 条路径,u 之前的路径现在和 v 的路径合并了,减少了一条。减少一条后是 i 条路径,说明之前应有 i+1j 条路径。

    这里同样也省略了一种让 u 自己作为一条退化链的情况。

  4. u 自己作为一条退化链的情况。

    为什么最后处理这种情况?

    如果将这种情况设为初值(或者在循环中更新这种情况),在更新 dp[u][i][2] 的第二种情况时,我们会认为 u 在 "之前的退化链 u 加上 v 延展过来的路径" 这构成的新路径的中间。但显然 u 应该是这条路径的一端。

    因此我们最后处理这种情况。u 自己作为一条退化链,要求了 u 一开始就不能有任何路径涉及到它,所以这种情况都根据 dp[u][i][0] 转移而来。

    dp[u][i][1]=max(dp[u][i][1],dp[u][i1][0])

    dp[u][i][2]=max(dp[u][i][2],dp[u][i1][0])

  5. 更新 dp[u][i][3]=max(dp[u][i][0/1/2])

以上就是转移方程的所有。

在实现时还有诸多细节需要注意。例如转移方程(就和大多数树形 DP 一样)会自己更新自己,如果不采取在循环顺序上的技巧,就要额外开数组保存原本的信息以避免重复更新。

如果使用倒序循环避免重复更新,还要特别注意一个地方:更新 dp[u][i][2] 的第二种情况,可能会涉及到从 dp[u][i+1j][1] 转移过来。i+1j 是可以取到 i 的!!! 如果先更新了 dp[u][i][1] 再更新 dp[u][i][2],也会导致多次更新。

复杂度和树形背包一样是 O(nk) 的,可以通过 60% 的数据。

点击查看代码
#include <bits/stdc++.h>

using namespace std;
typedef int ll;
const ll N = 3e5 + 5, inf = 0x3f3f3f3f3f3f3f3f;

int n, K;
struct Edge {
	ll to, val;
	Edge(ll t = 0, ll v = 0) {
		to = t, val = v;
	}
};
vector<Edge> e[N];

ll dp[N][105][5]; //dp[i][j][k]:i子树里恰好选j条不相交的路径且i处度数为k的最大和,dp[i][j][3]为dp[i][j][0/1/2]max 


void dfs(int x, int pr) {
	dp[x][0][0] = 0;
	for (int i = 0; i < e[x].size(); i++)
		if (e[x][i].to != pr) {
			dfs(e[x][i].to, x); //先把子树的求了 
			ll y = e[x][i].to, z = e[x][i].val;
			
			for (int j = K; j >= 1; j--) {
				
				for (int k = j; k >= 1; k--) {
					
					dp[x][j][2] = max(dp[x][j][2], 
								max(dp[x][j - k][2] + dp[y][k][3], dp[x][j + 1 - k][1] + dp[y][k][1] + z));
				}
				
				//k=j
				dp[x][j][0] = max(dp[x][j][0], dp[x][j - j][0] + dp[y][j][3]);
				dp[x][j][1] = max(dp[x][j][1], dp[x][j - j][0] + dp[y][j][1] + z);
				
				for (int k = j - 1; k >= 1; k--) {
					dp[x][j][0] = max(dp[x][j][0], dp[x][j - k][0] + dp[y][k][3]);				
					dp[x][j][1] = max(dp[x][j][1],
								max(dp[x][j - k][1] + dp[y][k][3], dp[x][j - k][0] + dp[y][k][1] + z));
				}
				
				//k=0不用变化 
			}
			//j=0只有dp[x][0][0]已经算过
			 
		}
		
	for (int j = 1; j <= K; j++) { //x成为退化链 
		dp[x][j][1] = max(dp[x][j][1], dp[x][j - 1][0]);
		dp[x][j][2] = max(dp[x][j][2], dp[x][j - 1][0]);
	}
	for (int j = 0; j <= K; j++)
		dp[x][j][3] = max(max(dp[x][j][0], dp[x][j][1]), dp[x][j][2]);dp[x][k][2]);
}

int main() {
	cin >> n >> K;
	K++;
	for (ll i = 1, u, v, w; i < n; i++) {
		cin >> u >> v >> w;
		e[u].push_back(Edge(v, w));
		e[v].push_back(Edge(u, w));
	}
	memset(dp, ~inf, sizeof dp);
	dfs(1, 0);
	cout << dp[1][K][3];
	return 0;
}

【优化】

复杂度是 O(nk) 的。如何优化?

感性理解一下,因为割掉每一条边都是独立的,所以我们一定会优先割掉能使答案增加最多/减少最少的边。对应到图像上,不可能有 "凹" 的部分,否则将方案调整可以更优。

因此可以 wqs 二分优化。

wqs 二分优化 DP 的题目,除了计算最优值,往往还需要计算最优值是取了多少个(或者说最多/最少取多少个),因此建议把 wqs 二分的 DP 状态定义成一个结构体,用重载运算符的方式写。

【wqs 二分的经典坑点】

想清楚二分结束后要选用的是 l 作为斜率还是 r

其实这里唯一会影响到的情况只有一种:就是 (ans,f(ans)) 在一个斜率相等段上。我们就假设如果二分到这个相等的斜率会怎么样。

如果我们二分到相等的斜率 slope,按照代码(这里按照我写的)我会选择尽量多的 0 代价,所以我找到的应该是这个斜率段的最右端 p,而 p>ans 所以我的程序会认为二分的斜率小了,所以会让 l=slope

所以最后调用的是 l 而不是 r

【AC Code】

时长:3.5h,主要时间在于写 DP 转移方程,调代码反而只用 1h。

点击查看代码
#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
const ll N = 3e5 + 5, inf = 0x3f3f3f3f3f3f3f3f;

int n, K;
struct Edge {
	ll to, val;
	Edge(ll t = 0, ll v = 0) {
		to = t, val = v;
	}
};
vector<Edge> e[N];

struct Node {
	ll dp, cnt;
	Node(ll d = 0, ll c = 0) {
		dp = d;
		cnt = c;
	}
};
bool operator<(Node a, Node b) {
	if (a.dp != b.dp)
		return a.dp < b.dp;
	return a.cnt < b.cnt;
}
Node operator+(Node a, Node b) {
	return Node(a.dp + b.dp, a.cnt + b.cnt);
}
Node operator+(Node a, ll b) {
	return Node(a.dp + b, a.cnt);
}
Node max(Node a, Node b) {
	if (a < b)
		return b;
	return a;
}

Node dp[N][5];

void dfs(int x, int pr, ll mid) {
	dp[x][0] = Node(0, 0);
	for (int i = 0; i < e[x].size(); i++)
		if (e[x][i].to != pr) {
			dfs(e[x][i].to, x, mid); //先把子树的求了 
			int y = e[x][i].to, z = e[x][i].val;
			
			Node t0 = dp[x][0], t1 = dp[x][1], t2 = dp[x][2];
			//dp[x][0]
			dp[x][0] = max(dp[x][0], t0 + dp[e[x][i].to][3]);
			//dp[x][1]
			dp[x][1] = max(dp[x][1], t1 + dp[e[x][i].to][3]);
			dp[x][1] = max(dp[x][1], t0 + dp[e[x][i].to][1] + e[x][i].val);
			//dp[x][2]
			dp[x][2] = max(dp[x][2], t2 + dp[e[x][i].to][3]);
			dp[x][2] = max(dp[x][2], Node(t1.dp + dp[e[x][i].to][1].dp + mid + e[x][i].val, t1.cnt + dp[e[x][i].to][1].cnt - 1));
		}
		
	//x成为退化链,开一段新的链 
	dp[x][1] = max(dp[x][1], Node(dp[x][0].dp - mid, dp[x][0].cnt + 1));
	dp[x][2] = max(dp[x][2], Node(dp[x][0].dp - mid, dp[x][0].cnt + 1));
	dp[x][3] = max(max(dp[x][0], dp[x][1]), dp[x][2]);
}

ll chk(ll mid) {
	for (int i = 1; i <= n; i++) {
		dp[i][0] = dp[i][1] = dp[i][2] = dp[i][3] = Node(~inf, ~inf);
	} 
	dfs(1, 0, mid);
	return dp[1][3].cnt;
}

int main() {
	cin >> n >> K;
	K++;
	for (int i = 1, u, v, w; i < n; i++) {
		cin >> u >> v >> w;
		e[u].push_back(Edge(v, w));
		e[v].push_back(Edge(u, w));
	}
	
	ll l = -1e12, r = 1e12;
	while (r - l > 1) {
		ll mid = (l + r) / 2; //mid变大了会导致选的边数变少 
		if (chk(mid) == K) {
		    cout << dp[1][3].dp + mid * K << endl;
		    return 0;
		}
		if (chk(mid) < K) {//mid大了 
			r = mid;
		}
		else
			l = mid;
	}
	chk(l);
	cout << dp[1][3].dp + l * K;
	return 0;
}
posted @   FLY_lai  阅读(47)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示