P4383 林克卡特树 题解

题意:给定带边权的树,要切掉 \(k\) 条边,再任意连上 \(k\) 条边权为 \(0\) 的边。问最优策略下得到的树的边权最大值。\(n,k\le 3\times 10^5\)

参考

【问题转化】

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

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

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

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

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

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

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

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

【状态转移】

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

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

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

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

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

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

    \[dp[u][i][0]=\max_{j=0}^i(dp[u][i][0],dp[u][i-j][0]+dp[v][j][0]),i=0\sim k \]

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

    对于这种情况,显然应有 \(i\ge 1\)

    \[dp[u][i][1]=\max_{j=0}^{i-1}(dp[u][i][1],dp[u][i-j][1]+dp[v][j][3]) \]

    这是第一种情况的转移方程。注意 \(j\le i-1\),因为要之前就有路径。

    \[dp[u][i][1]=\max_{j=1}^{i}(dp[u][i][1],dp[u][i-j][0]+dp[v][j][1]+w_{u,v}) \]

    这是第二种情况的转移方程。注意 \(j\ge 1\),因为要 \(v\) 有路径能延长。

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

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

    \[dp[u][i][2]=\max_{j=0}^{i-1}(dp[u][i][2],dp[u][i-j][2]+dp[v][j][3]) \]

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

    \[dp[u][i][2]=\max_{j=1}^{i}(dp[u][i][2],dp[u][i+1-j][1]+dp[v][j][1]+w_{u,v}) \]

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

    这里同样也省略了一种让 \(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][i-1][0]) \]

    \[dp[u][i][2]=\max(dp[u][i][2],dp[u][i-1][0]) \]

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

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

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

如果使用倒序循环避免重复更新,还要特别注意一个地方:更新 \(dp[u][i][2]\) 的第二种情况,可能会涉及到从 \(dp[u][i+1-j][1]\) 转移过来。\(i+1-j\) 是可以取到 \(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 @ 2024-04-14 22:25  FLY_lai  阅读(15)  评论(0编辑  收藏  举报