树的分治
树的分治分为点分治和边分治,在这里先介绍点分治,边的分治以后补充
点分治:
点分治最重要的一点就是找树的重心,在将点分治之前先介绍什么是树的重心
树的重心:树的重心也叫树的质心。对于一棵树n个节点的无根树,找到一个点,使得把树变成以该点为根的有根树时,最大子树的结点数最小。
只看定义的话对于初学者来说不是很好理解,简单的说,树的重心就是找到树中的一个节点,然后从该节点将树断开。那么这棵树将会分成几颗子树,其中最大的子树节点数最小,那么这个节点就是树的重心
举个例子:
上图从三号节点断开则最大子树的节点树为3,从1号节点断开,则最大子树的节点数为9,从4号节点断开,最大子树的节点数为8,从其他节点断开最大子树的节点数都会比从3号节点断开要大,所以3号节点为这棵树的重心
再求树的重心之前还要补充一个知识,链式前向星,用链式前向星来存图;
链式前向星参考链接 https://blog.csdn.net/qq_40707370/article/details/83827603
存树:
void add(int u,int v,int w)//加边
{//cnt初始化0
e[cnt].to=v;
e[cnt].w=w;
e[cnt].next=head[u];
head[u]=cnt++;
}
怎么求一棵树的重心,根据定义,我们要先求出每一课子树的大小,然后根据每一颗子树的大小来求树的重心
求每一颗子树的大小
void dfs_size(int u,int pre)//遍历整棵树并计算以u为根的树中每颗子树的大小,u表示当前访问的顶点,pre表示上次访问的顶点
{
size[u]=1;//以u为根树的大小初始化为1
maxv[u]=0;//以u为根树的最大子树的大小,初始化为0
for(int i=head[u];~i;i=e[i].next)
{
int to=e[i].to;//树根u的一个孩子节点
if(vis[to]||to==pre)
continue;
dfs_size(to,u);
size[u]+=size[to];//更新以u为根树的大小,
maxv[u]=max(maxv[u],size[to]);//更新最大子树的大小
}
}
求树的重心:
注意:以u为根会将树分为两部分,所以此时最大子树为这两部分较大的那一个,画个图就明白了
void dfs_root(int r,int u,int pre)//找出以r为根的树的重心,u为当前访问的节点,pre为上次访问的节点
{
maxv[u]=max(maxv[u],size[r]-size[u]);//以u作为根节点将一棵树分为两部分,最大子树的大小为这两部分之一
if(Max>maxv[u])
{
Max=maxv[u];//更新最大子树的大小
root=u;//以r为根树的重心
}
for(int i=head[u];~i;i=e[i].next)
{
int to=e[i].to;
if(to==pre||vis[to])
continue;
dfs_root(r,to,u);
}
}
分治思想:
例如给你一颗n个顶点的树其中连接顶点a,b的长度为l,问问最短距离不超过k的顶点的对数:
这时我们可以这样做,将树按重心s划分,那么最短距离不超过k的顶点对只可能有两行情况,两个顶点存在以s根根的同一颗子树中,两个顶点不再同一颗子树中,那么如何不重不漏的计算出所有点对呢?可以先计算出所有节点到重心的距离dis,(重心到重心的距离为0)找到所有满足dis[i]+dis[j]<=k的点数ans1,既然是分治思想,那么就是将一颗树分成更小的树,然后求满足题意的点对,这时我们就要减去两个点对在同一颗子树内的情况,即减去两个点对在同一颗子树内的数量ans2,所以当前结果为ans=ans1-ans2,然后在计算子树内符合点对的数量,分析和上述相同,最后的结果是所有的ans累加
经典例题:POJ - 1741
AC代码:
第一次写点分治的代码,看了好长时间才做出来QAQ
#include<iostream>
#include<algorithm>
#include<vector>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=1e5;
struct edge{
int to;
int w;
int next;
}e[maxn];
int n,k,root,Max,ans;
int size[maxn],maxv[maxn],head[maxn],cnt;
vector<int> dis;
bool vis[maxn];
void add(int u,int v,int w)//加边
{
e[cnt].to=v;
e[cnt].w=w;
e[cnt].next=head[u];
head[u]=cnt++;
}
void dfs_size(int u,int pre)//遍历整棵树并计算以u为根的树中每颗子树的大小,u表示当前访问的顶点,pre表示上次访问的顶点
{
size[u]=1;//以u为根树的大小初始化为1
maxv[u]=0;//以u为根树的最大子树的大小,初始化为0
for(int i=head[u];~i;i=e[i].next)
{
int to=e[i].to;//树根u的一个孩子节点
if(vis[to]||to==pre)
continue;
dfs_size(to,u);
size[u]+=size[to];//更新以u为根树的大小,
maxv[u]=max(maxv[u],size[to]);//更新最大子树的大小
}
}
void dfs_root(int r,int u,int pre)//找出以r为根的树的重心,u为当前访问的节点,pre为上次访问的节点
{
maxv[u]=max(maxv[u],size[r]-size[u]);//以u作为根节点将一棵树分为两部分,最大子树的大小为这两部分之一
if(Max>maxv[u])
{
Max=maxv[u];//更新最大子树的大小
root=u;//以r为根树的重心
}
for(int i=head[u];~i;i=e[i].next)
{
int to=e[i].to;
if(to==pre||vis[to])
continue;
dfs_root(r,to,u);
}
}
void dfs_dis(int u,int pre,int dist)//计算每一个顶点到重心的距离
{
dis.push_back(dist);//将当前节点到重心的距离存在dis中
for(int i=head[u];~i;i=e[i].next)
{
int to=e[i].to;
int w=e[i].w;
if(vis[to]||to==pre)
continue;
dfs_dis(to,u,dist+w);
}
}
int num(int rt,int dist)//计算所有到重心距离小于k的两个点对数
{
dis.clear();
dfs_dis(rt,-1,dist);
sort(dis.begin(),dis.end());//从小到大排序
int i=0,j=dis.size()-1,ans1=0;
while(i<j)
{
while(dis[i]+dis[j]>k&&i<j)
j--;
ans1+=j-i;
i++;
}
return ans1;
}
void dfs(int u)//计算以u为根的数中不再同一颗子树的两个点对距离小于k的数目
{
Max=n;//最大子树的大小初始化为整棵树的大小
dfs_size(u,-1);
dfs_root(u,u,-1);
int rt=root;//以u为根树的重心
ans+=num(rt,0);
vis[rt]=1;
for(int i=head[rt];~i;i=e[i].next)
{
int to=e[i].to;
int w=e[i].w;
if(vis[to])
continue;
ans-=num(to,w);//减去两个顶点在同一颗子树的情况
dfs(to);//计算儿子节点符合条件的点对数
}
}
int main()
{
while(cin>>n>>k,n+k)
{
int u,v,w;
cnt=0;
memset(head,-1,sizeof(head));
memset(vis,0,sizeof(vis));
for(int i=1;i<n;i++)
{
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
add(v,u,w);
}
ans=0;
dfs(1);
cout<<ans<<endl;
}
return 0;
}