[POI2015] MOD 题解

前言

题目链接:洛谷

题意简述

给定一棵树,求断掉一条边再连上一条边所得的新树直径最小值和最大值,以及相应方案(你可以不进行任何操作,即断掉并连上同一条边)。

题目分析

假设我们枚举断掉某一条边,得到了两棵树,并且知道它们的直径分别为 d0,d1,那么如何连接一条边让新树的直径最大 / 最小呢?

  1. 最大:显然,将两棵树的直径首尾相接,得到的直径是最大的,新树的直径长度是 d=d0+d1+1。别忘了新加的这条边的贡献 1
  2. 最小:和 HXY 造公园 里的思想一样,我们将两个树的直径的中点相连(或者没有中点时取直径中心相邻的那两点任一),得到的新直径长度是 d=max{d0,d1,d02+d12+1}。别忘了新加的这条边的贡献 1

可是,我们这样只能知道答案直径的长度,以及断掉哪条边,那怎么知道断边之后连接哪两个点呢?如果在 Θ(n) 枚举断边的同时把两棵树的直径求出来时间复杂度是 恐怖的 Θ(n2),显然超时。如何优化呢?事实上,我们完全不用每得到一个可能的答案就算出其具体方案,而是留到最后再处理,处理方法随便一个 Θ(n) 求直径的方法都行。这样,总体的时间复杂度就是 Θ(n) 的。于是,问题变成给出断开的边,如何求两颗树的直径长度。在这里提供了两种方法 Θ(n) 地求解此题。

1. 树形 DP

钦定原树以 1 为根结点。枚举断边可以使用深搜,那么我们就需要在搜索的时候快速求得以 u 为根的子树的直径长度以及 fa[u] 这个方向上的直径长度。于是我们想到了使用树形 DP 求解。前者是树形 DP 求直径的模板,可以用一遍深搜预处理出来。考虑如何换根求得后者。在根从 fa[u] 变成 u 的时候,发现 fa[u] 这个方向上的树多出了 u 的兄弟子树,那么可能构成直径的分为以下几个部分。

  • 原先 fa[fa[u]] 方向上的直径。
  • u 兄弟子树中的直径。
  • u 的两个兄弟(如果存在)xy,以及分别在以 x 为根的子树中和以 y 为根的子树中取出一条链 xxyy,组成的新链 xxfa[u]yy
  • fa[fa[u]] 方向连向 fa[u] 的一条链 pfa[u]。选取 u 的兄弟 x,以及 x 子树中一条链 xx。两条链拼接组成的新链 pfa[u]xx

显然,以上分析囊括了不越过 fa[u] 和越过 fa[u] 的所有可能情况,不存在漏解。为了帮助理解,可以参考下图。

对于第二点,想到记 wi 表示 i 所有子树中最长的直径,那么第二点直径长度就是 wfa[u],但是请注意,我们要的是 u 的兄弟子树而不包括 u 这棵子树,万一 wfa[u] 正好是 u 这棵子树中的直径就出现了问题。所以,套路化地,我们给 w 多加一维,变为 wi,0/1 表示以 i 的所有子树中最长的直径 / 次长的直径。这样,对于上文提到的情况,就使用 wfa[u],1 来转移就没有问题。

对于第三种情况,想到记 di,0/1 表示 i 所有子树中,根节点连出的最长链和次长链的长度。那么对于一般情况,合并后的直径长度就是 dfa[u],0 + dfa[u],1。套路化地,发现当 u 这棵子树贡献了最长链或者次长链会产生问题,所以需要再开一维,记 di,0/1/2 表示 i 所有子树中,根节点连出的最长链、次长链和次次长链的长度。转移的时候注意不要使用到 u 这棵子树产生的信息就可以了。

