【例题1】poj 1741 Tree
- n个点的树,每条边都有一个权值。
- 两点路径长度就是路径上各边权值之和。
- 求长度不超过K的路径有几条。
【分析】
本题中树的边是无向的,即是一个n个点,n-1条边构成的无向图。
这种树可以看成无根树,可以指定节点p为根。
对p而言,树上路径可以分为两类:
- 经过根节点p;
- 包含于p的某一棵子树中。
设点 i 的深度为 Depth[i],父亲为 Parent[i] 。
若 i 为根,则 Belong[i] = -1,若 Parent[i] 为根,则 Belong[i]=i,
否则 Belong[i] = Belong[ Parent[i] ]。可以通过一次BFS求得。
我们的目标是要统计,有多少对 (i,j) 满足:
i<j,Depth[i]+Depth[j]<=K,Belong[i]!=Belong[j]。
如果这样考虑问题会变得比较麻烦,我们可以考虑换一种角度:
设 X 为满足 i<j,Depth[i]+Depth[j]<=K 的数对 (i,j) 的个数
设 Y 为满足 i<j,Depth[i]+Depth[j]<=K 且 Belong[i]=Belong[j] 数对 (i,j) 的个数
那么我们要统计的量便等于X-Y。
求 X、Y 的过程均可以转化为以下问题:
已知 A[1],A[2],...A[m],求满足 i<j 且 A[i]+A[j]<=K 的数对 (i,j) 的个数。
对于这个问题,我们先将 A 从小到大排序。
设 B[i] 表示满足 A[i]+A[p]<=K 的最大的 p (若不存在则为0)。
我们的任务便转化为求出 A 所对应的 B 数组。
那么,若 B[i]>i,那么i对答案的贡献为 B[i]-i 。
显然,随着 i 的增大,B[i] 的值是不会增大的。
利用这个性质,我们可以在线性的时间内求出 B 数组,从而得到答案。
我们在每一棵子树中选择“最优”的点分割。
所谓“最优”,是指 [ 删除这个点后,最大的子树尽量小 ] 。
这个点就是重心,可以通过树形DP在O(N)时间内求出。
这样一来,即使是遇到一根链的情况时,L的值也仅仅是 O(logN) 的。
简单来说:点分治就是每次找到重心,然后把重心去掉。
对分成的每两棵树之间分别统计路径信息。
(以重心的每个相邻点为根,遍历整棵子树即可得到这个根到每个结点的统计信息)
就可以知道包含这个重心的所有路径的信息,
然后对于剩下的路径就是在子树里面进行同样的操作了,直到只剩一个点为止
(注意这一个点所构成的路径有时也要处理一下)。
边分治就是每次找到一条边,使得删掉这条边后分成的两棵子树大小尽可能平均,
然后以删掉的边的两端点为根,分别统计根到两棵树中的每个结点的路径信息,
最后合并算路径,即可得到包含这条边的所有路径的信息,剩下的路径在两棵树中递归处理。
【代码实现】
1.为了避免变量名指代不清的问题,我们先规定一下各变量的含义。
const int N = 10005;
struct edge{ int to,next,w; }a[N<<1]; //边集数组
int n,k,head[N],cnt; //n,k不解释,head[]和cnt是边集数组的辅助变量
int root,sum; //当前查询的根,当前递归这棵树的大小
int vis[N]; //某一个点是否被当做根过
int sz[N]; //每个点下面子树的大小
int f[N]; //每个点为根时的最大子树大小
int dep[N]; //每个点的深度
int o[N]; //每个点的深度(用于排序)
//(这个是以poj1741为例,其他题目不一定要用到这个)
int ans; //最终统计的答案
2.点分治的核心是找一个点作为根,而找出来的这个点就是我们所说的“重心”。
void getroot(int u,int fa){ //dfs寻找重心
sz[u]=1; f[u]=0;
for(int e=head[u];e;e=a[e].next){
int v=a[e].to;
if (v==fa||vis[v]) continue;
getroot(v,u); //dfs
sz[u]+=sz[v];
f[u]=max(f[u],sz[v]);
}
f[u]=max(f[u],sum-sz[u]);
if(f[u]<f[root]) root=u;
}
3.每次找出一个根以后,所有点对就只有两种可能了:
1)两个点都在根的某一棵子树中,即路径不过根;
2)两个点在根的不同子树中,或其中一个点就是根,此时路径必过根。
4.完整代码:
#include <bits/stdc++.h> //poj不让用的
using namespace std;
typedef long long ll;
const int N = 10005;
struct edge{ int to,next,w; }a[N<<1]; //边集
int n,m,k,head[N],cnt; //head[]和cnt是边集数组的辅助变量
int root,sum; //当前查询的根,当前递归的这棵树的大小
int vis[N]; //某一个点是否被当做根过
int sz[N]; //每个点下面子树的大小
int f[N]; //每个点为根时,最大子树大小
int dep[N]; //每个点的深度(此时是与根节点的距离)
int o[N]; //每个点的深度(用于排序)
int ans;//最终统计的答案
int reads(){ //读入优化
int x=0,w=1; char ch=getchar();
while((ch<'0'||ch>'9')&&ch!='-') ch=getchar();
if(ch=='-') w=0,ch=getchar();
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return w?x:-x;
}
void getroot(int u,int fa){ //dfs求重心和子树大小
sz[u]=1; f[u]=0;
for(int e=head[u];e;e=a[e].next){
int v=a[e].to;
if(v==fa||vis[v]) continue;
getroot(v,u); sz[u]+=sz[v];
f[u]=max(f[u],sz[v]);
}
f[u]=max(f[u],sum-sz[u]); //注意:可能是另外一半的树
if(f[u]<f[root]) root=u; //求出重心
}
void getdeep(int u,int fa){ //dfs求出与根节点的距离
o[++cnt]=dep[u]; //用于排序
for(int e=head[u];e;e=a[e].next){
int v=a[e].to;
if(v==fa||vis[v]) continue;
dep[v]=dep[u]+a[e].w;
getdeep(v,u);
}
}
int calc(int u,int d0){
//↑↑↑此时以u为根节点,统计子树中符合条件的点对个数
cnt=0; dep[u]=d0;
getdeep(u,0);
sort(o+1,o+cnt+1);
int l=1,r=cnt,res=0;
while(l<r){
if(o[l]+o[r]<=k)
res+=r-l,l++;
else r--;
}
return res;
}
void solve(int u){
ans+=calc(u,0); vis[u]=1;
for(int e=head[u];e;e=a[e].next){
int v=a[e].to;
if(vis[v]) continue;
ans-=calc(v,a[e].w); //???
sum=sz[v]; root=0;
getroot(v,0); solve(root);
}
}
int main(){
while(1){
n=reads(); k=reads();
if(n==0&&k==0) return 0;
memset(head,0,sizeof(head));
memset(vis,0,sizeof(vis));
int u,v,w; cnt=0; ans=0;
for(int i=1;i<n;i++){
u=reads(); v=reads(); w=reads(); //↓前向星
a[++cnt]=(edge){v,head[u],w}; head[u]=cnt; //poj也不让这样写
a[++cnt]=(edge){u,head[v],w}; head[v]=cnt;
}
root=0; sum=f[0]=n;
getroot(1,0); solve(root); //从重心开始点分治
printf("%d\n",ans);
}
return 0;
}
【例题2】Luogu 3806 点分治模板
- 询问距离为k的点对是否存在。
#include <bits/stdc++.h>
using namespace std;
struct data{
int nextt,to,w;
}p[20010]; //邻接表存边
int head[10010],tot;
int sum; //子树的总点数
int root; //当前子树的根
int f[10010],son[10010];
//↑↑↑ f为除去根时得到的最大连通块,son为以i为根的子树的节点
bool vis[10010]; //是否访问(标记)
int d[10010]; //i到当前根的距离
bool okk[10000100]; //bool型数组,k是否存在
struct node{
int dis,which;
}rp[1001000];
int tt;
void add(int x,int y,int w){ //建边
p[++tot]=(data){head[x],y,w};
head[x]=tot;
}
void getroot(int u,int papa){ //找重心
son[u]=1; f[u]=0;
for(int i=head[u];i;i=p[i].nextt){
int v=p[i].to;
if(vis[v]||v==papa) continue;
getroot(v,u);
son[u]+=son[v];
f[u]=max(f[u],son[v]); //注意:f存的是最大连通块
}
f[u]=max(f[u],sum-son[u]);
if(f[u]<f[root]) root=u; //更新重心
}
int pp; //以同一点为根的子树编号
void getdeep(int rearoot,int u,int fa,int ro){ //得到点到根的距离
for(int i=head[u];i;i=p[i].nextt){
int v=p[i].to;
if(vis[v]||v==fa) continue; //去重
if(u==rearoot) pp++; //子树编号
d[v]=d[u]+p[i].w;
if(u==rearoot) rp[++tt]=(node){d[v],pp};
else rp[++tt]=(node){d[v],ro};
okk[d[v]]=1; //更新k的可能值
if(rearoot==u) getdeep(rearoot,v,u,pp);
else getdeep(rearoot,v,u,ro); //下一个
}
}
void getans(int u) {
d[u]=0; tt=0; pp=0;
getdeep(u,u,0,0); vis[u]=1;
for(int i=1;i<=tt;i++) //!!!!!注意,十分重要
for(int j=i+1;j<=tt;j++)
if(rp[i].which!=rp[j].which)
ko[rp[i].dis+rp[j].dis]=1;
for(int i=head[u];i;i=p[i].nextt){
int v=p[i].to;
if(!vis[v]){ //去重
root=0; // !!
sum=son[v]; // !!
getroot(v,0);//得到子树的重心
getans(root); //以重心建树
}
}
}
int main(){
int n,m,a,b,c; scanf("%d%d",&n,&m);
for(int i=1;i<=n-1;i++){ //树的特点:n个点,n-1条边
scanf("%d%d%d",&a,&b,&c);
add(a,b,c); add(b,a,c); //建边
}
sum=f[0]=n; root=0; //【注意】sum、root必须初始化
getroot(1,0); getans(root); //找重心,以重心建树找k值
for(int i=1;i<=m;i++){ //判断是否存在
int kk; scanf("%d",&kk);
if(okk[kk]==1) printf("AYE\n");
else printf("NAY\n");
}
return 0;
}
——时间划过风的轨迹,那个少年,还在等你。