小简单正在学习离散数学,今天的内容是图论基础,在课上他做了如下两条笔记:
∑ ( u , v ) ∈ E ( ∑ 1 ≤ x ≤ n 且 x 号 点 是 S u ′ 的 重 心 x + ∑ 1 ≤ y ≤ n 且 y 号 点 是 S v ′ 的 重 心 y ) \sum_{(u,v) \in E} \left( \sum_{1 \leq x \leq n \atop 且 x 号点是 S'_u 的重心} x + \sum_{1 \leq y \leq n \atop 且 y 号点是 S'_v 的重心} y \right) (u,v)∈E∑⎝⎜⎛且x号点是Su′的重心1≤x≤n∑x+且y号点是Sv′的重心1≤y≤n∑y⎠⎟⎞
上式中, E E E 表示树 S S S 的边集, ( u , v ) (u,v) (u,v) 表示一条连接 u u u 号点和 v v v 号点的边。 S u ′ S'_u Su′与 S v ′ S'_v Sv′分别表示树 S S S 删去边 ( u , v ) (u,v) (u,v) 后, u u u 号点与 v v v 号点所在的被分裂出的子树。
小简单觉得作业并不简单,只好向你求助,请你教教他。
求一颗树断掉任意一条边之后的两棵树的所有重心的编号的和的和。
注:一棵树存在一个或者两个重心。
n ≤ 3 × 1 0 5 n\leq 3\times 10^5 n≤3×105。
暴力按题意断掉一条边然后分别对两个树dfs找出重心即可。
链上的重心显然为这条链的中点(一个或两个),直接模拟即可。
一个显然的想法是完美二叉树具有极强的对称性,考虑我们每层都只删一条边然后暴力,其它的边我们可以通过对称方法等价出来。因此删的边只会是 O ( log n ) O(\log n) O(logn)级别的,能够通过数据。
当然这题也有很强的规律性。
例如下面这个完美二叉树:
这样我们就可以直接计算重心编号之和了。
应该是本题最自然的一种解法。
这种做法依据的一个关键性质是:
引理:
对于一个点 x x x,如果它不是这颗树的重心,那么重心一定在以它为根的重儿子的子树里面。
这个证明其实比较好理解,大概可以看这张图感性理解一下:
智障与大佬问答环节:
小P:如果一个节点有两个大小相同的最大siz的时候该走那边啊…
巨佬:你可以直接放弃选择。
小P:???
巨佬:此时两个节点的siz显然 ≤ ⌊ n 2 ⌋ \leq \lfloor\frac{n}{2}\rfloor ≤⌊2n⌋,那么这个点就是重心。
小P:…
知道这个做法就好做了,我们考虑换根,每次我们计算割去 x x x为父亲 v v v为儿子的边两颗树的重心。
定义 h s x , k hs_{x,k} hsx,k表示以 x x x为根时向它的重儿子走 k k k步时到达的节点。
假设我们已经求出 h s x , k hs_{x,k} hsx,k数组,考虑如何求答案:
注意到我们总是往儿子走,那么走到的点的 s i z siz siz会随着走的步数的增加而递减,而我们向下走始终会找到一个重心,因此我们找深度最大的满足 s i z x − s i z v ≤ ⌊ s i z x 2 ⌋ siz_x-siz_v\leq \lfloor\frac{siz_x}{2}\rfloor sizx−sizv≤⌊2sizx⌋的点 v v v即为这个树的一个重心,由于可能存在两个重心,我们还需要检验 v v v的父亲是不是重心。
那么我们分别考虑在 x x x为根和 v v v为根的两个联通块内倍增即可。
然而我们还需要处理 s i z x siz_x sizx和 h s x , k hs_{x,k} hsx,k。
处理完 x x x之后,我们还需要记住还原 x x x被修改的信息。
一次dfs找重儿子和次重儿子,一次dfs找答案即可。
复杂度: O ( n log n ) O(n\log n) O(nlogn)
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define LL long long
#define MAXN 3000000
#define mem(x,v) memset(x,v,sizeof(x))
LL read(){
LL x=0,F=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')F=-1;c=getchar();}
while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
return x*F;
}
int T,n;
vector<int> G[MAXN+5];
int siz[MAXN+5],hs[MAXN+5][18],f[MAXN+5];
int phs[MAXN+5];LL ans;
void upd(int x){
for(int k=1;k<=17;k++)
hs[x][k]=hs[hs[x][k-1]][k-1];
}
bool check(int x,int N){
return max(N-siz[x],siz[hs[x][0]])<=(N/2);
}
void dfs1(int x,int fa){
siz[x]=1,hs[x][0]=phs[x]=0,f[x]=fa;
for(int i=0;i<G[x].size();i++){
int v=G[x][i];
if(v==fa)continue;
dfs1(v,x);
siz[x]+=siz[v];
if(siz[hs[x][0]]<siz[v])phs[x]=hs[x][0],hs[x][0]=v;
else if(siz[phs[x]]<siz[v])phs[x]=v;
}
}
void dfs2(int x,int fa){
int prehs=hs[x][0],presz=siz[x];
for(int i=0;i<G[x].size();i++){
int v=G[x][i];
if(v==fa)continue;
if(v==prehs)hs[x][0]=phs[x];
else hs[x][0]=prehs;
if(siz[fa]>siz[hs[x][0]])hs[x][0]=fa;
upd(x);
siz[x]=n-siz[v],f[x]=0,f[v]=0;
int p=x;
for(int k=17;k>=0;k--)
if(hs[p][k]&&siz[x]-siz[hs[p][k]]<=(siz[x]/2))p=hs[p][k];
ans+=p+f[p]*check(f[p],siz[x]);
//printf("::%d %d\n",x,v);
//printf("**%d %d\n",p,f[p]*check(f[p],siz[x]));
p=v;
for(int k=17;k>=0;k--)
if(hs[p][k]&&siz[v]-siz[hs[p][k]]<=(siz[v]/2))p=hs[p][k];
ans+=p+f[p]*check(f[p],siz[v]);
//printf("**%d %d\n",p,f[p]*check(f[p],siz[v]));
f[x]=v;
dfs2(v,x);
}
siz[x]=presz,hs[x][0]=prehs,f[x]=fa,upd(x);
}
int main(){
T=read();
while(T--){
ans=0;
n=read();
for(int i=1;i<=n;i++)G[i].clear();
for(int i=1;i<n;i++){
int u=read(),v=read();
G[u].push_back(v),G[v].push_back(u);
}
dfs1(1,0);
for(int k=1;k<=17;k++)
for(int i=1;i<=n;i++)
hs[i][k]=hs[hs[i][k-1]][k-1];
dfs2(1,0);
printf("%lld\n",ans);
}
}
这个做法具有一定的通用性。
考虑对于每一个点计算作为重心的次数。
- 切取它子树内的一个子树,这个点成为重心。
- 切取它子树外的一个子树,这个点成为重心。
- 保留包含该子树的一个子树,这个点成为重心。
这样讨论不是不可做,但是讨论起来相当麻烦,因此我们试着优化一下。
考虑我们首先找到原树的一个重心作为根 r t rt rt,对于一个点 x ≠ r t x≠rt x=rt就不会存在第一种情况。因为子树内的点数始终小于 ⌊ n 2 ⌋ \lfloor \frac{n}{2}\rfloor ⌊2n⌋,父亲方向的连通块大小就会始终大于 ⌊ n 2 ⌋ \lfloor \frac{n}{2}\rfloor ⌊2n⌋。
设 m x x = max i = 1 s i z x s i z s o n x , i mx_x=\max^{siz_x}_{i=1}{siz_{son_{x,i}}} mxx=maxi=1sizxsizsonx,i
考虑割掉子树外大小为 P P P的连通块使得 x x x成为重心,那么应该满足:
n − P − s i z x ≤ ⌊ n − P 2 ⌋ m x x ≤ ⌊ n − P 2 ⌋ n-P-siz_x\leq \lfloor \frac{n-P}{2}\rfloor\\ mx_x\leq \lfloor\frac{n-P}{2}\rfloor\\ n−P−sizx≤⌊2n−P⌋mxx≤⌊2n−P⌋
联立不等式得到
n − 2 s i z x ≤ P ≤ n − 2 m x x n-2siz_x\leq P\leq n-2mx_x n−2sizx≤P≤n−2mxx
仍然考虑换根,我们在换根时维护 P = x P=x P=x时有多少种割法,一次换根只会影响到一个 P P P值的改变。
然而这样统计我们也统计了在 x x x子树内的 P P P值,我们需要想办法减去它。
注意到子树是dfs过程中连续的一段,因此我们可以让答案在dfs出 x x x点时已经过的点符合条件的 P P P值的个数减去进 x x x点时已经过的点符合条件的 P P P值的个数。类似NOIP2016天天爱跑步的思路。
同样可以用一个树状数组解决这个问题。当然如果你没有想到这个思路,无脑线段树合并也是可做的。
但是我们还需要统计 x = r t x=rt x=rt时合法的割边数。
由于只有一个点,我们暴力讨论即可。
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define LL long long
#define MAXN 3000000
#define mem(x,v) memset(x,v,sizeof(x))
LL read(){
LL x=0,F=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')F=-1;c=getchar();}
while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
return x*F;
}
int T,n,rt;
int siz[MAXN+5],hs[MAXN+5],phs[MAXN+5],flag[MAXN+5];
vector<int> G[MAXN+5];
LL ans;
struct bit{
int C[MAXN+5];
void init(){mem(C,0);}
int lowbit(int x){return x&(-x);}
void Insert(int x,int v){
x++;
while(x<=n+1)C[x]+=v,x+=lowbit(x);
}
LL Query(int x){
x++;LL res=0;
while(x)res+=C[x],x-=lowbit(x);
return res;
}
}T1,T2;
bool check(int x,int N){
return max(N-siz[x],siz[hs[x]])<=(N/2);
}
void dfs1(int x,int fa){
siz[x]=1,hs[x]=phs[x]=0;
for(int i=0;i<G[x].size();i++){
int v=G[x][i];
if(v==fa)continue;
dfs1(v,x);
siz[x]+=siz[v];
if(siz[hs[x]]<siz[v])phs[x]=hs[x],hs[x]=v;
else if(siz[phs[x]]<siz[v])phs[x]=v;
}
if(check(x,n))rt=x;
}
void dfs2(int x,int fa){
if(x!=rt){
flag[x]|=flag[fa];
T1.Insert(siz[fa],-1);
T1.Insert(n-siz[x],1);
ans+=x*(T1.Query(n-2*siz[hs[x]])-T1.Query(n-2*siz[x]-1));
ans+=x*(T2.Query(n-2*siz[hs[x]])-T2.Query(n-2*siz[x]-1));
if(flag[x])ans+=rt*(siz[x]<=n-2*siz[phs[rt]]);
else ans+=rt*(siz[x]<=n-2*siz[hs[rt]]);
}
T2.Insert(siz[x],1);
for(int i=0;i<G[x].size();i++){
int v=G[x][i];
if(v==fa)continue;
dfs2(v,x);
}
if(x!=rt){
T1.Insert(siz[fa],1);
T1.Insert(n-siz[x],-1);
ans-=x*(T2.Query(n-2*siz[hs[x]])-T2.Query(n-2*siz[x]-1));
}
}
int main(){
T=read();
while(T--){
ans=rt=0;
n=read();
for(int i=1;i<=n;i++)G[i].clear();
for(int i=1;i<n;i++){
int u=read(),v=read();
G[u].push_back(v),G[v].push_back(u);
}
dfs1(1,0),dfs1(rt,0);
T1.init(),T2.init();
for(int i=1;i<=n;i++)T1.Insert(siz[i],1),flag[i]=0;
flag[hs[rt]]=1;
dfs2(rt,0);
printf("%lld\n",ans);
}
}