题解 P1099 【树网的核】
天泽龟
·
·
题解
虽然本题题解已有很多,但基本上是O(N^3)的暴力或O(N)的递推做法,而仍有一种O(N·logN)的做法没人提及,但仍可通过Vijos上N=400000的大数据。
没错,他就是二分。
本文将从二分的正确性,二分的具体实现过程,二分边界等多个方面详细阐述这种解法,希望您在观看过后也能对二分答案有一个更为详细的理解。
顺便,bzoj的加强版挂了,我把数据贴到了洛谷可供给大家验题,戳这。
1. ~二分的正确性/单调性的证明
众所周知,一道题如果能通过二分求解,那么他一定具有单调性。
对于本题而言,如果我们对偏心距进行二分答案,在check函数的变化,那么很显然的是,当你设定的偏心距越大,可满足的核就会越向树的中心集中,s自然也就越小了,所以这是满足单调性的。
然而又由于涉及到边界问题,导致其并非严格单调,关于这个细节部分等到第三节再来细谈。
2.~二分的具体实现过程/check函数
既然我们已经确定了偏心距的范围,那么问题就转化成 “对于一条直径是否存在一个核,长度小于s且偏心距不超过定值mid?”
那我们知道对于树上的任意点,他到其他点的最大距离即为他到某直径端点的距离。从而我们可以推断出,某一核的偏心距实质上也就是 核的端点到直径端点的距离最值。如果我们设直径两端点为A,B,核F的两端点为p,q,那么以上结论就可以表述为:
ECC(F)=\max(dis[p,A],dis[q,B])
对于上式,p,q是我们要求的未知范围,而A,B是我们已知的直径端点,易想到去通过A,B去反推p,q。
具体的说,我们可以 以A,B为圆心,以mid为半径画圆 以A,B端点分别向直径内部递推,找到与端点距离不超过mid的最远节点, 此时的两点即为所求的p,q。
最终我们判断p,q两点距离是否小于s,作为我们二分答案的判断标志。注意,这里不用考虑绝对值!! 如果距离为负,标志着两点范围已经互相越过去到达了对面,那么这时候在范围内随便取一个点都应该是满足的。
对于具体的操作来说,可以先以直径某一端点(如A)为根节点,以此不断向下递归同时记录已走的距离,找到最远满足条件的节点p;对于另一端B可以直接往上不断跳,找到最远满足节点q。
放上check的部分代码:
ll drop(ll u,ll fa,ll x) //从高往下掉
{
for (ll i=fir[u];i;i=e[i].nex)
if (v[e[i].u]) {
if (e[i].u==fa) continue;
l1+=e[i].w; if (l1>x) return u;
else return drop(e[i].u,u,x);
}
}
ll up(ll u,ll x) //从低往上爬
{
while (u!=A&&l2+w[u]<=x) l2+=w[u],u=f[u][0];
return u;
}
bool check(ll x)
{
l1=l2=0; p=drop(A,0,x); q=up(B,x);
return (d[q]-d[p]<=s);
}
算上主函数里的二分,应该是O(N·logN)的时间复杂度。
3.~二分的边界判断
所谓二分边界,就是指放在主程序中的l,r。普遍的来说,二分答案是可以从0取到INTMAX的,但对于本题,情况发生了改变。
我们不妨从一个例子引入:
6 6
1 2 5
2 3 3
3 4 3
4 5 5
3 6 6
错解:5 正解:6
恩??为什么会这样呢? 我们来看一下这棵树长什么样:
当我们取{2,3,4}作为核的点集时,按上面的做法,据直径端点的距离应为5,但很显然答案应该是由6号节点造成的6!
相信你肯定恍然大悟了:我们全程没有考虑支链对答案的影响!!
如果放在考场上,相信如果换做我的一位朋友,他一定会心态崩溃,疯狂怀疑二分的正解性,开始胡乱打暴力骗分,最后惨遭爆炸 (你说的这个朋友到底是不是你自己)(这题好像骗分就能AC)。 但是在平常做题,有较多时间的情况下,我们不妨详细探讨一下这个问题。
- 很明显的是,在众多支链中,仅会有最长支链对答案造成影响(由偏心距的定义可推出)。当核的左右两端点 到直径端点的距离比最长支链到核的距离短的时候,偏心距就不受直径的影响了。
由第一章的证明,在达到边界前仍是满足单调性的,于是我们就大胆猜想:二分边界的上界是树的直径,下界就是最长支链到直径的距离!
我们对其证明:
由定义可知,核是直径的一段路径,设最长支链与该直径交于F点,最长支链的叶节点为C,如果F\in \text{核的点集},那么偏心距必然\geq FC。
如果f\notin \text{核的点集},那么设核到F的距离为d,由于支链必然小于到直径端点A,B的距离,则亦满足:d+FB\geq d+FC,,取等条件是C是另一直径的端点。上式表明若核不包含F,偏心距仍在直径端点处取得,最长支链无贡献。
形象地说,如果以A为端点找核的话,偏心距应该满足这么一个样子:
---
2. 现在我们来想想如何求最长支链。
我们不妨用暴力的思想,考虑每一个不在直径上的点,用$LCA$求出与直径的交点$F$,然后再利用简单的树上两点公式算出最大支链即可。
枚举每个点是$O(N)$的,算LCA是$O(logN)$的,合起来是$O(N·logN)$,依然是在时限内的。
至此,我们算出了二分的下界,整篇题解就结束了。
## $4.$日 后谈
从写这道题,到写完这篇题解,共花了我7个小时(下午4时~晚上11时),主要是期间有很多钻牛角尖的地方,这告诉我们没事千万别死磕题,智商会下降的。
期间一直在想能不能通过类比树的重求出树的中心,然后类似洪水填充向直径扩散求最小偏心距,最终码量有点大,没能实现,期望有后人能帮我验证这一想法可行性。
最后的最后,还是上我丑陋的代码:
```cpp
#include <cstdio>
#include <map>
#include <vector>
#include <algorithm>
#define mp make_pair
#define pr pair<int,int>
#define maxn 500010
#define ll long long
using namespace std;
//1. 搞出一条直径O(N),可证明上面必有最优核。
//2. 对于不在直径AB的点, 算LCA,比较与直径关系求最长支链
//3. 二分答案,A,B两端点以mid为半径画圆,标记核范围p,q
//4. 判断p,q距离是否小于s,
struct ed{
ll u,nex,w;
}e[maxn*2];
ll n,s,st,fir[maxn];
ll ans,A,B,D,l,r,d[maxn];//树的重心D信息
ll f[maxn][21],dd[maxn],v[maxn],w[maxn],F,minn;//树的中心F信息
ll p,q,l1,l2;
void add(ll x,ll y,ll w)
{
e[++st].u=y; e[st].nex=fir[x]; e[fir[x]=st].w=w;
e[++st].u=x; e[st].nex=fir[y]; e[fir[y]=st].w=w;
}
void dfs(ll u,ll fa,ll w)
{
d[u]=d[fa]+w; if (d[u]>ans) ans=d[u],l=u;
for (ll i=fir[u],ax=0;i;i=e[i].nex)
{
ll v=e[i].u,w=e[i].w;
if (v==fa) continue; dfs(v,u,w);
}
}
void dfsl(ll u,ll fa)
{
f[u][0]=fa; dd[u]=dd[fa]+1;
for (ll i=1;i<=20;i++) f[u][i]=f[f[u][i-1]][i-1];
for (ll i=fir[u];i;i=e[i].nex) if (e[i].u!=fa) dfsl(e[i].u,u),w[e[i].u]=e[i].w;
}
void Fdis(ll u) { while (u) v[u]=1,u=f[u][0]; } //标记直径
ll lca(ll x,ll y)
{
if (dd[x]>dd[y]) swap(x,y);
for (ll i=20;i>=0;i--)
if (dd[y]-(1<<i)>=dd[x]) y=f[y][i];
if (x==y) return x;
for (ll i=20;i>=0;i--)
if (f[y][i]!=f[x][i]) y=f[y][i],x=f[x][i];
return f[x][0];
}
ll dismin() //找最大支链
{
dfsl(A,0); Fdis(B);
for (ll i=1;i<=n;i++)
if (!v[i])
{
ll L=lca(i,B);
minn=max(minn,d[i]-d[L]);
}
return minn;
}
ll drop(ll u,ll fa,ll x)
{
for (ll i=fir[u];i;i=e[i].nex)
if (v[e[i].u]) {
if (e[i].u==fa) continue;
l1+=e[i].w; if (l1>x) return u;
else return drop(e[i].u,u,x);
}
}
ll up(ll u,ll x)
{
while (u!=A&&l2+w[u]<=x) l2+=w[u],u=f[u][0];
return u;
}
bool check(ll x)
{
l1=l2=0; p=drop(A,0,x); q=up(B,x); //极短的核心内容
return (d[q]-d[p]<=s);
}
int main()
{
scanf("%lld%lld",&n,&s);
for (ll i=1,x,y,w;i<n;i++)
scanf("%lld%lld%lld",&x,&y,&w),add(x,y,w);
dfs(1,0,0); ans=0; r=l; dfs(l,0,0);
A=r; B=l; D=ans;//1.
l=dismin(); r=D; //2. l即为最大支链长
while (l<r)
{
ll mid=(l+r)/2;
if (check(mid)) r=mid; else l=mid+1;
}
printf("%lld\n",l);
}
```
写这东西真心累,~~字数我觉得都能上洛谷日报~~。