题解 P1099 【树网的核】

· · 题解

虽然本题题解已有很多,但基本上是O(N^3)的暴力或O(N)的递推做法,而仍有一种O(N·logN)的做法没人提及,但仍可通过VijosN=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)。 但是在平常做题,有较多时间的情况下,我们不妨详细探讨一下这个问题。

  1. 很明显的是,在众多支链中,仅会有最长支链对答案造成影响(由偏心距的定义可推出)。当核的左右两端点 到直径端点的距离最长支链到核的距离短的时候,偏心距就不受直径的影响了。

由第一章的证明,在达到边界前仍是满足单调性的,于是我们就大胆猜想:二分边界的上界是树的直径,下界就是最长支链到直径的距离!

我们对其证明:

由定义可知,核是直径的一段路径,设最长支链与该直径交于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); } ``` 写这东西真心累,~~字数我觉得都能上洛谷日报~~。