一些树上问题的解题报告
树的直径问题
luogu P3304 直径
分析
板子题,可以证明所有的直径都经过的边是几条连续的边
那么只需要找出任意一条直径,对于直径上的任意一个点,以这个点作为根节点做一次dfs
如果以该点为起点的路径(不经过原直径上的点)最长长度等于它到原直径某一段端点的长度,就说明从该点到这一端点的这一段都不是必须被经过的。
假设从直径左端到右端遍历,那么在遍历过程中找到的第一个满足 路径最长长度等于到直径右端端点长度 的点,
从该点到最右端之间的边都不是必须经过的;
而在遍历过程中找到的路径最长长度等于到直径左端端点长度的点,只能保证当前节点之前的路径是不必须经过的。
下面证明所有的直径都经过的边必然连续:
假设这样的边不连续,那么不妨在脑海里画一个图,本来是一条直径,脑海里画一条直线,根据假设,所有直径都经过的边不连续,
不妨把这条直径中间拉开,使得这条直径分为上下两边,必然满足假设的情况,然而这样必然成环,与树的结构矛盾。
代码
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
#define rg register
#define IOS ios::sync_with_stdio(false);cin.tie(NULL),cout.tie(NULL)
/*inline int read(){
rg int x = 0, f = 1;
rg char c = getchar();
while (c < '0' || c > '9') {if (c == '-') f = -1; c = getchar();}
while (c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
return x * f;
}*/
#define ll long long
const int N = 2e5 + 7;
struct Edge{
int to, val, next;
}e[N << 1];
int head[N], tot;
inline void add(int u, int v, int w){
e[++tot].to = v, e[tot].val = w, e[tot].next = head[u];
head[u] = tot;
}
int pathleft, pathright, templ;
ll maxd, dep[N];
int f[N];
bool ondis[N];
int ansl, ansr;
void dfs1(int u, int fa){
f[u] = fa;
for (int i(head[u]); i; i = e[i].next){
int w = e[i].val, v = e[i].to;
if (v == fa) continue;
dep[v] = dep[u] + w;
if (dep[v] > maxd){
maxd = dep[v];
templ = v;
}
dfs1(v, u);
}
}
void dfs2(int u, int fa){
for (int i(head[u]); i; i = e[i].next){
int w = e[i].val, v = e[i].to;
if (v == fa || ondis[v]) continue;
dep[v] = dep[u] + w;
if (dep[v] > maxd){
maxd = dep[v];
}
dfs2(v, u);
}
}
int main(){
IOS;
//freopen("in.txt", "r", stdin);
int n;
cin >> n;
for (int i(1); i < n; ++i){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
dfs1(1, 0);
pathleft = templ;
dep[pathleft] = 0, maxd = 0;
dfs1(pathleft, 0);
pathright = templ;
ansl = pathleft, ansr = pathright;
cout << maxd << endl;
for (int i(pathright); i; i = f[i]) ondis[i] = true;
bool flag = true;
for (int i(f[pathright]); i != pathleft; i = f[i]){
int leftdis = dep[i], rightdis = dep[pathright] - dep[i];
maxd = dep[i] = 0;
dfs2(i, 0);
if (maxd == rightdis) ansr = i;
else if (maxd == leftdis && flag) flag = false, ansl = i;
}
int ans = 0;
for (int i(ansr); i != ansl; i = f[i]) ++ans;
cout << ans;
return 0;
}
luogu P5536 核心城市
分析
考虑核心城市为1的情况,要使得答案最小,显然核心城市选在直径的中点满足条件。
再考虑座核心城市,实际上直径中点仍是第一首选。现在我们要考虑的是从剩下的点中选出座核心城市。
考虑贪心,用每个点能达到的最大深度减去该点深度即可得到它作为核心城市对答案的贡献,对其排序,去掉前大的点即可。
以直径中点为根dfs,记录每个点自身的深度以及能达到的最大深度。
代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
#define rg register
#define IOS ios::sync_with_stdio(false);cin.tie(NULL),cout.tie(NULL)
/*inline int read(){
rg int x = 0, f = 1;
rg char c = getchar();
while (c < '0' || c > '9') {if (c == '-') f = -1; c = getchar();}
while (c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
return x * f;
}*/
const int N = 1e5 + 7;
vector<vector<int> > G(N + 1);
int f[N];
int dep[N], templ, disleft, disright, maxd, dismid, ans[N];
void dfs1(int u, int fa){
f[u] = fa;
for (auto v: G[u]){
if (v == fa) continue;
dep[v] = dep[u] + 1;
if (dep[v] > maxd){
maxd = dep[v];
templ = v;
}
dfs1(v, u);
}
}
int maxdep[N];
void dfsdep(int u, int fa){
for (auto v: G[u]){
if (v == fa) continue;
dep[v] = dep[u] + 1;
maxdep[v] = dep[v];
dfsdep(v, u);
maxdep[u] = max(maxdep[u], maxdep[v]);
}
}
int main(){
IOS;
//freopen("in.txt", "r", stdin);
int n, k;
cin >> n >> k;
for (int i(1); i < n; ++i){
int u, v;
cin >> u >> v;
G[u].push_back(v), G[v].push_back(u);
}
dfs1(1, 0);
disleft = templ;
maxd = dep[disleft] = 0;
dfs1(disleft, 0);
disright = templ;
dismid = disright;
for (int i(1); i <= (dep[disright] + 1) / 2; ++i) dismid = f[dismid];
dfsdep(dismid, 0);
for (int i(1); i <= n; ++i) ans[i] = dep[i] - maxdep[i];
sort(ans + 1, ans + 1 + n);
int ansk = 0;
for (int i(k + 1); i <= n; ++i) ansk = max(ansk, 1 - ans[i]);
cout << ansk;
return 0;
}
luogu P4408 逃学的小孩
分析
是必然经过的一段,不难想到为直径时贡献最大。
枚举C即可。找到直径后记录两个端点到各个点的距离,枚举点时便于统计。
代码
#include <iostream>
#include <cstdio>
using namespace std;
#define rg register
#define IOS ios::sync_with_stdio(false);cin.tie(NULL),cout.tie(NULL)
/*inline int read(){
rg int x = 0, f = 1;
rg char c = getchar();
while (c < '0' || c > '9') {if (c == '-') f = -1; c = getchar();}
while (c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
return x * f;
}*/
#define ll long long
const int N = 2e5 + 7;
struct Edge{
int to, val, next;
}e[N << 1];
int head[N], tot;
inline void add(int u, int v, int w){
e[++tot].to = v, e[tot].val = w, e[tot].next = head[u];
head[u] = tot;
}
int pathleft, pathright, templ;
ll maxd, dep1[N], dep2[N];
int f[N];
bool ondis[N];
int ansl, ansr;
void dfs1(int u, int fa){
f[u] = fa;
for (int i(head[u]); i; i = e[i].next){
int w = e[i].val, v = e[i].to;
if (v == fa) continue;
dep1[v] = dep1[u] + w;
if (dep1[v] > maxd){
maxd = dep1[v];
templ = v;
}
dfs1(v, u);
}
}
void dfs2(int u, int fa){
f[u] = fa;
for (int i(head[u]); i; i = e[i].next){
int w = e[i].val, v = e[i].to;
if (v == fa) continue;
dep2[v] = dep2[u] + w;
if (dep2[v] > maxd){
maxd = dep2[v];
templ = v;
}
dfs2(v, u);
}
}
int main(){
IOS;
//freopen("in.txt", "r", stdin);
int n, m;
cin >> n >> m;
for (int i(1); i <= m; ++i){
int u, v, w;
cin >> u >> v >> w;
add(u, v, w), add(v, u, w);
}
dfs1(1, 0);
pathleft = templ;
dep1[pathleft] = maxd = 0;
dfs1(pathleft, 0);
pathright = templ;
maxd = 0;
dfs2(pathright, 0);
ll ans = 0;
for (int i(1); i <= n; ++i){
if (i == pathleft || i == pathright) continue;
ans = max(ans, dep1[pathright] + min(dep1[i], dep2[i]));
}
cout << ans;
return 0;
}
luogu P3629 巡逻
分析
先考虑答案,如果没有加边每条边都要经过两次,即 ,加边必然成环,
那么环上所有的边只需要经过一次即可,相当于在原来的基础上减去了这个环加边之前的长度,再加上加的那条边。
考虑 的情况,只要满足这个环加边之前的长度尽可能长,显然想到直径 ,答案就是 。
再考虑 的情况,在基于上述情况下,可能有人选择在图上加边,但是题目中有要求到新加的边只能经过一次,所以加边实际上没有意义。
新加的边也会成环,那么新环和旧环存在两种位置关系,一种是有重叠边,一种是没有重叠边;
没有重叠边的情况就是再找一遍直径,相当于再跑一次 的情况即可;
有重叠边的情况(重叠边必不可能是所加的边,上面有提到),那么第一次加边时重叠边给答案的贡献就没有了,因为还是要走一遍
那么不妨在 的处理基础上,只需要再减去重叠边即可。不妨将第一次处理的直径边权全部改为 ,再用树上dp找一个直径长度
再套用 情况下的答案统计方式即可。
代码
#include <iostream>
#include <cstdio>
using namespace std;
#define rg register
#define IOS ios::sync_with_stdio(false);cin.tie(NULL),cout.tie(NULL)
/*inline int read(){
rg int x = 0, f = 1;
rg char c = getchar();
while (c < '0' || c > '9') {if (c == '-') f = -1; c = getchar();}
while (c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
return x * f;
}*/
const int N = 1e5 + 7;
struct Edge{
int to, val, next;
}e[N << 1];
int head[N], cnt;
inline void add(int u, int v ,int w){
e[++cnt].to = v, e[cnt].val = w, e[cnt].next = head[u];
head[u] = cnt;
}
int f[N], dis[N], maxd, templ;
bool ondia[N];
void dfs(int u, int fa){
f[u] = fa;
for (int i(head[u]); i; i = e[i].next){
int v = e[i].to;
if (v == fa) continue;
dis[v] = dis[u] + 1;
if (dis[v] > maxd){
templ = v;
maxd = dis[v];
}
dfs(v, u);
}
}
int d1[N], d2[N];
void treedp(int u, int fa){
d1[u] = d2[u] = 0;
for (int i(head[u]); i; i = e[i].next){
int v = e[i].to;
if (v == fa) continue;
if (ondia[u] && ondia[v]) e[i].val = -1;
treedp(v, u);
int t = d1[v] + e[i].val;
if (t > d1[u])
d2[u] = d1[u], d1[u] = t;
else if (t > d2[u])
d2[u] = t;
}
maxd = max(maxd, d1[u] + d2[u]);
}
int main(){
IOS;
//freopen("in.txt", "r", stdin);
int n, k; cin >> n >> k;
int ans = 0;
for (int i(1); i < n; ++i){
int a, b; cin >> a >> b;
add(a, b, 1), add(b, a, 1);
}
dfs(1, 0);
int dialeft = templ;
dis[dialeft] = 0; maxd = 0;
dfs(dialeft, 0);
int diaright = templ;
//cout << dialeft << endl << diaright << endl;
ans = 2 * (n - 1) - (maxd - 1);
if (k == 1){
cout << 2 * (n - 1) - maxd + 1;
return 0;
}
for (int i(diaright); i != dialeft; i = f[i])
ondia[i] = true;
ondia[dialeft] = true;
//add(dialeft, diaright, -1), add(diaright, dialeft, -1);
maxd = 0;
treedp(1, 0);
cout << ans - (maxd - 1);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)