对于第四种情况,我们需要知道 fa[u]fa[fa[u]] 方向上最长链的长度,这个假设已经求得,为 chainfa[u]。和 u 兄弟子树中根节点连出的最长链的长度,发现就是上文求的 dfa[u],0,当 u 这棵子树存在最长链的时候是 dfa[u],1。那么合并后的直径长度就是 chainfa[u]+dfa[u],0/1。考虑如何使用信息更新 chainu。首先,可能新的链是 chainfa[u] 的基础上连上了 fa[u]u 这条边,长度是 chainfa[u]+1。其次可能是 u 兄弟子树连过来的一条边,长度是 dfa[u],0/1+1,这个要根据 u 是否是最长链分类讨论。两者合并,得到 chainu=max{chainfa[u]+1,dfa[u],0/1+1}

分析结束,具体使用代码实现就是两遍 DFS,第一遍预处理出 u 子树中直径长度 fudu,0/1/2wu,0/1。第二遍使用信息更新 fa[u] 方向上的直径 guchainu,同时更新答案即可。具体实现和细节见代码。

2. 在原直径上 DP

假设在想到断掉一条边后,我们没有往树形 DP 的方向思考,而是想到了如下结论:

结论一:如果要获得直径的最小值,把原直径断开一定不劣。

证明:
设原树直径为 d。如果没有断开原直径,那么答案 D=max{d,l,d2+l2+1} 一定有 Dd,而我们断开连接同一条边获得的答案就是原树直径 d 显然不劣。所以为了得到更优的答案,就必须要把原树直径断开。

结论二:如果要获得直径的最大值,只可能是断开直径或者断开和直径连接的边

证明:
方案分为两种,即断开直径或者不断开直径。如果不断开直径,我们就需要和直径分离的那棵树直径最长,所以此时有删除和直径连接的这条边不劣。这是因为考虑直径上一个点 u 和与其相连的不在直径上的儿子 v,如果断开的边在 v 这棵子树里,获得了一条直径,那么这条直径同样在断开连接 uv 这条边后 v 的子树里,故删除和直径连接的这条边不劣

有了如上两个结论,实现方法呼之欲出。考虑先将原树直径“拉下来”,树的其他部分“挂”在这条直径上(详见下图),发现树上的问题变成了一个类似序列上的问题,简单了许多。从右向左遍历直径上相邻的点对 (u,v),删除他们之间的边,快速求得 u 这边和 v 这边树的直径,然后统计答案。对于断开和直径相连的边,暴力枚举时间复杂度不超过 Θ(n),问题就得到解决。

接下来考虑从右向左枚举断边从 (v,y) 变为 (u,v) 的过程,两树直径变化。首先对于左树的直径我们可以预处理出来,那么只需要考虑多出的 y 以及它的不在直径上的子树对右半部分直径产生的贡献,和前文树形 DP 讨论方法类似,分为不经过 y 和经过 y 的直径,具体如下:

  • 原来 y 右边的直径。
  • y 为根的不经过原树直径的直径。
  • 对于 y 一个儿子 yzh 和它子树里以 yzh 为一个端点的链 yzhyzh,以及 y 右边延伸过来的一条链 py 组成的直径 yzhyzhyp

可以借助下图进行形象地理解。

对于第二点,发现可以和求左树直径一样用同一个 DFS 预处理出来。

对于第三点,我们只用记 y 连出的不经过原直径最长链的长度 fy,和右边伸过来的链 Rlen 和并得 fy+Rlen(你看看次长、次次长都不见了)。那么,我们怎么算得对于 vRlen 呢?发现可以是目前链再向左延伸或者是 y 中一条长链连过来,故 Rlen=max{Rlen+1,fy+1}

分析结束,完成了对本题的求解。具体使用一遍深搜把原树直径“拉下来”再反着枚举断边,同时更新 Rlen。随后枚举断于直径相连的边。最后分别求出答案要求的连接哪些边。具体实现和细节见代码。

代码及具体实现(已略去快读快写,码风清新,注释详尽)

1. 树形 DP 目前最优解 rank3

