P4383 林克卡特树 题解
题意:给定带边权的树,要切掉
条边,再任意连上 条边权为 的边。问最优策略下得到的树的边权最大值。 。
【问题转化】
切掉
再转化一下,等价于在原树里找
下文中直接用
处理树上的最优,考虑树形 DP。
按照套路,
然鹅发现这个玩意
对于状态描述信息太少的情况,我们一如既往地升维。(这么朴素的想法,是否能想得到呢?)
新增一维描述子树根处路径的状态。
我们允许一个点作为一个 "退化链" 存在。
【状态转移】
(转移方程和细节在参考题解中写了而且写的很好)
认为一个单个结点构成的退化链既可以作为路径的一端,也可以作为路径的中间结点。
设当前结点为
先不考虑让
-
更新
。 的路径也不可能上到 这里。(否则 属于了 的那条路径)让 的子树自治即可。 -
更新
。有两种可能: 之前就属于路径的一端, 不干涉 ; 之前不属于任何路径,现在属于 延长上来的路径。第二种情况要求 能延长上来,自然要求 是路径的一端。对于这种情况,显然应有
。这是第一种情况的转移方程。注意
,因为要之前就有路径。这是第二种情况的转移方程。注意
,因为要 有路径能延长。这里本还有一种让
自己作为一条退化链的情况,但是我们上面说了先不考虑这种情况。 -
更新
。有两种可能: 之前就属于一条路径的中间结点; 之前属于一条路径的一端,现在 又延长上来。这是第一种情况的转移方程。
这是第二种情况的转移方程。注意观察:
,为什么是 ?因为最终是 条路径, 之前的路径现在和 的路径合并了,减少了一条。减少一条后是 条路径,说明之前应有 条路径。这里同样也省略了一种让
自己作为一条退化链的情况。 -
让
自己作为一条退化链的情况。为什么最后处理这种情况?
如果将这种情况设为初值(或者在循环中更新这种情况),在更新
的第二种情况时,我们会认为 在 "之前的退化链 加上 延展过来的路径" 这构成的新路径的中间。但显然 应该是这条路径的一端。因此我们最后处理这种情况。
自己作为一条退化链,要求了 一开始就不能有任何路径涉及到它,所以这种情况都根据 转移而来。 -
更新
。
以上就是转移方程的所有。
在实现时还有诸多细节需要注意。例如转移方程(就和大多数树形 DP 一样)会自己更新自己,如果不采取在循环顺序上的技巧,就要额外开数组保存原本的信息以避免重复更新。
如果使用倒序循环避免重复更新,还要特别注意一个地方:更新
复杂度和树形背包一样是
点击查看代码
#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;
}
【优化】
复杂度是
感性理解一下,因为割掉每一条边都是独立的,所以我们一定会优先割掉能使答案增加最多/减少最少的边。对应到图像上,不可能有 "凹" 的部分,否则将方案调整可以更优。
因此可以 wqs 二分优化。
wqs 二分优化 DP 的题目,除了计算最优值,往往还需要计算最优值是取了多少个(或者说最多/最少取多少个),因此建议把 wqs 二分的 DP 状态定义成一个结构体,用重载运算符的方式写。
【wqs 二分的经典坑点】
想清楚二分结束后要选用的是
其实这里唯一会影响到的情况只有一种:就是
如果我们二分到相等的斜率
所以最后调用的是
【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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!