[CSP-S2019]树的重心 题解
CSP-S2 2019 D2T3
考场上扔了T2来打这题的部分分,然后没看到数据范围是等号,不知道怎么判完全二叉树然后40分滚粗……
----
思路分析
很容易想到$O(n^2)$每次暴力找重心,这个暴力可以用各种神仙方法优化。
通过分析35分的特殊构造分,可以有一个想法,既然特殊构造可以有结论,那么是否也可以有一些结论来解决或者优化整个问题的解法。实际上,通过分析样例可以得到一些性质,这些性质可能有利于问题的求解:
1. 一棵树如果有两个重心,这两个重心一定是相邻的
2. 一棵树的重心一定在根节点所在的重链上
3. 一棵树的重心一定是以该树根节点重儿子为根的子树的重心的祖先
这些性质可以节省下很多不必要的时间。根据性质1,我们可以先找到深度较大的重心,然后对于已求出的重心再判断其父亲是否也是重心(因此接下来说的重心都是深度较大的重心);根据性质2,我们在找重心的时候可以只往根节点所在的重链上找;根据性质3,我们可以从下往上找重心,不用每次重新找。
还有一些性质,接下来对于每种情况具体分析。
删掉一条边后一棵树会变成两部分,设该边的两个端点为$x,y$,其中深度较大的一点为$y$,则这两部分分别为以$y$为根的子树和整棵树减掉以$y$为根的子树。我们先分析以$y$为根的子树。
以$y$为根的子树
我们可以根据性质3预处理出以所有节点为根的子树的重心,只要不断向上走就可以,时间复杂度$O(n)$。
整棵树减掉以$y$为根的子树
根据性质2,这棵树的重心一定在根节点所在的重链上。试想,只要删掉的以$y$为根的子树大小一样,位置实际上是对重心没有影响的,唯一有可能影响的情况就是删掉的这个子树在以根节点重儿子为根的子树中。我们将这些情况再具体分析。
$y$不在以根节点重儿子为根的子树中
这是最平常的情况。既然只有删掉子树的大小对重心有影响,我们可以预处理出删去所有大小的子树之后的重心,然后直接询问。这个预处理只需要从根节点所在的重链从下到上走一遍就可以,时间复杂度$O(n)$。
$y$在以根节点重儿子为根的子树中
试想,如果删掉以$y$为根的子树后,这个重儿子仍然是重儿子,那么重心还是会在原来这条重链上,而重心显然只可能往重链上远离$y$的一端移动,而如果根节点就是重心,删掉子树之后根节点仍然会是重心。因此,最开始我们可以让整棵树的重心作为根节点,处理这种情况就变得很方便。
如果删掉子树后这个重儿子不再是重儿子了怎么办?显然,现在这个重儿子只会是原来的次重儿子。因此,我们可以预处理出次重儿子所在的重链上删去所有大小的子树之后的重心,然后按照$y$不在以根节点重儿子为根的子树中的情况处理。
所有预处理都是$O(n)$,每次询问$O(1)$,总的复杂度$O(n)$。
下面给出具体实现。(由于本人太菜,实现过程可能常数比较大,请勿模仿)
具体实现
1.找重心
dfs一遍随便找到一个重心即可。把整棵树的重心作为之后的根节点。
void findroot(int x,int f)
{
siz[x]=1;
for(int i=head[x],y=ver[i];i;i=Next[i],y=ver[i])
if(y!=f)
{
findroot(y,x);
siz[x]+=siz[y];
if(siz[y]>siz[son[x]])
son[x]=y;
}
if(siz[son[x]]*2<=n && (n-siz[x])*2<=n)
root=x;
} findroot(1,0);
2.预处理节点信息
预处理每个节点的子树大小、重儿子、深度、父亲以及属于根节点哪个儿子的子树,同时找到根节点的次重儿子。dfs一遍即可。
void pre(int x,int f)
{
siz[x]=1,d[x]=d[f]+1,fa[x]=f;//分别表示子树大小、深度以及父亲
if(f==root)
ffa[x]=x;
else
ffa[x]=ffa[f];//属于根节点的哪个子树
for(int i=head[x],y=ver[i];i;i=Next[i],y=ver[i])
if(y!=f)
{
pre(y,x);
siz[x]+=siz[y];
if(siz[y]>siz[son[x]])
{
if(x==root)
son2=son[x];
son[x]=y;//找重儿子
}
else
if(x==root && siz[y]>siz[son2])
son2=y;//找根节点的次重儿子
}
} memset(son,0,sizeof(son));
pre(root,0);
3.预处理答案
分别预处理出根节点重儿子所在的重链的答案、根节点次重儿子所在的重链的答案以及以每个节点为根的子树的重心。每种情况从下到上走一遍即可。
第一种情况,对于每个删掉子树大小$y$,应该在根节点重儿子所在的重链上找到深度最大的$x$满足$2siz_x\geq n-y$。第二种情况同理。
第三种情况,对于每个子树的根节点$y$,应该其子树内深度最大的$x$满足$siz[x]\geq siz[y]$。
void get1(int x,int f)
{
if(son[x])
get1(son[x],x);//有重儿子走重儿子
while(n-2*siz[x]<=nowans && nowans)
{
ans1[nowans]=x;
nowans--;
}
}//根节点重儿子所在的重链的答案 void get2(int x,int f)
{
if(x==root)
nowans=n,get2(son2,x);//走根节点次重儿子
else
if(son[x])
get2(son[x],x);//其余节点有重儿子走重儿子
while(n-2*siz[x]<=nowans && nowans)
{
ans2[nowans]=x;
nowans--;
}
}//根节点次重儿子所在的重链的答案 void get3(int r)
{
if(son[r])
get3(son[r]);//先走重儿子
for(int i=head[r],y=ver[i];i;i=Next[i],y=ver[i])
if(y!=son[r] && d[y]>d[r])
get3(y);
int now=son[r]?ans3[son[r]]:r;//从重儿子的重心往上找
while(siz[now]*2<siz[r])
now=fa[now];
ans3[r]=now;
}//以r为根的子树的重心 get1(root,0),get2(root,0),get3(root);
4.枚举删边求答案
实际上枚举删掉子树也是可以的,这里还是用删边来写。
按照前面说到的情况,对于每种情况,先找到一个重心,然后再判断其父亲是否也是重心。判断的时候只要按照重心的定义来判断即可,注意有些特殊的节点要进行特判。
for(int i=1;i<=tot;i+=2)
{
int x=ver[i],y=ver[ano];
if(d[x]>d[y])
swap(x,y);//令y为深度较大的节点
int h1=ans3[y];
ans+=h1;//h1就是以y为根的子树的重心
if(d[fa[h1]]>=d[y] && check1(fa[h1],y))
ans+=fa[h1];//判断h1的父亲是否也是以y为根的子树的重心
if(ffa[y]==son[root])//y在以根节点重儿子为根的子树中
if(siz[son[root]]-siz[y]>=siz[son2])
ans+=root;//不影响重链,重心为根节点
else
{
ans+=ans2[siz[y]];
if(check2(fa[ans2[siz[y]]],y))
ans+=fa[ans2[siz[y]]];
}//影响重链
else//y不在以根节点重儿子为根的子树中
{
ans+=ans1[siz[y]];
if(check3(fa[ans1[siz[y]]],y))
ans+=fa[ans1[siz[y]]];
}
}
(感觉好多地方可以整合在一起写……凑合着看吧qwq)
下面给出完整代码:
//40分暴力
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#define ll long long
#define ano ((i-1)^1)+1
using namespace std;
const int N=3e5+100;
int T,n,cnt,tot;
ll ans;
int head[N],ver[2*N],Next[2*N];
int maxson[N],siz[N],d[N],nowh[N],nowsiz[N];
bool sp[N],v[N];
void add(int x,int y)
{
ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
ver[++tot]=x,Next[tot]=head[y],head[y]=tot;
}
void dfs(int x,int root)
{
if(x!=root)
nowsiz[x]=1;
v[x]=1;
for(int i=head[x];i;i=Next[i])
if(!sp[ver[i]] && !v[ver[i]])
{
int y=ver[i];
dfs(y,root);
maxson[x]=max(maxson[x],nowsiz[y]);
if(x!=root)
nowsiz[x]+=nowsiz[y];
}
if(maxson[x]*2<=nowsiz[root] && (nowsiz[root]-nowsiz[x])*2<=nowsiz[root])
nowh[++cnt]=x;
}//找重心
void pre(int x,int f,int root)
{
siz[x]=1,d[x]=d[f]+1;
for(int i=head[x];i;i=Next[i])
if(!d[ver[i]] && ver[i])
{
int y=ver[i];
if(x==root)
ffa[y]=y;
else
ffa[y]=ffa[f];
pre(y,x,root);
siz[x]+=siz[y];
}
}//预处理子树大小和深度
void clearly()
{
memset(maxson,0,sizeof(maxson));
memset(nowsiz,0,sizeof(nowsiz));
memset(v,0,sizeof(v));
cnt=0;
}
void solve()
{
pre(1,0,1);
for(int i=1;i<=tot;i+=2)
{
clearly();
int x=ver[i],y=ver[ano];
sp[x]=sp[y]=1;//dfs时不走x,y
if(d[x]>d[y])
swap(x,y);//令y为深度较大的节点
nowsiz[x]=n-siz[y],nowsiz[y]=siz[y];
dfs(x,x),dfs(y,y);
sp[x]=sp[y]=0;//还原
for(int j=1;j<=cnt;j++)
ans+=nowh[j];
}
}
void clear()
{
memset(head,0,sizeof(head));
memset(Next,0,sizeof(Next));
memset(siz,0,sizeof(siz));
memset(d,0,sizeof(d));
tot=ans=0;
}
int main()
{
scanf("%d",&T);
while(T--)
{
clear();
scanf("%d",&n);
for(int i=1,x,y;i<n;i++)
{
scanf("%d%d",&x,&y);
add(x,y);
}
solve();
printf("%lld\n",ans);
}
return 0;
}
//100分
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
#define ano ((i-1)^1)+1
using namespace std;
const int N=3e5+100;
int T,n,tot,root,son2,nowans;
ll ans;
int head[N],ver[2*N],Next[2*N];
int siz[N],son[N],ans1[N],ans2[N],ans3[N],ffa[N],d[N],od[N],fa[N];
void add(int x,int y)
{
ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
ver[++tot]=x,Next[tot]=head[y],head[y]=tot;
}
void findroot(int x,int f)
{
siz[x]=1;
for(int i=head[x],y=ver[i];i;i=Next[i],y=ver[i])
if(y!=f)
{
findroot(y,x);
siz[x]+=siz[y];
if(siz[y]>siz[son[x]])
son[x]=y;
}
if(siz[son[x]]*2<=n && (n-siz[x])*2<=n)
root=x;
}//找重心
void pre(int x,int f)
{
siz[x]=1,d[x]=d[f]+1,fa[x]=f;
if(f==root)
ffa[x]=x;
else
ffa[x]=ffa[f];
for(int i=head[x],y=ver[i];i;i=Next[i],y=ver[i])
if(y!=f)
{
pre(y,x);
siz[x]+=siz[y];
if(siz[y]>siz[son[x]])
{
if(x==root)
son2=son[x];
son[x]=y;
}
else
if(x==root && siz[y]>siz[son2])
son2=y;
}
}//预处理节点信息
void get1(int x,int f)
{
if(son[x])
get1(son[x],x);
while(n-2*siz[x]<=nowans && nowans)
{
ans1[nowans]=x;
nowans--;
}
}
void get2(int x,int f)
{
if(x==root)
nowans=n,get2(son2,x);
else
if(son[x])
get2(son[x],x);
while(n-2*siz[x]<=nowans && nowans)
{
ans2[nowans]=x;
nowans--;
}
}
void get3(int r)
{
if(son[r])
get3(son[r]);
for(int i=head[r],y=ver[i];i;i=Next[i],y=ver[i])
if(y!=son[r] && d[y]>d[r])
get3(y);
int now=son[r]?ans3[son[r]]:r;
while(siz[now]*2<siz[r])
now=fa[now];
ans3[r]=now;
}//预处理答案
void clear()
{
memset(head,0,sizeof(head));
memset(Next,0,sizeof(Next));
memset(ver,0,sizeof(ver));
memset(son,0,sizeof(son));
nowans=n,son2=tot=ans=0;
}
bool check1(int x,int y)
{
return x && siz[son[x]]*2<=siz[y] && (siz[y]-siz[x])*2<=siz[y];
}
bool check2(int x,int y)
{
if(x==root)
return siz[son2]*2<=n-siz[y];
return x && siz[son[x]]*2<=n-siz[y] && (n-siz[y]-siz[x])*2<=n-siz[y];
}
bool check3(int x,int y)
{
if(x==root)
return siz[son[x]]*2<=n-siz[y];
return x && siz[son[x]]*2<=n-siz[y] && (n-siz[y]-siz[x])*2<=n-siz[y];
}
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d",&n);
clear();
for(int i=1,x,y;i<n;i++)
{
scanf("%d%d",&x,&y);
add(x,y);
}
findroot(1,0);
memset(son,0,sizeof(son));
pre(root,0),get1(root,0),get2(root,0),get3(root);
for(int i=1;i<=tot;i+=2)
{
int x=ver[i],y=ver[ano];
if(d[x]>d[y])
swap(x,y);
int h1=ans3[y];
ans+=h1;
if(d[fa[h1]]>=d[y] && check1(fa[h1],y))
ans+=fa[h1];
if(ffa[y]==son[root])
if(siz[son[root]]-siz[y]>=siz[son2])
ans+=root;
else
{
ans+=ans2[siz[y]];
if(check2(fa[ans2[siz[y]]],y))
ans+=fa[ans2[siz[y]]];
}
else
{
ans+=ans1[siz[y]];
if(check3(fa[ans1[siz[y]]],y))
ans+=fa[ans1[siz[y]]];
}
}//枚举删边求答案
printf("%lld\n",ans);
}
return 0;
}
[CSP-S2019]树的重心 题解的更多相关文章
- CSP2019 树的重心 题解
本题当然可以通过大力讨论每棵子树的size的大小关系,然后用各种数据结构暴力维护.但是我更倾向于用一种更为性质的做法. 首先讲一下我在考场上想到的做法(没写).就是考虑换根,在换根的过程中计算每一条边 ...
- 【CSP模拟赛】仔细的检查(树的重心&树hash)
题目描述 nodgd家里种了一棵树,有一天nodgd比较无聊,就把这棵树画在了一张纸上.另一天nodgd更无聊,就又画了一张. 这时nodgd发现,两次画的顺序是不一样的,这就导致了原本的某一个节点 ...
- 题解-FJOI2014 树的重心
FJOI2014 树的重心 \(Q\) 组测试数据.给一棵树大小为 \(n\),求有多少个子树与其重心相同.重心可能有多个. 数据范围:\(1\le Q\le 50\),\(1\le n\le 200 ...
- POJ 1655 求树的重心
POJ 1655 [题目链接]POJ 1655 [题目类型]求树的重心 &题意: 定义平衡数为去掉一个点其最大子树的结点个数,求给定树的最小平衡数和对应要删的点.其实就是求树的重心,找到一个点 ...
- 洛谷P3345 [ZJOI2015]幻想乡战略游戏(动态点分治,树的重心,二分查找,Tarjan-LCA,树上差分)
洛谷题目传送门 动态点分治小白,光是因为思路不清晰就耗费了不知道多少时间去gang这题,所以还是来理理思路吧. 一个树\(T\)里面\(\sum\limits_{v\in T} D_vdist(u,v ...
- 点分治模板(洛谷P4178 Tree)(树分治,树的重心,容斥原理)
推荐YCB的总结 推荐你谷ysn等巨佬的详细题解 大致流程-- dfs求出当前树的重心 对当前树内经过重心的路径统计答案(一条路径由两条由重心到其它点的子路径合并而成) 容斥减去不合法情况(两条子路径 ...
- POJ 1655 - Balancing Act - [DFS][树的重心]
链接:http://poj.org/problem?id=1655 Time Limit: 1000MS Memory Limit: 65536K Description Consider a tre ...
- BZOJ 3510 - 首都 「 $LCT$ 动态维护树的重心」
这题 FlashHu 的优化思路值得借鉴 前置引理 树中所有点到某个点的距离和中,到重心的距离和是最小的. 把两棵树通过某一点相连得到一颗新的树,新的树的重心必然在连接原来两棵树重心的路径上. 一棵树 ...
- poj 1655 树的重心
Balancing Act Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 13178 Accepted: 5565 De ...
随机推荐
- 5.5 省选模拟赛 B Permutation 构造 贪心
LINK:Permutation 对于这种构造神题 我自然是要补的.为啥就我没想出来哇. 30分还是很好写的 注意8!实际上很小 不需要爆搜 写bfs记录状态即可.至于判断状态是否出现与否 可以开ma ...
- [C#] (原创)一步一步教你自定义控件——01,TrackBar
一.前言 技术没有先进落后之分,只有合不合适. WinForm有着非常多的优点,在使用WinForm久了之后,难免会觉得WinForm自带的某些控件外观上有些许朴素.或者功能上有些不如意,自然而然便想 ...
- Python网络数据采集PDF高清完整版免费下载|百度云盘|Python基础教程免费电子书
点击获取提取码:jrno 内容提要 本书采用简洁强大的 Python 语言,介绍了网络数据采集,并为采集新式网络中的各种数据类 型提供了全面的指导.第一部分重点介绍网络数据采集的基本原理:如何用 Py ...
- day2. 六大基本数据类型简介
一.基本数据类型 Number 数字类型 (int float bool complex) str 字符串类型 list 列表类型 tuple 元组类型 set 集合类型 dict 字典类型 二.Nu ...
- 15 张精美动图全面讲解 CORS
前言: 本文翻译自 Lydia Hallie 小姐姐写的
- C++类、函数、指针
1.初始化所有指针. 2. (1)指向常量的指针: (2)常量指针:指针本身为常量: 3.若循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环. 4.字符数组要注意字符串字面值 ...
- 618购物节要到了,Python帮你实现商品有货的微信提醒
时间过的真快,不知不觉一年已过了一半,又快到618购物狂欢节了,剁手党们都希望无论是618购物节还是双11购物节,都能够买到便宜又实惠的商品,但是真心实惠的东西往往是紧俏的,经常会出现无货的情况,我们 ...
- Hibernate配置文件和映射文件详解
Hibernate是一个彻底的ORM(Object Relational Mapping,对象关系映射)开源框架. 我们先看一下官方文档所给出的,Hibernate 体系结构的高层视图: 其中PO=P ...
- JS 鼠标、键盘事件对象
鼠标事件对象 mouseEvent鼠标事件对象 e.clientX 在可视区的x和y的坐标 e.pageX 在页面文档的X和Y的坐标 <script> docume ...
- 93复原IP地址。
from typing import List# 这道题不是很难,但是限制条件有很多.# 用递归的方法可以很容易的想到.只需要四层递归就好了.# 每次进行加上限制条件.过滤每一层就好了..class ...