//#pragma GCC optimize(3)
//#pragma GCC optimize("Ofast", "inline", "-ffast-math")
//#pragma GCC target("avx", "sse2", "sse3", "sse4", "mmx")
#include <iostream>
#include <cstdio>
#define debug(a) cerr << "Line: " << __LINE__ << " " << #a << endl
#define print(a) cerr << #a << "=" << (a) << endl
#define file(a) freopen(#a".in","r",stdin), freopen(#a".out","w",stdout)
#define main Main(); signed main(){ return ios::sync_with_stdio(0), cin.tie(0), Main(); } signed Main
using namespace std;
struct node{
int to, nxt;
} edge[500010 << 1];
int eid, head[500010];
void add(int u, int v){
edge[++eid] = node({v, head[u]});
head[u] = eid;
}
int n;
int kmin = 0x3f3f3f3f, x1min, y1min, x2min, y2min;
int kmax = -0x3f3f3f3f, x1max, y1max, x2max, y2max;
// f[i] 表示以 i 为子树的直径长度
// d[i][0/1/2] 表示表示 i 向其子树连出的最长链、次长链、次次长链的长度
// w[i][0/1] 表示 i 所有子树中的最长直径(也就是不跨过 i 的最长直径)
// chain[i] 表示 fa[i] 方向连过来的最长链的长度
int f[500010], d[500010][3], w[500010][2], chain[500010];
void Dfs(int now, int fa){
for (int i = head[now]; i; i = edge[i].nxt){
int to = edge[i].to;
if (to == fa) continue;
Dfs(to, now);
f[now] = max<int, int, int>(f[now], f[to], d[now][0] + d[to][0] + 1);
// 树形 DP 求直径
if (d[to][0] + 1 > d[now][0]) d[now][2] = d[now][1], d[now][1] = d[now][0], d[now][0] = d[to][0] + 1;
else if (d[to][0] + 1 > d[now][1]) d[now][2] = d[now][1], d[now][1] = d[to][0] + 1;
else if (d[to][0] + 1 > d[now][2]) d[now][2] = d[to][0] + 1;
// d[to][0] + 1 就是 now 向 to 连出的最长链的长度,用其更新 now 的最长链、次长链、次次长链的长度
// 如果有两条相同的最长链,我们把一个看做次长链,就避免了冗长的分类讨论
if (f[to] > w[now][0]) w[now][1] = w[now][0], w[now][0] = f[to];
else if (f[to] > w[now][1]) w[now][1] = f[to];
// 更新 i 所有子树中的最长直径
}
}
int g[500010];
void redfs(int now, int fa){
if (fa != 0){
// 不是根节点就尝试断开 now 和 fa 之间的边
if (kmax < g[now] + f[now] + 1) kmax = g[now] + f[now] + 1, x1max = fa, y1max = now;
// 求最长直径
int len = max<int, int, int>(f[now], g[now], (f[now] + 1) / 2 + (g[now] + 1) / 2 + 1);
if (kmin > len) kmin = len, x1min = fa, y1min = now;
// 求最短直径
}
for (int i = head[now]; i; i = edge[i].nxt){
int to = edge[i].to;
if (to == fa) continue;
chain[to] = chain[now] + 1; // 新的链是 fa[now] -> now 的基础上连上了 now -> to
g[to] = g[now]; // 对应第一种情况
if (d[to][0] + 1 == d[now][0]){
g[to] = max<int, int, int>(g[to], chain[now] + d[now][1], d[now][1] + d[now][2]);
chain[to] = max(chain[to], d[now][1] + 1);
} else if (d[to][0] + 1 == d[now][1]){
g[to] = max<int, int, int>(g[to], chain[now] + d[now][0], d[now][0] + d[now][2]);
chain[to] = max(chain[to], d[now][0] + 1);
} else {
g[to] = max<int, int, int>(g[to], chain[now] + d[now][0], d[now][0] + d[now][1]);
chain[to] = max(chain[to], d[now][0] + 1);
}
// 判断链长是不是最长链,次长链、次次长链,可以画图辅助理解
if (f[to] == w[now][0]) g[to] = max(g[to], w[now][1]);
else g[to] = max(g[to], w[now][0]);
// 对应第二种情况
redfs(to, now);
}
}
int pre[500010], dis[500010], mxpos;
void dfs(int now, int fa, int skip = -1){
if (dis[now] > dis[mxpos]) mxpos = now;
for (int i = head[now]; i; i = edge[i].nxt){
int to = edge[i].to;
if (to != fa && to != skip){
dis[to] = dis[now] + 1, pre[to] = now;
dfs(to, now, skip);
}
}
}
int Diameter[500010], Dlen;
bool InDiameter[500010];
void GetDiameter(int u = 1, int v = -1){
int p = -1, now = -1;
mxpos = u, dis[u] = 0, pre[u] = -1, dfs(u, 0, v), p = mxpos;
mxpos = p, dis[p] = 0, pre[p] = -1, dfs(p, 0, v), now = mxpos;
for (int i = 1; i <= n; ++i) InDiameter[i] = false;
for (Dlen = 0; ~now; InDiameter[now] = true, Diameter[++Dlen] = now, now = pre[now]);
}
// 搜直径并把直径“拉下来”
int GetNodeOfDiameter(int u = 1, int v = -1){
return mxpos = u, dis[u] = 0, pre[u] = -1, dfs(u, 0, v), mxpos;
}
// 获取直径的一端
signed main(){
read(n);
for (int i = 1, u, v; i <= n - 1; ++i) read(u, v), add(u, v), add(v, u);
Dfs(1, 0), redfs(1, 0);
GetDiameter(x1min, y1min), x2min = Diameter[(Dlen + 1) / 2];
GetDiameter(y1min, x1min), y2min = Diameter[(Dlen + 1) / 2];
x2max = GetNodeOfDiameter(x1max, y1max);
y2max = GetNodeOfDiameter(y1max, x1max);
write(kmin, ' ', x1min, ' ', y1min, ' ', x2min, ' ', y2min, '\n');
write(kmax, ' ', x1max, ' ', y1max, ' ', x2max, ' ', y2max, '\n');
return 0;
}

