点分治相关 学习笔记
点分治相关
前置知识:树的重心
1.定义:重心是指树中的一个节点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,这个节点被称为树的重心。
(注:这里的 子树 指的是无根树的子树,即包括向上的那棵子树,不包括整棵树自身* 。)
2.性质:
-
树的重心如果不唯一,则至多两个,且两个重心相邻。
-
以树的重心为根时,所有子树的大小都不超过整棵树大小的一半。
-
树上所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,到它们的距离和一样。
-
把两棵树通过一条边相连得到一棵新的树,那么新的树的重心在连接原来两棵树重心的路径上。
3.求法
记录 \(size_u\) 为 \(u\) 的最大子树节点数,\(sum_u\) 记录以 \(u\) 为根的子树节点数,所以\(n-sum\) 为 \(u\) 上面的部分的节点数,有 \(ans=\max(size,n-sum)\)。
然后在树上跑 dp 更新答案即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
int n;
struct edge{
int to,nxt;
}e[2*maxn];
int head[maxn],edgenum;
void add_edge(int u,int v){
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
head[u]=edgenum;
}
int siz[maxn],ans=1e9,pos;//pos为重心位置
void dfs(int u,int fa){
siz[u]=1;
int mx=0;//存储将该点删去后 剩下各联通块大小的最大值
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa) continue;
dfs(v,u);
siz[u]+=siz[v];
mx=max(mx,siz[v]);
}
mx=max(mx,n-siz[u]);
if(mx<ans) ans=mx,pos=x;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1,u,v;i<n;i++){
cin>>u>>v;
add_edge(u,v);
add_edge(v,u);
}
dfs(1,0);
cout<<ans<<endl;
}
点分治
1.定义
点分治,是一种在树上进行路径静态统计的算法,与树剖不同,并不是维护路径最值,路径和之类的统计。点分治本质上是将一棵树拆分成许多棵子树处理,并不断进行。对于点分治能解决的问题,都与树上路径的统治和询问有关。
树上的路径可以分为两类,一类是经过根节点的路径,一类是不经过根节点的路径。
对于不经过根的路径,它们一定在根的某个子节点所构成的子树中,然后我们可以通过递归找到这个路径所经过的根节点。
这样就对答案进行了划分。树T的答案=T中经过根的路径的答案+根的所有子节点构成子树的答案。对于第二部分的答案,可以递归处理。我们只需要考虑如何计算第一部分的答案,即计算经过根的路径的个数。
2.具体做法
对于经过根节点的路径,可以预处理出每个点到根节点的路径,然后 \(dis[u,v]=dis[u,root]+dis[v,root]\) 。这时要注意排除不合法路径(即 \(u,v\) 在同一棵子树内,两者之间的路径不经过根节点),先把前面子树中各点到根的距离存入一个队列 \(q_i\) ,开一个 bool 数组存队列中的距离 \(judge_i\) (其实也就相当于一个桶)。再枚举当前子树中各点到根的距离 \(dis_j\) ,若询问 \(k\) 与 \(dis_j\) 的差存在,说明此解合法。
对于不经过根节点的路径,可以对子树不断分治,转化为经过根节点的路径。
3.例题
代码
/*点分治
1.找出树的重心做根
2.求出子树中各点到根节点的距离
3.对当前树统计答案
4.分治各个子树,重复以上操作*/
#include<bits/stdc++.h>
using namespace std;
const int maxn=10005;
const int inf=10000005;
struct edge{
int to,nxt,val;
}e[2*maxn];
int head[maxn],edgenum;
void add_edge(int u,int v,int w){
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
e[edgenum].val=w;
head[u]=edgenum;
}
int del[maxn],siz[maxn],mxs,sum,root;//del表示此点是否作为重心分治过删掉过
int dis[maxn],d[maxn],cnt;//dis表示到根节点的距离
int ans[maxn],q[inf],judge[inf];
int n,m,ask[maxn];
void getroot(int u,int fa){//找当前 u 子树的重心root 作为根节点
siz[u]=1;
int s=0;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa || del[v]) continue;
getroot(v,u);
siz[u]+=siz[v];
s=max(s,siz[v]);
}
s=max(s,sum-siz[u]);
if(s<mxs) mxs=s,root=u;
}
void getdis(int u,int fa){//求子树所有点到重心的距离
dis[++cnt]=d[u];
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa || del[v]) continue;
d[v]=d[u]+e[i].val;
getdis(v,u);
}
}
void calc(int u){
del[u]=judge[0]=1;//judge[0]=1,说明距离为0的点是存在的,就是根节点到自己的距离
int p=0;//计算经过根u的路径
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(del[v]) continue;
cnt=0;//用于记录v这棵子树的节点到根节点的路径条数
d[v]=e[i].val;//v到u的初值 是 uv 这条边的权值
getdis(v,u);//求出v这个子树每个点到u的距离(获取子树节点到根节点的距离
for(int j=1;j<=cnt;j++){
for(int k=1;k<=m;k++){
if(ask[k]>=dis[j]){//dis[j]记录的是 本次遍历的子树节点到根节点距离为dis的距离是否存在
ans[k]|=judge[ask[k]-dis[j]];//judge 是其他子树
}
}
}
for(int j=1;j<=cnt;j++){
if(dis[j]<inf){
q[++p]=dis[j],judge[q[p]]=1;
}
}
//记录下合法距离
//将本棵子树记录下的路径距离存进judge数组,对于下一棵子树而言,本次的距离就是可以组合的
}
for(int i=1;i<=p;i++) judge[q[i]]=0;
}
void divide(int u){
//计算经过根u的路径
calc(u);
del[u]=1;
//对u的子树进行分治
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(del[v]) continue;
mxs=sum=siz[v];//对哪个子树求根,就用哪个子树大小初始化mxs和sum
getroot(v,0);//求子树v的根(重心
divide(root);//分治
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1,u,v,w;i<n;i++){
cin>>u>>v>>w;
add_edge(u,v,w),add_edge(v,u,w);
}
for(int i=1;i<=m;i++){
cin>>ask[i];
}
mxs=sum=n;
getroot(1,0);//任意找一个点开始,找重心
getroot(root,0);//重构siz 求以 root为根的每个子树的节点个数
divide(root);
for(int i=1;i<=m;i++){
if(ans[i]) cout<<"AYE"<<endl;
else cout<<"NAY"<<endl;
}
}
点击查看代码
/*对距离为i的点建立 tmp[i] 表示在当前递归到的子树中,走到距离为i的顶点最少需要走多少边
点分治,对每棵子树遍历,求出每个点i到root的距离 dis[i],走过的边数d[i]
然后就有 ans=min(ans,tmp[k-dis[i]]+d[i])
这里特别注意一下初始化和清空
遍历完这棵子树再修改被访问了的 tmp[dis[i]] 然后下一棵
所有子树遍历完了之后 再遍历一遍所有节点 把修改到的tmp值变回inf
*/
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
const int inf=1e9;
int n,k,ans=inf;
struct edge{
int to,nxt,val;
}e[2*maxn];
int head[maxn],edgenum;
void add_edge(int u,int v,int w){
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
e[edgenum].val=w;
head[u]=edgenum;
}
int del[maxn];
int siz[maxn],mxs,sum,root;
void getroot(int u,int fa){
siz[u]=1;
int s=0;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa || del[v]) continue;
getroot(v,u);
siz[u]+=siz[v];
s=max(s,siz[v]);
}
s=max(s,sum-siz[u]);
if(s<mxs) mxs=s,root=u;
}
int dis[maxn],d[maxn],tmp[10*maxn];
//d[i]表示当前树走到i点走过的边数 dis是距离
void getdis(int u,int fa){
if(dis[u]<=k){
ans=min(ans,tmp[k-dis[u]]+d[u]);
}
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa || del[v]) continue;
dis[v]=dis[u]+e[i].val;
d[v]=d[u]+1;
getdis(v,u);
}
}
/*getdis(v,0) 后每个子树会得到每个点到根节点的dis 然后尝试更新这个距离的tmp
fl表示求完当前子树的信息来更新tmp / 准备处理下一棵子树需要把tmp初始化为inf
*/
void update(int u,int fa,int fl){
if(dis[u]<=k){
if(fl) tmp[dis[u]]=min(tmp[dis[u]],d[u]);//fl==1 代表更新
else tmp[dis[u]]=inf;//否则是初始化
}
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa || del[v]) continue;
update(v,u,fl);
}
}
void solve(int u){
del[u]=1;
tmp[0]=0;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(del[v]) continue;
dis[v]=e[i].val;
d[v]=1;
getdis(v,0);
//以v为根 求其他节点到v的距离
update(v,0,1);
//用v子树上的点尝试更新各个长度的tmp
}
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(!del[v]) update(v,0,0);
//把没被当作根分治的节点v的 所有子树中节点的tmp被初始化为inf
}
//分治
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(del[v]) continue;
sum=mxs=siz[v];
getroot(v,0);
solve(root);
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>k;
for(int i=1;i<=k;i++){
tmp[i]=inf;
}
for(int i=1,u,v,w;i<n;i++){
cin>>u>>v>>w;
u++,v++;
add_edge(u,v,w),add_edge(v,u,w);
}
sum=mxs=n;
getroot(1,0);
solve(root);
if(ans!=inf) cout<<ans<<endl;
else cout<<-1<<endl;
}
靠近