学习博客:POJ-1741 (点分治模板)_Jacky_50的博客-CSDN博客
算法分析课设(四)采用分治法求树中任意两点之间距离小于K的点对及路径_Hillbox的博客-CSDN博客m
思想:
点分治是树分治的一种,树分治分为点分治和边分治
树的重心,也叫树的质心。即树的一个结点,把它删掉后的所有子树的最大结点数(相比于删掉其他结点)最小。换句话说,删除这个节点后最大连通块(一定是树)的节点数最少。
树的重心使得树尽可能的平衡,对于降低时间复杂度有帮助
求树的重心的复杂度为O(n)
主要看图,前面的数组表示点集,后面链接的数组表示边集。
参数d的作用:dis[i]+dis[j] <= K
且在去除重心后,i 与 j 不在同一个联通块里。
work的定义为:按照给定的根节点,所有以根节点为中间节点的dis[i]+dis[j] <= K
的数量,但是经过分治后,发现分治后的子树有所重复
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cmath>
#include<cstring>
///定义邻接表去存储树
#define add(u,v,w) (To[++num]=Head[u],Head[u]=num,V[num]=v,W[num]=w)///To表示边集,里面记录的是相同起点的上一条边;head表示点集,里面记录的是相同起点的最后一条边的编号;V代表边集的末端节点
#define For(x) for (int h=Head[x],o=V[h]; h; h=To[h],o=V[h])///用来遍历,h代表一条边的边号,默认一个顶点对应刚加入的第一条边的上一条边编号为0,表示没有,
#define Input for (int i=1,u,v,w; i<N; i++) scanf("%d%d%d",&u,&v,&w),add(u,v,w),add(v,u,w)///为什么是<N,因为树的边有N-1条
const int v=1e5+10;
using namespace std;
int mask[v];///用来标记当前节点是否被删除
int To[v*2],Head[v],V[v*2],W[v*2];
int siz[v],dis[v],cnt,mins,root;
int N,K,num;
int get_size(int x,int fa)///返回的是包括根节点的,整棵树的大小
{
siz[x]=1;
For(x) if (o!=fa && !mask[o])///如果到了叶子节点,就不会进for循环,siz[x]也便只能返回1,mask[o]确保只在分治的子树当中进行操作,为什么需要fa,因为fa还没有被mask标记
siz[x]+=get_size(o,x);
return siz[x];
}
void get_dis(int x,int d,int fa)//x 到重心的长度为 d,之后继续 dfs ,d应该相当于一个负重
{
dis[++cnt]=d;///这里d的作用是造出一棵当前子树分割前的树
For(x) if (o!=fa && !mask[o])
get_dis(o,d+W[h],x);
return;
}
void dfs_root(int x,int tot,int fa)///每一次分治后都要去算一次重心
{
//求目标子树的重心(要求除去 x 点时,它的 maxs 值最小,那么 x 就是这棵子树的重心了),其中 tot 是这棵子树的总大小(节点个数)
int maxs=tot-siz[x]; //这棵子树中x 父亲那一支先赋给 maxs
For(x) if (o!=fa && !mask[o])
{
maxs=max(maxs,siz[o]);
dfs_root(o,tot,x);
}
if (maxs<mins)
{
mins=maxs;
root=x;
}
return;
}
int work(int x,int d)///这个函数不是递归函数,实际上就是在进行一种合并的操作,对于这棵子树来讲,重复的节点的数量
{
//返回以 x 为根的子树内长度小于等于 K 的路径数(两个端点都在子树内)
//其实 d 在这里用处只有一个,是在做减法时方便把重心的儿子节点的 dis 先弄好,你也可以在分治的时候弄,不过就稍微有点麻烦了
cnt=0;
get_dis(x,d,0);
sort(dis+1,dis+cnt+1);
int daan=0,i=1,j=cnt;
while (i<j)
{
while (i<j && dis[i]+dis[j]>K) j--;
daan+=j-i; //相当于选一条路径 i,另一条可以为 [i+1,j] 里任意一条路径,这样得到的两个点之间长度(经过重心的那条路径)肯定是小于等于 K 的
i++;
}
return daan;
}
int dfs(int x)///x的作用是传入一棵树的一个起点,把这个起点先当做根进行遍历;这个函数的作用是求原问题的答案,在这个函数中体现了分治
{ ///在子树合并时,需要求得以经过合并的节点为中间节点的答案数量,但是涉及到了重复这样一种问题,
///也就是此合并节点不是联通块的直接父节点而是间接父节点,会导致加上直接父节点到合并节点的路径长度也有可能满足<=k这样一个条件,导致重复
mins=0x7fffffff;///对于每一个节点,所有子树中节点的最大值,在每一个节点中最小,用来记录最小的所有子树中节点的最大值
get_size(x,0);///0表示当前节点为根节点,siz[x]表示以x为根节点,没有被mask标记节点删除的树的节点个数
dfs_root(x,siz[x],0);///求重心
int ans=work(root,0);
mask[root]=1;
For(root) if (!mask[o])///为什么必须要mask,因为fa可能会再次遍历到,如果没有标记
{ //注意这里是以重心开始
ans-=work(o,W[h]); //注意,这里 dis[o] 要先赋成 W[h](即它到重心的距离)
ans+=dfs(o);
}
return ans;
} ///还有一种没有返回值的写法
int main()
{
while(scanf("%d%d",&N,&K)!=EOF && N && K)///节点的编号是从1开始的
{
Input;
int ans=dfs(1);
printf("%d\n",ans);
num=0;
for (int i=1; i<=N; i++) Head[i]=mask[i]=dis[i]=0;
}
return 0;
}
最后其时间复杂度我认为:O(n*(logn)^2)