洛谷题单指南-图论之树-P1099 [NOIP 2007 提高组] 树网的核
原题链接:https://www.luogu.com.cn/problem/P1099
题意解读:计算树中其他点与树的直径上一段长度不超过s的路径上最近的点的最大距离的最小值。
解题思路:
在思考此题之前,需要了解树的直径,先来做这道题:https://www.luogu.com.cn/problem/B4016
树的直径详解
1、定义
树的直径是指树中任意两个节点之间最长的路径长度,这条路径也被称为树的最长链。在一棵树中,可能存在多条直径,但它们的长度是相等的。
2、性质
中点性质:树的直径的中点(如果直径长度为偶数,则是中间两个节点;如果为奇数,则是中间一个节点)是树的重心之一(当树有两个重心时,直径中点刚好是这两个重心)。
端点性质:从树的任意一个节点出发,找到距离它最远的节点 u,再从节点 u 出发,找到距离 u 最远的节点 v,那么节点 u 到节点 v 的路径就是树的一条直径。
3、求解方法
主要有两种常见的方法来求解树的直径:
a、两次深度优先搜索(DFS)或广度优先搜索(BFS)
从树中任意一个节点 s 开始进行 DFS 或 BFS,找到距离 s 最远的节点 u。
再从节点 u 开始进行 DFS 或 BFS,找到距离 u 最远的节点 v。节点 u 到节点 v 的路径就是树的一条直径,直径的长度就是两次搜索中记录的最大距离。
注意:如果图中存在负权边,两次DFS法将无法应对。
b、树形动态规划(DP)
对于树中的每个节点 u,计算以 u 为根的子树中,从 u 出发的最长路径 d1[u] 和次长路径 d2[u]。
树的直径就是所有节点 u 的 d1[u] + d2[u] 中的最大值。
100分代码-B4016-两次DFS法:(其中注释内容为输出直径路径的代码)
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
vector<int> g[N];
int n;
int st; //搜索的起点
int ed; //距离起点最远距离的点
int maxdist; //距离起点最远的距离
int from[N]; //记录路径
void dfs(int u, int parent, int depth)
{
if(depth > maxdist)
{
ed = u;
maxdist = depth;
//from[u] = parent; 记录路径
}
for(auto v : g[u])
{
if(v == parent) continue;
dfs(v, u, depth + 1);
}
}
int main()
{
cin >> n;
int u, v;
for(int i = 1; i <= n; i++)
{
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
st = 1;
dfs(st, 0, 0); //从1开始搜索路径最长的节点
maxdist = 0;
st = ed;
//memset(from, 0, sizeof(from)); 清空路径
dfs(st, 0, 0); //搜索直径长度
cout << maxdist << endl;
/*
输出直径上的点
do
{
cout << ed << " ";
ed = from[ed];
} while(ed != 0);
*/
return 0;
}
100分代码-B4016-树形DP法:(其中注释内容为输出直径路径的代码)
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
vector<int> g[N];
int dist1[N]; //dist1[u]表示以u为根的子树中,以u为起点的最长路径长度
int dist2[N]; //dist2[u]表示以u为根的子树中,以u为起点的次长路径长度
//int to1[N]; //to1[u]表示以u为根的最长路中,u的下一个节点
//int to2[N]; //to2[u]表示以u为根的次长路中,u的下一个节点
int ans; //直径长度
//int pos; //直径所在树的根
int n;
void dfs(int u, int parent)
{
dist1[u] = dist2[u] = 0;
for(auto v : g[u])
{
if(v == parent) continue;
dfs(v, u);
int t = dist1[v] + 1;
if(t > dist1[u])
{
dist2[u] = dist1[u];
//to2[u] = to1[u];
dist1[u] = t;
//to1[u] = v;
}
else if(t > dist2[u])
{
dist2[u] = t;
//to2[u] = v;
}
}
if(dist1[u] + dist2[u] > ans)
{
ans = dist1[u] + dist2[u];
//pos = u;
}
}
//输出直径的点
/*
void showpath()
{
vector<int> path;
int t = pos;
while(t != 0)
{
path.push_back(t);
t = to1[t];
}
reverse(path.begin(), path.end());
t = to2[pos];
while(t != 0)
{
path.push_back(t);
t = to2[t];
}
for(auto v : path) cout << v << " ";
cout << endl;
}
*/
int main()
{
cin >> n;
int u, v;
for(int i = 1; i < n; i++)
{
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
cout << ans << endl;
//showpath();
return 0;
}
回到本题,要计算与直径上一段路径的距离最大值,而直径可能不止一条,需要证明两个性质:
性质1:如果树有多条直径,直径一定相交
证明:反证法。假设有两条直径,且直径不相交,则有如下情况:
a+b = c+d,分别为两条直径,x是两条直径之间的通路(因为树一定连通),设a<=b,c<=d,a小于等于直径的一半,d大于等于直径的一半,一定有d>=a,那么一定有b+x+d >= b+x+a > a + b,那么就找到一条比a+b更长的路径,产生矛盾,所以直径一定相交,也就是多条直径一定有公共的一段。
性质2:到树网的核的偏心距的最小值一定在多条直径相交的区域
证明:设a+x+b = c+x+d,是两条直径,并且a<=b, c<=d
先来证明a一定等于c,b一定等于d。反证法,假设a!=c,设a<c,那么一定有c+x+b>a+x+b,与直径是最长路径产生矛盾,所以a=c,b=d。
再来证明核在x区域才会带来最小的偏心距。
假设核在a上,偏心距(到直径上一段路径的距离最大值)主要看三部分:核两端与直径两端的距离,核中间分支出来的最远距离,a中红色分支出来的最远距离一定不会超过到直径端点的距离,否则从分支走路径更长,因此核如果在a上,偏心距一定是到核的端点到直径两端点的距离最大值。同理,如果核在x上,偏心距也看三部分,但是核的两端点与直径两端点距离较大值一定比核在a上的最大值要小,也就是核在x上对答案的贡献是最优的。
因此,无论取哪条直径来取核,对答案最优贡献都在公共相交区域,这样一来,任取一条直径来枚举核就可以。
由于核这段路径在直径上,有两种情况:
1、<=s的路径存在,必须是一段路径,路径以外的点与路径的距离又分两种情况:
第一种:路径两端点显然距离直径两端点最远,因为路径在直径上
第二种:路径除两端点之外中间的点能到的最远距离,就是路径中间形成分支的部分,可以把直径上所有点经过非直径能到的最远距离通过dfs预处理出来
对于距离一段路径的最大值,可以结合上面两种情况综合计算。
2、<=s的路径不存在,是直径上的每个点,每个点能到的最远距离必然是直径上的两端点
因此,可以分三步走:
1、寻找直径,将直径中的点、直径每段长度分别保存
2、判断<=s的路径是否存在,如果存在则枚举所有符合要求的路径,计算路径两端点以及路径中间点的最大偏心距,更新答案
100分代码-P1099:
#include <bits/stdc++.h>
using namespace std;
const int N = 305;
int g[N][N];
int n, s;
int st, ed, maxdist; //起始点,结束点,最大距离
int fa[N]; //父节点
int diamond[N], cnt1; //直径中所有点
int section[N], cnt2, minw = 1e9; //直径中每一段的长度,最小的一段长度
int vis[N]; //对直径中点进行标记
int maxoffset[N]; //直径上的点沿着非直径能到的最大距离
int offset; //偏心距
int ans;
//计算以u为根的子树,从u能到的最远的点
void dfs1(int u, int parent, int dist)
{
fa[u] = parent; //记录路径
if(dist > maxdist)
{
maxdist = dist;
ed = u;
}
for(int v = 1; v <= N; v++)
{
if(g[u][v] == -1 || v == parent) continue;
dfs1(v, u, dist + g[u][v]);
}
}
//计算从u开始,不经过直径上的点,能到的最远距离
void dfs2(int u, int parent, int dist)
{
offset = max(offset, dist);
for(int v = 1; v <= N; v++)
{
if(g[u][v] == -1 || vis[v] || v == parent) continue;
dfs2(v, u, dist + g[u][v]);
}
}
int main()
{
memset(g, -1, sizeof(g));
cin >> n >> s;
int u, v, w;
for(int i = 1; i < n; i++)
{
cin >> u >> v >> w;
g[u][v] = g[v][u] = w;
}
st = 1;
dfs1(st, 0, 0);
st= ed, maxdist = 0;
dfs1(st, 0, 0);
int t = ed;
while(t != 0)
{
vis[t] = true; //标记直径上的点
diamond[++cnt1] = t; //记录直径中的点
if(fa[t] != 0) section[++cnt2] = g[fa[t]][t]; //记录直径的每一段长度
minw = min(minw, section[cnt2]);
t = fa[t];
}
//直径上的点沿着非直径能到的最大距离
for(int i = 1; i <= cnt1; i++)
{
offset = 0;
dfs2(diamond[i], 0, 0);
maxoffset[diamond[i]] = offset;
}
ans = maxdist;
if(minw <= s) //树网的核是直径上的一段
{
int left = 0, right = 0;
for(int i = 1; i <= cnt2; i++)
{
int center = 0;
left += section[i - 1]; //i之前所有段的长度和
int maxo = 0;
for(int j = i; j <= cnt2; j++)
{
center += section[j]; //i~j的长度和
if(j > i) maxo = max(maxo, maxoffset[diamond[j]]); //i~j一段中间的点沿着非直径能到的最远距离,不包括两边端点
if(center > s) break;
right = maxdist - left - center; //j之后的长度和
ans = min(ans, max(maxo, max(left, right))); //树网的和是i~j一段的情况
}
}
}
else //树网的核是直径上的某点
{
for(int i = 1; i <= cnt2; i++)
{
int left = 0, right = 0;
for(int i = 1; i <= cnt2; i++)
{
left += section[i - 1];
right = maxdist - left;
ans = min(ans, max(left, right));
}
}
}
cout << ans;
return 0;
}
进一步优化
由于在枚举核的过程复杂度为O(n^2),如果数据范围扩大将无法通过
优化的关键在于贪心+双指针:
贪心1:当枚举核路径时,左端点固定,右端点越远越好,如果右端点远到路径和超过s,左端点再往前移动,因为更多的点参与到计算最小的偏心距,对答案只会更优。
贪心2:不需要额外考虑是取一个点还是取一段区域的情况,只需要枚举所有最长可能的路径(包括一个点的情况),然后在左端点距离直径开头的距离、右端点距离直径结尾的距离、直径所有点分支出来的最远距离的最大值,三者中求最大值即可得到最大偏心距,然后取最小者。因为缩到一个点一定是可能的情况,这样每个点只考虑分支的最远距离,路径区间只考虑距离直径两端点的距离即可。
存储图也改为邻接表。整体复杂度为O(n)
100分代码:https://www.acwing.com/problem/content/description/353/
#include <bits/stdc++.h>
using namespace std;
const int N = 500005;
struct Node
{
int v, w;
};
vector<Node> g[N];
int n, s;
int st, ed, maxdist; //起始点,结束点,最大距离
int fa[N], fd[N]; //父节点,到父节点的距离
int diamond[N], cnt1; //直径中所有点
int section[N], cnt2, minw = 1e9; //直径中每一段的长度,最小的一段长度
int sum[N]; //section的前缀和
int vis[N]; //对直径中点进行标记
int offset, maxoffset; //偏心距, 最大偏心距
int ans;
//计算以u为根的子树,从u能到的最远的点
void dfs1(int u, int parent, int dist)
{
fa[u] = parent; //记录路径
if(dist > maxdist)
{
maxdist = dist;
ed = u;
}
for(auto nd : g[u])
{
int v = nd.v, w = nd.w;
if(v == parent) continue;
fd[v] = w; //记录到父节点距离
dfs1(v, u, dist + w);
}
}
//计算从u开始,不经过直径上的点,能到的最远距离
void dfs2(int u, int parent, int dist)
{
offset = max(offset, dist);
for(auto nd : g[u])
{
int v = nd.v, w = nd.w;
if(v == parent || vis[v]) continue;
dfs2(v, u, dist + w);
}
}
int main()
{
cin >> n >> s;
int u, v, w;
for(int i = 1; i < n; i++)
{
cin >> u >> v >> w;
g[u].push_back({v, w});
g[v].push_back({u, w});
}
st = 1;
dfs1(st, 0, 0);
st= ed, maxdist = 0;
dfs1(st, 0, 0);
int t = ed;
while(t != 0)
{
vis[t] = true; //标记直径上的点
diamond[++cnt1] = t; //记录直径中的点
if(fa[t] != 0)
{
section[++cnt2] = fd[t]; //记录直径的每一段长度
sum[cnt2] = sum[cnt2 - 1] + section[cnt2]; //前缀和
}
minw = min(minw, section[cnt2]);
t = fa[t];
}
//直径上的点沿着非直径能到的最大距离
for(int i = 1; i <= cnt1; i++)
{
offset = 0;
dfs2(diamond[i], 0, 0);
maxoffset = max(maxoffset, offset);
}
ans = maxdist;
for(int i = 0, j = 0; i <= cnt2; i++) //i尽可能往后走
{
while(sum[i] - sum[j] > s) j++; //如果j~i距离超过s,j再往前,确保
int left = sum[j];
int right = sum[cnt2] - sum[i];
ans = min(ans, max(maxoffset, max(left, right)));
}
cout << ans;
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
2024-03-04 洛谷题单指南-搜索-P1443 马的遍历
2024-03-04 洛谷题单指南-搜索-P2392 kkksc03考前临时抱佛脚
2024-03-04 洛谷题单指南-搜索-P1219 [USACO1.5] 八皇后 Checker Challenge
2024-03-04 洛谷题单指南-二分查找与二分答案-P3743 kotori的设备