点分治模板
这两天跟着学了一手树上点分治模板,然后有一些感悟,决定来写一发博客.
首先,鉴于鄙人的经验,如果想要较快速地学习一个新算法,肯定还是先看一道经典的例题比较好,所以我们先来一道例题.
Luogu P4178 Tree
题目描述
给你一棵TREE,以及这棵树上边的距离.问有多少对点它们两者间的距离小于等于K
输入输出格式
输入格式:
N(n<=40000) 接下来n-1行边描述管道,按照题目中写的输入 接下来是k
输出格式:
一行,有多少对点之间的距离小于等于k
点分治讲解:
所谓点分治,顾名思义,就是将一棵树上的点按照一定的顺序分开来处理.然后我们这里找的是一棵树的重心.然后以重心为根.并且递归处理每一棵子树.
所谓树的重心,其实就是找一个结点为根,然后使得它的子树里面子节点数最多的子树子节点数最小.
Q: 为什么要找重心?
A其实想一想传统的朴素算法时间超限的主要原因就是因为每次去找到一个点的距离,都因为树的深度可能会比较深 ( 尤其可能会退化成一条链 ) ,而导致每一次递归的次数很多.所以就会效率很低.
然而其实和 splay 相似的目的,我们使用重心也是为了使得每一次操作的深度都尽可能低.然而重心又满足上文中的性质.
然后我们来看一手如何找到重心.
int getroot(int u,int fa) { size[u]=1; maxson[u]=0; for(int i=head[u];i;i=a[i].next) { int tt=a[i].to; if(tt!=fa&&!vis[tt]) { getroot(tt,u); size[u]+=size[tt]; maxson[u]=max(maxson[u],size[tt]); } } maxson[u]=max(maxson[u],S-size[u]); if(maxson[u]<mmx) root=u,mmx=maxson[u]; }
在上面这个代码中,size 代表的是每一个结点的子树的结点个数.
然后 maxson 代表的是当前这个结点的子树里面最多的子节点个数, vis 代表的是当前这个点是否已经被删除过..
maxson[u]=max(maxson[u],S-size[u]);
然后这一行,因该也比较好理解.因为除了它当前的子节点外,它的父亲那边也还连着一棵树.
这样的话其实是 时间复杂度是 O (n);
找完 root 之后就是分治了. 那么具体怎么分治呢,我们也来看一手代码.
void divide(int rt) { ans=ans+findans(rt,0); vis[rt]=true; for(int i=head[rt];i;i=a[i].next) { int tt=a[i].to; if(!vis[tt]) { ans-=findans(tt,a[i].w); S=size[tt]; root=0; mmx=inf; getroot(tt,0); divide(root); } } return; }
在这其中,S代表的是当前尚还剩余的结点个数.
然后在这里,我们做的操作是递归处理每一棵子树.然后外加统计答案.
统计答案这里有两次:
ans=ans+findans(rt,0);
这一次是将当前这棵子树里面所有的距离小于等于K 的点对全部找出来.
然后另外一次:
ans-=findans(tt,a[i].w);
这里是将一开始多统计的减去.
这个东西应该也比较好理解.(这个和后面统计答案的方式有关).
因为我们统计答案的时候总是先处理一遍所有的点到当前根节点的距离,然后通过队列一次次求和然后比较.然后统计得出答案.
这样我们很明显只考虑了点与点之间的距离关系( 也是为了更加简便 否则我们需要去考虑其他一些问题会极大的将问题复杂化 ).
会有一些点是不合法的,比如说两个点在一条路径上或者统计的两个点到根构成的路径根本就不是一条简单路径 (一条链) .
但是因为我们只考虑了距离问题,所以只要两个点到根节点的距离之和小于K,我们也将其计入.
于是乎此时我们会发现,其实我们一开始已经将所有的路径(包括不合法) 都已经计入,然后在之后的子树处理中,我们就需要将多出来的删去.
以上,似乎就是我对分治的理解,相信我,我会填 动态点分治 和 分治CDQ 这两个坑的 还有后面的习题也会填上的.
例题代码
// luogu-judger-enable-o2 #include<bits/stdc++.h> using namespace std; const int maxn=40008; const int inf=1e10+9; int k; struct sj{ int to; int next; int w; }a[maxn*2]; int sizeb,head[maxn]; int ans,S; // S 是当前这棵树的结点数量 int dep[maxn],vis[maxn]; // vis 代表当前这个点是否已经被删除 int fa[maxn],n,size[maxn]; void add(int x,int y,int z) { a[++sizeb].to=y; a[sizeb].next=head[x]; head[x]=sizeb; a[sizeb].w=z; } int root; int maxson[maxn],mmx; int getroot(int u,int fa) { size[u]=1; maxson[u]=0; for(int i=head[u];i;i=a[i].next) { int tt=a[i].to; if(tt!=fa&&!vis[tt]) { getroot(tt,u); //递归来得到size数组 size[u]+=size[tt]; maxson[u]=max(maxson[u],size[tt]); } } maxson[u]=max(maxson[u],S-size[u]); //除了它下面的子节点外 还有一棵经过经过父亲结点的树 if(maxson[u]<mmx) root=u,mmx=maxson[u]; } int dis[maxn],summar; void getdis(int u,int fa,int dist) { dis[++summar] = dist; for(int i=head[u];i;i=a[i].next) { int tt=a[i].to; if(!vis[tt]&&tt!=fa) getdis(tt,u,dist+a[i].w); } //通过递归求出每个点到当前这个根的距离 } int findans(int sta,int len) { summar=0; memset(dis,0,sizeof(dis)); getdis(sta,0,len); sort(dis+1,dis+summar+1); //summar 是从当前这个根结点出发可以到达 int ll=1,rr=summar,tep=0; while(ll<=rr) if(dis[rr]+dis[ll]<=k)tep=tep+rr-ll,ll++; else rr--; // 队列求出所有在距离上满足要求的点 return tep; } void divide(int rt) { ans=ans+findans(rt,0); vis[rt]=true; for(int i=head[rt];i;i=a[i].next) { int tt=a[i].to; if(!vis[tt]) { ans-=findans(tt,a[i].w); S=size[tt]; root=0; mmx=inf; getroot(tt,0); divide(root); } } return; } int main() { scanf("%d",&n); int x,y,z; for(int i=1;i<n;i++) {scanf("%d%d%d",&x,&y,&z);add(x,y,z);add(y,x,z);} scanf("%d",&k); mmx=inf; S=n; getroot(1,0); divide(root); cout<<ans<<endl; return 0; }