[POI2015] MOD 题解
前言
题目链接:洛谷。
题意简述
给定一棵树,求断掉一条边再连上一条边所得的新树直径最小值和最大值,以及相应方案(你可以不进行任何操作,即断掉并连上同一条边)。
题目分析
假设我们枚举断掉某一条边,得到了两棵树,并且知道它们的直径分别为
- 最大:显然,将两棵树的直径首尾相接,得到的直径是最大的,新树的直径长度是
。别忘了新加的这条边的贡献 。 - 最小:和 HXY 造公园 里的思想一样,我们将两个树的直径的中点相连(或者没有中点时取直径中心相邻的那两点任一),得到的新直径长度是
。别忘了新加的这条边的贡献 。
可是,我们这样只能知道答案直径的长度,以及断掉哪条边,那怎么知道断边之后连接哪两个点呢?如果在 恐怖的
1. 树形 DP
钦定原树以
- 原先
方向上的直径。 兄弟子树中的直径。- 取
的两个兄弟(如果存在) 和 ,以及分别在以 为根的子树中和以 为根的子树中取出一条链 和 ,组成的新链 。 - 取
方向连向 的一条链 。选取 的兄弟 ,以及 子树中一条链 。两条链拼接组成的新链 。
显然,以上分析囊括了不越过
对于第二点,想到记
对于第三种情况,想到记
对于第四种情况,我们需要知道
分析结束,具体使用代码实现就是两遍 DFS,第一遍预处理出
2. 在原直径上 DP
假设在想到断掉一条边后,我们没有往树形 DP 的方向思考,而是想到了如下结论:
结论一:如果要获得直径的最小值,把原直径断开一定不劣。
证明:
设原树直径为。如果没有断开原直径,那么答案 一定有 ,而我们断开连接同一条边获得的答案就是原树直径 显然不劣。所以为了得到更优的答案,就必须要把原树直径断开。
结论二:如果要获得直径的最大值,只可能是断开直径或者断开和直径连接的边
证明:
方案分为两种,即断开直径或者不断开直径。如果不断开直径,我们就需要和直径分离的那棵树直径最长,所以此时有删除和直径连接的这条边不劣。这是因为考虑直径上一个点和与其相连的不在直径上的儿子 ,如果断开的边在 这棵子树里,获得了一条直径,那么这条直径同样在断开连接 和 这条边后 的子树里,故删除和直径连接的这条边不劣
有了如上两个结论,实现方法呼之欲出。考虑先将原树直径“拉下来”,树的其他部分“挂”在这条直径上(详见下图),发现树上的问题变成了一个类似序列上的问题,简单了许多。从右向左遍历直径上相邻的点对
接下来考虑从右向左枚举断边从
- 原来
右边的直径。 - 以
为根的不经过原树直径的直径。 - 对于
一个儿子 和它子树里以 为一个端点的链 ,以及 右边延伸过来的一条链 组成的直径 。
可以借助下图进行形象地理解。
对于第二点,发现可以和求左树直径一样用同一个 DFS 预处理出来。
对于第三点,我们只用记
分析结束,完成了对本题的求解。具体使用一遍深搜把原树直径“拉下来”再反着枚举断边,同时更新
代码及具体实现(已略去快读快写,码风清新,注释详尽)
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; }
本文作者:XuYueming,转载请注明原文链接:https://www.cnblogs.com/XuYueming/p/18073697。
若未作特殊说明,本作品采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 为DeepSeek添加本地知识库
· 精选4款基于.NET开源、功能强大的通讯调试工具
· DeepSeek智能编程
· 大模型工具KTransformer的安装
· [计算机/硬件/GPU] 显卡