[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 ...
随机推荐
- 字典内置函数&方法
字典内置函数&方法 Python字典包含了以下内置函数:高佣联盟 www.cgewang.com 序号 函数及描述 1 cmp(dict1, dict2)比较两个字典元素. 2 len(dic ...
- luogu P4884 多少个1?
LINK:多少个1? 题目要求:\(\sum_{i=0}^{n-1}10^i \equiv k \mod m\) 最小的n. 看起来很难求的样子 这个同余式 看起来只能暴力枚举. 不过既然是同余 我们 ...
- idea安装testng插件后,无法使用调用testng里面的类
1.已经安装好idea的testng插件,并且应用上相关的两个插件. 2.已经导入maven仓库的testng.jar,并且重启过idea后:发现不能调用. 本人在百度很多相关资料,始终没有找到解决方 ...
- 安装ElasticSearch遇到的深坑
实验需要ES,安装过程中遇到一些奇葩的问题,记录下.下面介绍下安装步骤: 第一步:安装java ES是运行在java虚拟机上面的,所以首先需要安装java环境,安装过程不再赘述,唯一需要注意的是ES对 ...
- 面试题:JVM 堆内存溢出后,其他线程是否可继续工作?
来源:http://sina.lt/gqaM 最近网上出现一个美团面试题:“一个线程OOM后,其他线程还能运行吗?”.我看网上出现了很多不靠谱的答案.这道题其实很有难度,涉及的知识点有jvm内存分配. ...
- 017_go语言中的指针
代码演示 package main import "fmt" func zeroval(ival int) { ival = 0 } func zeroptr(iptr *int) ...
- MYSQL 按某个字段分组,然后取每组前3条记录
先初始化一些数据,表名为 test ,字段及数据为: SQL执行结果为:每个 uid 都只有 3 条记录. SQL语句为: SELECT * FROM test main WHERE ...
- redis(二)redis的主从模式和集群模式
redis(二)redis的主从模式和集群模式 主从模式 集群模式 主从模式 redis的主从模式,指的是针对多台redis实例时候,只存在一台主服务器master,提供读写的功能,同时存在依附在这台 ...
- SpringBoot进阶教程(六十三)Jasypt配置文件加密
数据库密码直接明文写在配置中,对安全来说,是一个很大的挑战.一旦密码泄漏,将会带来很大的安全隐患.尤其在一些企业对安全性要求很高,因此我们就考虑如何对密码进行加密.本文着重介绍Jasypt对Sprin ...
- 存储系列之 VFS虚拟文件系统简介
引言:文件系统发展到一定阶段,开始进一步抽象和分层. 前面我们介绍了ext系列文件系统和xfs文件系统,这些是Linux使用最多的文件系统,也是很多发布版本默认选择的文件系统.而事实上,Linux ...