2. 在原直径上 DP 目前最优解 rank1

//#pragma GCC optimize(3)
//#pragma GCC optimize("Ofast", "inline", "-ffast-math")
//#pragma GCC target("avx", "sse2", "sse3", "sse4", "mmx")
#include <iostream>
#include <cstdio>
#define debug(a) cerr << "Line: " << __LINE__ << " " << #a << endl
#define print(a) cerr << #a << "=" << (a) << endl
#define file(a) freopen(#a".in","r",stdin), freopen(#a".out","w",stdout)
#define main Main(); signed main(){ return ios::sync_with_stdio(0), cin.tie(0), Main(); } signed Main
using namespace std;
struct node{
int to, nxt;
} edge[500010 << 1];
int eid, head[500010];
void add(int u, int v){
edge[++eid] = node({v, head[u]});
head[u] = eid;
}
int n;
int kmin = 0x3f3f3f3f, x1min, y1min, x2min, y2min;
int kmax = -0x3f3f3f3f, x1max, y1max, x2max, y2max;
int pre[500010], dis[500010], mxpos;
void dfs(int now, int fa, int skip = -1){
if (dis[now] > dis[mxpos]) mxpos = now;
for (int i = head[now]; i; i = edge[i].nxt){
int to = edge[i].to;
if (to != fa && to != skip){
dis[to] = dis[now] + 1, pre[to] = now;
dfs(to, now, skip);
}
}
}
int Diameter[500010], Dlen;
bool InDiameter[500010];
void GetDiameter(int u = 1, int v = -1){
int p = -1, now = -1;
mxpos = u, dis[u] = 0, pre[u] = -1, dfs(u, 0, v), p = mxpos;
mxpos = p, dis[p] = 0, pre[p] = -1, dfs(p, 0, v), now = mxpos;
for (int i = 1; i <= n; ++i) InDiameter[i] = false;
for (Dlen = 0; ~now; InDiameter[now] = true, Diameter[++Dlen] = now, now = pre[now]);
}
// 搜直径并把直径“拉下来”
int GetNodeOfDiameter(int u = 1, int v = -1){
return mxpos = u, dis[u] = 0, pre[u] = -1, dfs(u, 0, v), mxpos;
}
// 获取直径的一端
// f[i] 表示 i 向非直径连出的最长链长度
// g[i] 表示 i 子树的直径
int f[500010], g[500010];
void TreeDP(int now, int fa){
for (int i = head[now]; i; i = edge[i].nxt){
int to = edge[i].to;
if (to != fa){
TreeDP(to, now);
if (InDiameter[to]) continue; // 这句话很巧妙地做到了分别以直径上的每个结点往直径外搜索
g[now] = max<int, int, int>(g[now], g[to], f[to] + 1 + f[now]);
f[now] = max(f[now], f[to] + 1);
// 说明 to 不是直径上的结点,更新最长链和直径
}
}
}
int p[500010];
signed main(){
read(n);
for (int i = 1, u, v; i <= n - 1; ++i) read(u, v), add(u, v), add(v, u);
GetDiameter(), TreeDP(Diameter[1], 0);
// 先把直径拉下来
for (int i = 1, now = 0; i <= Dlen; ++i){
// 这里 i 表示把直径拉下来后第 i 个直径结点
// 正着扫,p[i] 表示前缀直径
// 考虑新增部分的贡献,可能直径完整在 i 的的子树里,即 g[Diameter[i]]
// 也可能是之前连向 i 的最长链和 i 向子树连出的最长链
p[i] = max<int, int, int>(p[i - 1], g[Diameter[i]], now + f[Diameter[i]]);
// 这里的 now 就是维护连到 i 的最长链的长度
// 可能是之前那条链再向右延伸,或者是从 i 的子树里连过来
now = max(now + 1, f[Diameter[i]] + 1);
}
// 接下来倒着扫一遍,尝试删除直径上 i - 1 号点和第 i 号点之间的边
// 同样用 Rlen 记录右半部分的直径长度
// 用 now 记录从右边连向 i 的最长链的长度
for (int i = Dlen, Rlen = 0, now = 0; i - 1 >= 1; --i){
Rlen = max<int, int, int>(Rlen, g[Diameter[i]], now + f[Diameter[i]]);
now = max(now + 1, f[Diameter[i]] + 1);
// 同前面的维护
int len = max<int, int, int>(p[i - 1], Rlen, (Rlen + 1) / 2 + (p[i - 1] + 1) / 2 + 1);
if (len < kmin) kmin = len, x1min = Diameter[i], y1min = Diameter[i - 1];
// 维护最小直径
if (Rlen + 1 + p[i - 1] > kmax) kmax = Rlen + 1 + p[i - 1], x1max = Diameter[i], y1max = Diameter[i - 1];
// 维护最长直径
}
// 为了获得最长直径,我们还要把和直径相连的边都尝试断一遍
// 这里直接枚举直径上的点,在枚举它连出的边
for (int i = 1; i <= Dlen; ++i) for (int j = head[Diameter[i]]; j; j = edge[j].nxt){
int to = edge[j].to;
if (!InDiameter[to]){
// 更新最长直径
if (Dlen + g[to] > kmax) kmax = Dlen + g[to], x1max = Diameter[i], y1max = to;
}
}
// 最后求得具体方案
GetDiameter(x1min, y1min), x2min = Diameter[(Dlen + 1) / 2];
GetDiameter(y1min, x1min), y2min = Diameter[(Dlen + 1) / 2];
x2max = GetNodeOfDiameter(x1max, y1max);
y2max = GetNodeOfDiameter(y1max, x1max);
write(kmin, ' ', x1min, ' ', y1min, ' ', x2min, ' ', y2min, '\n');
write(kmax, ' ', x1max, ' ', y1max, ' ', x2max, ' ', y2max, '\n');
return 0;
}
posted @   XuYueming  阅读(20)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 为DeepSeek添加本地知识库
· 精选4款基于.NET开源、功能强大的通讯调试工具
· DeepSeek智能编程
· 大模型工具KTransformer的安装
· [计算机/硬件/GPU] 显卡
点击右上角即可分享
微信分享提示