这种关键点的重心问题,一般有两种思路。
一种是合并:对于两个不交的点集 S , T S,T S,T, S ∪ T S\cup T S∪T 的重心必在 S , T S,T S,T 重心间的路径上,二分即可,需要数据结构支持 dfn 区间内 S ∪ T S\cup T S∪T 内的点的个数。
使用该做法能将本题做到 O ( n log 3 n + q log n ) O(n\log^3n+q\log n) O(nlog3n+qlogn)。
另一种想法是,对于单次询问点集 S S S,先从任意一个点开始,考虑往当前最大的子树走,直到最大的子树大小小于等于 S / 2 S/2 S/2。
如果用树剖+二分暴躁加速这个过程的话,单次询问还是 O ( log 3 n ) O(\log^3n) O(log3n) 的。
优秀的做法是使用三度化和边分治。我们先一一初步介绍。
三度化的具体作用是:将带权树,在不改变任意两点间距离、任意两点间祖先后代关系的情况下,转化成每个点度数至多为 3 3 3 的点数至多翻倍的树.
但三度化会改变两点间的直接父子关系,以及新增一些原树没有的虚点,这些都是需要注意的。
三度化有两种方法,如下图:
称原树中的点为旧点,新增的点为虚点,三度化后的树为新树。
其中使用第一种方法,能让新树的深度不超过 O ( d log n d ) O(d\log \frac{n}{d}) O(dlogdn),其中 d , n d,n d,n 是原树的深度和大小。但这个性质和本题没有关系。
边分治是如下的过程:
假设当前树大小为 n n n,每个点的度数不超过 D D D。找到一条边使得分出来两棵树的大小相差最小,设找到的边是 ( u , v ) (u,v) (u,v),且 u u u 对应的那棵子树的大小更大,设为 S S S。
设 u u u 的各个儿子的子树大小为 s 1 , ⋯ , s D − 1 s_1,\cdots,s_{D-1} s1,⋯,sD−1(不足 D − 1 D-1 D−1 个儿子用 0 0 0 补齐),那么显然对于任意 i i i 满足 s i ≤ n − s i s_i\leq n-s_i si≤n−si,进一步地,应满足 ( n − s i ) − s i ≥ S − ( n − S ) (n-s_i)-s_i\geq S-(n-S) (n−si)−si≥S−(n−S),得到 S + s i ≤ n S+s_i\leq n S+si≤n。
将这 D − 1 D-1 D−1 条不等式加起来,得到 ( D − 1 ) S + ∑ s i ≤ ( D − 1 ) n (D-1)S+\sum s_i\leq (D-1)n (D−1)S+∑si≤(D−1)n,即 D S − 1 ≤ ( D − 1 ) n DS-1\leq (D-1)n DS−1≤(D−1)n,那么 S ≤ D − 1 D n + 1 S\leq\frac{D-1}{D}n+1 S≤DD−1n+1。
从而,在将树三度化的基础上,按上述方法找到的边,能使得分成的两棵子树大小都不超过 2 3 n + 1 \frac{2}{3}n+1 32n+1。
于是边分树的深度是 O ( log 3 2 n ) = O ( log n log 3 2 ) = O ( log n ) O(\log_{\frac{3}{2}}n)=O\left(\dfrac{\log n}{\log\frac{3}{2}}\right)=O(\log n) O(log23n)=O(log23logn)=O(logn) 级别的。
具体回到这题。我们考虑用如下的方式找原树上若干关键点的重心:对于每一条边 ( a , b ) (a,b) (a,b),若 a a a 子树内的关键点个数小于 b b b 子树内的关键点个数,那么令这条边的方向是 a → b a\to b a→b;否则,若 b b b 子树内的关键点个数小于 a a a 子树内的关键点个数,那么令这条边的方向是 b → a b\to a b→a。若 a , b a,b a,b 子树内的关键点个数相同,那么令这条边无向。
那么可以证明,最后树上肯定形如:存在一个无向边的连通块,然后其余所有边都指向这个连通块。此时连通块内的任意一个点都可以是这些关键点的重心。
考虑将这个方法作用在新树上:那么原树上一条边 ( f a a , a ) (fa_a,a) (faa,a) 的指向,和新树上 ( f a a ′ , a ) (fa_a',a) (faa′,a) 的指向相同。
不妨假设新树上那些关键点的重心在 r t rt rt,注意 r t rt rt 可能是虚点。设 r t rt rt 在旧点 u u u 在新树上的子树内,但不在 u u u 任意一个旧儿子在新树上的子树内。考虑 ( f a u , u ) (fa_u,u) (fau,u) 的指向,由于 f a u ′ → u fa_u'\to u fau′→u,那么 f a u → u fa_u\to u fau→u。对于 u u u 的任意旧儿子 v v v,考虑 ( u , v ) (u,v) (u,v) 的指向,由于 v → f a v ′ v\to fa_v' v→fav′,那么 v → u v\to u v→u。从而,我们证明了 u u u 是原树上关键点的重心。
于是我们找出新树上的重心即可。我们从边分树的根开始往下走。每次考虑当前边 e e e 的指向。考虑直接数出 e e e 两边的关键点个数。首先要数 A , B A,B A,B 子树内编号在 [ l , r ] [l,r] [l,r] 内的点数,可以通过 dfn 序+可持久化做到 O ( n log n ) − O ( log n ) O(n\log n)-O(\log n) O(nlogn)−O(logn)。而对于 A , B A,B A,B 外的那些点,它们在边分树上对应不超过 O ( log n ) O(\log n) O(logn) 棵子树,且这些子树内的关键点个数我们也可以在走下来的过程中顺便记录,于是我们只需要知道当前这些子树每一棵属于 A A A 还是 B B B 即可:这其实很简单,因为这些子树肯定整体都在 e e e 的一边,所以随便选取一个代表点,利用 dfn 序判断它在 e e e 的哪一边即可。
于是,我们单次可以 O ( log 2 n ) O(\log^2n) O(log2n) 找到一个区间的关键点的重心。
再离线下来用点分治求出在原树上对应的重心到所有点的距离,可以做到 O ( ( n + q ) log n ) O((n+q)\log n) O((n+q)logn)。
从而,我们在 O ( n log n + q log 2 n ) O(n\log n+q\log^2n) O(nlogn+qlog2n) 的时间内解决这个问题。
#include
#define N 100010
#define ll long long
using namespace std;
inline int read()
{
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<1)+(x<<3)+(ch^'0');
ch=getchar();
}
return x*f;
}
int n,Q;
vector<int> e[N];
namespace P2
{
struct query{int u,id,coef;};
vector<query> q[N];
}
namespace P1
{
#define lc(u) ch[u][0]
#define rc(u) ch[u][1]
const int N1=N<<1,N2=N1<<1;
int n1,cnt=1,head[N1],to[N1<<1],nxt[N1<<1];
int dfx,fa[N1],in[N1],out[N1],belong[N1];
void adde(int u,int v)
{
to[++cnt]=v,nxt[cnt]=head[u],head[u]=cnt;
to[++cnt]=u,nxt[cnt]=head[v],head[v]=cnt;
}
void D3(int u,int fa)
{
vector<int> son;
for(int v:e[u]) if(v!=fa) son.push_back(v);
function<int(int,int)> dfs=[&](int l,int r)
{
if(l==r) return son[l];
int mid=(l+r)>>1;
int x=(l==0&&r==(int)son.size()-1?u:++n1);
belong[x]=u;
adde(x,dfs(l,mid)),adde(x,dfs(mid+1,r));
return x;
};
belong[u]=u;
if(!son.empty())
{
if(son.size()==1) adde(u,son[0]);
else dfs(0,son.size()-1);
}
for(int v:son) D3(v,u);
}
void dfs(int u)
{
in[u]=++dfx;
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(v==fa[u]) continue;
fa[v]=u,dfs(v);
}
out[u]=dfx;
}
int n2,Rt,ch[N2][2],eid[N2];
int idx,id[N1],L[N2],R[N2];
int nrt,maxn,nn,size[N1];
bool vis[N1<<1];
void getsize(int u,int fa)
{
size[u]=1;
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(v==fa||vis[i]) continue;
getsize(v,u),size[u]+=size[v];
}
}
void getroot(int u,int fa)
{
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(v==fa||vis[i]) continue;
int x=abs(nn-size[v]-size[v]);
if(x<maxn) maxn=x,nrt=i;
getroot(v,u);
}
}
int build(int u)
{
getsize(u,0);
if(size[u]==1)
{
id[u]=L[u]=R[u]=++idx;
return u;
}
int x=++n2;
nn=size[u],maxn=INT_MAX,getroot(u,0);
eid[x]=nrt,vis[nrt]=vis[nrt^1]=1;
int a=to[nrt],b=to[nrt^1];
lc(x)=build(a),rc(x)=build(b);
L[x]=L[lc(x)],R[x]=R[rc(x)];
return x;
}
int rt[N];
namespace Seg
{
const int NN=10000000;
int node,ch[NN][2],sum[NN];
void update(int &u,int lst,int l,int r,int x)
{
u=++node,lc(u)=lc(lst),rc(u)=rc(lst),sum[u]=sum[lst]+1;
if(l==r) return;
int mid=(l+r)>>1;
if(x<=mid) update(lc(u),lc(lst),l,mid,x);
else update(rc(u),rc(lst),mid+1,r,x);
}
int query(int u,int l,int r,int ql,int qr)
{
if(!u) return 0;
if(ql<=l&&r<=qr) return sum[u];
int mid=(l+r)>>1,ans=0;
if(ql<=mid) ans+=query(lc(u),l,mid,ql,qr);
if(qr>mid) ans+=query(rc(u),mid+1,r,ql,qr);
return ans;
}
int query(int pl,int pr,int ql,int qr)
{
return query(rt[pr],1,n1,ql,qr)-query(rt[pl-1],1,n1,ql,qr);
}
}
struct data{int kp,s;};
bool judge(int p,int a,int b)
{
if(fa[a]==b) return in[a]<=in[p]&&in[p]<=out[a];
return !(in[b]<=in[p]&&in[p]<=out[b]);
}
int query(int u,int pl,int pr,vector<data> c)
{
if(u<=n1) return u;
int a=to[eid[u]],b=to[eid[u]^1];
int sa=Seg::query(pl,pr,L[lc(u)],R[lc(u)]),ta=0;
int sb=Seg::query(pl,pr,L[rc(u)],R[rc(u)]),tb=0;
for(auto now:c) (judge(now.kp,a,b)?ta:tb)+=now.s;
if(sa+ta>sb+tb)
{
c.push_back(data{b,sb});
return query(lc(u),pl,pr,c);
}
else
{
c.push_back(data{a,sa});
return query(rc(u),pl,pr,c);
}
}
void main()
{
n1=n,D3(1,0); dfs(1);
n2=n1,Rt=build(1);
for(int i=1;i<=n;i++)
Seg::update(rt[i],rt[i-1],1,n1,id[i]);
for(int i=1;i<=Q;i++)
{
int l=read(),r=read();
int u=belong[query(Rt,l,r,vector<data>())];
P2::q[r].push_back(P2::query{u,i,1});
P2::q[l-1].push_back(P2::query{u,i,-1});
}
}
#undef lc
#undef rc
}
namespace P2
{
int fa[N];
int rt,maxn,nn,size[N];
bool vis[N];
vector<int> dis[N];
void getsize(int u,int fa)
{
size[u]=1;
for(int v:e[u]) if(v!=fa&&!vis[v])
getsize(v,u),size[u]+=size[v];
}
void getroot(int u,int fa)
{
int nmax=nn-size[u];
for(int v:e[u]) if(v!=fa&&!vis[v])
getroot(v,u),nmax=max(nmax,size[v]);
if(nmax<maxn) maxn=nmax,rt=u;
}
void getdis(int u,int fa,int dis,vector<int> *D)
{
D[u].push_back(dis);
for(int v:e[u]) if(v!=fa&&!vis[v])
getdis(v,u,dis+1,D);
}
void build(int u)
{
vis[u]=1;
getdis(u,0,0,dis);
for(int v:e[u])
{
if(vis[v]) continue;
getsize(v,0);
nn=size[v],maxn=INT_MAX,getroot(v,0);
fa[rt]=u,build(rt);
}
}
struct data
{
ll sd; int sz;
data(){}
data(ll a,int b){sd=a,sz=b;}
void operator += (data b){sd+=b.sd,sz+=b.sz;}
data operator - (data b){return data(sd-b.sd,sz-b.sz);}
ll calc(int d){return 1ll*d*sz+sd;}
}s1[N],s2[N];
ll ans[N];
void add(int x)
{
int u=x;
s1[u]+=data(0,1);
for(int i=(int)dis[x].size()-2;i>=0;i--,u=fa[u])
s1[fa[u]]+=data(dis[x][i],1),s2[u]+=data(dis[x][i],1);
}
ll query(int x)
{
int u=x;
ll ans=s1[u].sd;
for(int i=(int)dis[x].size()-2;i>=0;i--,u=fa[u])
ans+=(s1[fa[u]]-s2[u]).calc(dis[x][i]);
return ans;
}
void main()
{
getsize(1,0);
nn=size[1],maxn=INT_MAX,getroot(1,0);
build(rt);
for(int i=1;i<=n;i++)
{
add(i);
for(auto now:q[i]) ans[now.id]+=now.coef*query(now.u);
}
for(int i=1;i<=Q;i++)
printf("%lld\n",ans[i]);
}
}
int main()
{
n=read(),Q=read();
for(int i=1;i<n;i++)
{
int u=read(),v=read();
e[u].push_back(v),e[v].push_back(u);
}
P1::main();
P2::main();
return 0;
}