[10.12模拟赛] 老大 (二分/树的直径/树形dp)

时间:2022-12-29 10:13:47

[10.12模拟赛] 老大

题目描述

因为 OB 今年拿下 4 块金牌,学校赞助扩建劳模办公室为劳模办公室群,为了体现 OI 的特色,办公室群被设计成了树形(n 个点 n − 1 条边的无向连通图),由于新建的办公室太大以至于要将奖杯要分放在两个不同的地方以便同学们丢硬币进去开光,OB 想请你帮帮他看看奖杯放在哪两个办公室使得在任意一个在劳模办公室做题的小朋友能最快地找到奖杯来开光。

一句话题意:给出一个 n 个点的树,在两个合适且不同的点放上奖杯,使得每个点到最近的奖杯距离最大值最小。

输入

第一行,一个整数 n。

接下来的 n − 1 行,每行两个数 x y

输出

一个数,表示最小的最大距离。

样例输入

8

1 2

1 3

2 4

2 5

3 6

3 7

1 8

样例输出

2

提示

对于前 60% 的数据,n ≤ 100。

对于前 80% 的数据,n ≤ 2000。

对于 80% 的数据,保证树的形态随机。

对于 100% 的数据,保证 3 ≤ n ≤ 200000。

Solution

这道题解决方法非常多,然而博主蒟蒻只会\(O(n\log n)\)的做法

那么这两个奖杯到底要放哪里呢?可以证明一定在树的直径上

简易的讲一下

这就要说到树的直径的性质:树的直径是树上最长的一条路径,且树上任意一个点距它距离最远的点一定是树的直径的一个端点

假设奖杯在树的直径上能满足条件,那么我们最起码要保证它一定能覆盖到直径的至少一个端点,否则肯定不满足条件,既然它能满足端点,那么一定能满足直径上的点的子树中的节点,除非一个点子树中最深深度比它离直径端点的距离还远,但这又违反了直径的性质,故假设成立

知道了这两个点在直径上,怎么知道它们距离直径的距离呢?因为这两个点离直径端点的距离肯定是在满足题意的前提下距离最大的,我们要让这个距离最小,可以二分这个距离,然后去\(O(n)\)验证

怎么验证,就是我开始说的性质,首先找到这两个节点,然后找两个节点之间的那一段区间的每一颗子树,看不在直径上的节点的最大深度是不是超过了我们二分的这个mid

考场上匆忙打出来的代码,有点丑陋,将就着看吧~~

听说这道题还可以树形dp\(O(n)\)做,在这里贴一下题解说的各种做法

\(3.1\ 60\% O(n^3 )\)

\(n^2\)枚举两个奖杯位置,再\(O(n)\)扫一遍看看每个位置离最近奖杯最远是多少。

\(3.2\ 80\% O(n^2)\)

考虑两个奖杯管辖的区域必定有一个边界,我们枚举这个边界,也就是一条边,其中一部分是子树,一部分是子树外,我们只要分别求出另外两个树的直径。

\(3.3\) 树形态随机

期望树的直径很短,两个奖杯都在直径上枚举。

\(3.4\ 100\%\) 二分答案1 \(O(nlogn)\)

奖杯在直径上,二分答案后取离直径上离端点距离答案的点,遍历 check 一遍。

\(3.5\ 100\%\) 二分答案 2 \(O(nlogn)\)

随便提一个节点为根,二分答案,深度最深的节点一定要被照顾到,所以最深的点往上跳答案层即可,和其距离答案以内的点都删掉,再做一次。

此法可以拓展到 k 个奖杯,由皮皮轩友情提供。

\(3.6\ 100\%\) 树形dp\(\ O(n)\)

在 80 分的基础上用树形 dp,记下每个点向下前三长和向上一格后不回该子树最长的路径长度。子树内直径是前两长的和与该子树各个子树直径取 max;子树外直径是父节点向上一格后不回该子树最长的路径长度,前两长不进入该子树的向下最长路径这三条取前两长加起来与父节点以上的答案取 max。

Code

#include<bits/stdc++.h>
#define rg register
#define il inline
#define Min(a,b) (a)<(b)?(a):(b)
#define Max(a,b) (a)>(b)?(a):(b)
#define lol long long
#define in(i) (i=read())
using namespace std; const int N=2e5+10; int read() {
int ans=0,f=1; char i=getchar();
while(i<'0' || i>'9') {if(i=='-') f=-1; i=getchar();}
while(i>='0' && i<='9') ans=(ans<<1)+(ans<<3)+i-'0',i=getchar();
return ans*=f;
} int n,cur,s,t;
int to[N<<1],nex[N<<1],head[N];
int dis[N],f[N],son[N],vis[N],dep[N]; void add(int a,int b) {
to[++cur]=b,nex[cur]=head[a],head[a]=cur;
to[++cur]=a,nex[cur]=head[b],head[b]=cur;
} void dfs(int u,int fa) {
f[u]=fa;
for(int i=head[u];i;i=nex[i]) {
if(to[i]==fa) continue;
dis[to[i]]=dis[u]+1;
dfs(to[i],u);
}
} int dfs2(int u,int ans=1) {
dep[u]=dis[u];
for(int i=head[u];i;i=nex[i]) {
if(to[i]==f[u] || vis[to[i]]) continue;
dfs2(to[i]); dep[u]=max(dep[u],dep[to[i]]);
}return ans;
} bool check(int mid) {
int a=s,b=t;
while(a!=t) {
if(dis[a]-dis[s]==mid) break;
a=son[a];
}
while(b!=s) {
if(dis[t]-dis[b]==mid) break;
b=f[b];
}
if(dis[b]-dis[a]>2*mid) return 0;
int ll=a,rr=b;
while(a!=b && b) {
dfs2(b); int AQ=dep[b]-dis[b];
if(min(dis[b]-dis[ll],dis[rr]-dis[b])+AQ>mid) return 0;
b=f[b];
}
dfs2(b); int AQ=dep[b]-dis[b];
if(min(dis[b]-dis[ll],dis[rr]-dis[b])+AQ>mid) return 0;
return 1;
} int main()
{
//freopen("ob.in","r",stdin);
//freopen("ob.out","w",stdout);
in(n);
for(int i=1,a,b;i<n;i++)
in(a),in(b),add(a,b);
dfs(1,0); for(int i=1;i<=n;i++) if(dis[i]>dis[s]) s=i;
memset(dis,0,sizeof(dis)); dfs(s,0);
for(int i=1;i<=n;i++) if(dis[i]>dis[t]) t=i; int id=t; while(id) vis[id]=1,son[f[id]]=id,id=f[id];
vis[s]=vis[t]=1;
int l=0,r=dis[t];
while(l<r) {
int mid=l+r>>1;
if(check(mid)) r=mid;
else l=mid+1;
}cout<<r<<endl;
}