【题解】P3174 - [HAOI2009] 毛毛虫
题目大意
给出一棵包含 \(n\) 个结点的无根树 \(T\),可以将树 \(T\) 中的一条链种的结点以及与这条链 直接相连 的若干树边的两个端点加入集合 \(S\)。试求集合 \(S\) 中可能的最大元素个数。此处的链定义为树上任意两点之间的路径,如果树中只有一个结点 \(1\),那么此时将结点 \(1\) 连出的边视作结点 \(1\) 的自环,也就是集合 \(S\) 中存在且仅存在结点 \(1\)。
\(1 \leq n \leq 300000\)
解题思路
看到题目为单次不带修查询且需要求最值可以知道这道题大概率使用树形 \(dp\)。首先考虑树形 \(dp\) 的传统状态,设 \(dp_u\) 为以 \(u\) 为根且整条树链在 \(u\) 的子树内的最大集合元素个数。显然我们需要枚举树链的下一个结点。我们选择 \(dp\) 值最大的子结点转移,此时树链子结点及以下的部分以及与下面树链直接相连的树边已经被统计,考虑与结点 \(u\) 直接相连的结点个数,显然为 \(son_u - 1\),其中 \(son_u\) 表示结点 \(u\) 的子结点个数。\(- 1\) 的原因是转移的子结点已经被统计过;所以对于 \((u, v) \in E\),\(dp_u = \max(dp_v) + son_u - 1, (u, v) \in E\)
答案似乎是 \(\forall 1 \leq i \leq n, \max(dp_i)\),实则不然。题目并没有要求树链的端点必须是祖先后代关系,因此我们不能保证整条树链都在某个结点的子树内并且包含该结点。换言之,这条树链是一条树上路径与若干和这条路径直接相连的边取并,而非原树的某一棵子树。
因此我们需要将子树的状态合并。我们可以在遍历整棵树的时候顺便合并答案。对于当前遍历到的结点 \(u\),假设我们正在计算 \(dp_u\)。不妨设 \(f_u = \max(f_v), (u, v) \in E\)。假设当前即将遍历到的结点为 \(v\),显然 \(dp_v\) 还没有贡献 \(f_u\)。此时 \(f_u\) 的意义为当前遍历过的子树内最大的 \(dp\) 值。我们计算出 \(dp_v\),接着进行状态合并。假设存在一条树链,满足这条树链中所有结点的 \(LCA = u\)。我们在遍历结点 \(u\) 的时候统计所有以上树链对答案的贡献。
我们可以形象地理解一下,此时的 dfs
实质上是枚举每一个可能的树链中转点,在枚举的过程中顺便维护 \(f_u\) 和 \(dp_u\)。那么此时因为 \(u\) 是树链内最高的结点,所以树链的两端只能向 \(u\) 的子树内伸展。我们先考虑树链两端向 \(u\) 的不同子树拓展的情况。我们在链式前向星中还顺便枚举了 \(u\) 的子结点 \(v\),我们可以把它视为树链的一端在 \(v\) 的子树内延伸。在最好情况下,\(v\) 子树内的树链长度为 \(dp_v\)。
我们考虑树链的另一端。因为我们维护了 \(f_u\),也就是当前遍历过的子树中最大的树链长度。假如我们仅考虑把 \(v\) 与已经遍历过的子树配对,那么结点 \(u\) 至少有两个子结点的前提下,最好情况下树链的长度为 \(ans = dp_v + f_u + (son_u - 2) + 2\),最后的 \(2\) 指的是结点 \(u\) 以及其父亲,当结点 \(u\) 是根时为 \(1\)。事实上,我们只需要考虑把当前遍历的子树与已经遍历的子树配对。我们实际上是取结点 \(u\) 的子树中最大和次大的树链长度并将它们合并,所以其他子树对答案并没有实质性影响。假设我们先遍历贡献最大值的子树,那么在遍历次大值之前,\(f_u\) 将被贡献最大值的子树占据。当遍历到次大值时,我们取 \(f_u\),也就是最大值与次大值合并答案显然正确。
反之亦然,当我们先遍历贡献次大值的子树时,因为除最大值外没有树链长度比次大值更大,所以在遍历贡献最大值的子树前,\(f_u\) 将会一直被贡献次大值的子树占据。遍历最大值时将次大值与最大值合并,还是合法的。
最后,我们需要考虑当前遍历的结点 \(u\) 只有一个子结点可以转移的情况。回顾结点 \(u\) 有多棵子树时且不为根时的状态转移方程:\(ans = \max(dp_v) + (son_u - 2) + 2\)。这里的 \(dp_v\) 意味转移 \(dp_u\) 的子树大小,只有一棵子树时显然也没有影响。\(2\) 意为结点 \(u\) 以及其父结点,还是没有影响。\(son_u - 2\) 表示减去被重复计算的转移 \(f_u\) 的 两个 子结点。此时因为结点 \(u\) 仅有一个可以转移的子结点,故而需要更改成 \(ans = \max(dp_v) + (son_u - 1) + 2\)。结点 \(u\) 为根结点时同理,末尾改为 \(+ 1\) 即可。
归纳一下状态转移方程。不考虑结点 \(u\) 是否为根和结点 \(u\) 的子结点个数时,\(ans = \max(dp_v + f_u + (son_u - 2) + 2) = \max(dp_v + f_u + son_u)\),若 \(u\) 为根额外 \(+ 1\),若 \(u\) 只有一个子结点可以转移时额外 \(- 1\)。注意当结点 \(u\) 含有多个 子结点,但是转移时 只遍历了一个 子结点时,我们同样要按照结点 \(u\) 只含有一个子结点来转移。状态转移方程用代码可以表示为 ans = max(ans, dp[v] + f[u] + son[u] - (f[u] != 0) + (u != 1))
。\(dp_u = \max(dp_v) + (son_u - 1) + 1 = \max(dp_v) + son_u, f_u = \max(dp_v)\)。
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 3e5 + 5;
const int maxm = 6e5 + 5;
struct node
{
int to, nxt;
} edge[maxm];
int n, m;
int cnt, ans;
int head[maxn], son[maxn];
int f[maxn], dp[maxn];
void add_edge(int u, int v)
{
cnt++;
edge[cnt].to = v;
edge[cnt].nxt = head[u];
head[u] = cnt;
}
void dfs(int u, int fa)
{
ans = dp[u] = 1;
for (int i = head[u]; i; i = edge[i].nxt)
{
int v = edge[i].to;
if (v != fa)
{
son[u]++;
dfs(v, u);
}
}
for (int i = head[u]; i; i = edge[i].nxt)
{
int v = edge[i].to;
if (v != fa)
{
dp[u] = max(dp[u], dp[v] + son[u]);
ans = max(ans, dp[v] + f[u] + son[u] - (f[u] != 0) + (u != 1));
f[u] = max(f[u], dp[v]);
}
}
}
int main()
{
int u, v;
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
scanf("%d%d", &u, &v);
add_edge(u, v);
add_edge(v, u);
}
dfs(1, 0);
printf("%d\n", ans);
return 0;
}