点分治算法是一种对树上路径统计极其有效的算法。在要求求解树上路径等方面的问题时,我们可以思考能否用点分治解决。
分治是将一个问题拆解成几个子问题的递归解决问题的方法,而在树上的分治受到了树的形状的影响。
我们看到,加入根节点选取的不合适,造成一棵树的深度过于的大,那么时间复杂度也是极高的,因此我们需要选取合适的点,使得最大子树的最小。而满足这个条件的点我们称之为树的重心,其可以通过一个dfs来获得
int get_root(int u,int fa)//找出树的重心
{//siz[i]是指以i为根子树的大小,maxson[i]是指以i为根最大的子树的大小
//SIZE 为当前处理的这棵树的大小 maxx代表已经找到的最大子树的最小值
siz[u] = 1;maxson[u] = 0;
for(int i = head[u];i != 0;i = edge[i].next){
int v = edge[i].to;
if(vis[v] || v == fa) continue;
get_root(v,u);
siz[u] = siz[u] + siz[v];
maxson[u] = max(maxson[u],siz[v]);
}
maxson[u] = max(maxson[u],SIZE-siz[u]);
if(maxson[u] < maxx) root = u,maxx = maxson[u];
}
相应的我们也要获得,以当前节点为根节点的子树上的各节点到根节点的距离值
void get_dis(int u,int fa,int d)//从每一棵新建的子树求距离函数
{
dis[++num] = d;//d数组保存当前根节点到每一个点的距离
for(int i = head[u];i;i = edge[i].next){
int v = edge[i].to;
if(vis[v] || v == fa) continue;
get_dis(v,u,d+edge[i].w);
}
return ;
}
接下来就是最为核心的函数了。因为这个题目中我们一开始直接对根节点就行统计时,会有重复的统计,所以我们在接下来的统计中需要消除这些重复的部分。所以solve函数len的数值是会变化的,对dis数组排一个序这样利用双指针的方法,可以求解出有多少对路径满足这个条件。
可以看到,在分治的函数中,初始solve的len不同,因为我们从子树的根节点出发,所以我们需要把根节点到子树根节点的这段距离加上来统计答案
int calculate(int rt,int len)
{
num = 0;
memset(dis,0,sizeof(dis));
get_dis(rt,0,len);//以当前子树的根节点出发 获得一下路径长度
sort(dis+1,dis+1+num);
int L = 1,R = num,res = 0;
while(L <= R){//双指针法 确定答案
if(dis[L] + dis[R] <= k){
res += R-L;+
L++;
}
else R--;
}
return res;
}
void Divide(int rt)//分治函数 核心部分
{
ans = ans + calculate(rt,0);
vis[rt] = 1;
for(int i = head[rt];i;i = edge[i].next){//对没棵子树进行分治
int v = edge[i].to;
if(vis[v]) continue;
ans = ans - calculate(v,edge[i].w);
SIZE = siz[v];//都重新换一下 以下的信息全部重新赋一个值
maxx = inf;
root = 0;
get_root(v,rt);
Divide(root);//分治新的根节点
}
return ;
}
一般来说,点分治的题目找重心,算距离,分治求解这三个函数具体内容是差不多,关键的还是solve函数(计算函数),根据题目而不同,需要具体分析
完整代码:
Problem: 1741 User: tzteyang777
Memory: 1316K Time: 844MS
Language: G++ Result: Accepted
Source Code
//#include <bits/stdc++.h>
#include <cstring>
#include <iostream>
#include <cmath>
#include <algorithm>
#include <cstdio>
using namespace std;
typedef long long ll;
//点分治
//找到一个节点使其最大子树的大小尽量小 这时分治递归的时间复杂度是最低的
//而这样的节点 就叫做树的重心 可以用一个dfs来求O(n)的时间
const int MAXN = 1e4+7;
#define inf 0x3f3f3f3f
int head[MAXN],maxson[MAXN],vis[MAXN],siz[MAXN],dis[MAXN];
int cnt,SIZE,maxx,num,k,root;
ll ans;
struct Edge
{
int to,next,w;
}edge[MAXN<<1];
void addedge(int u,int v,int w)
{
edge[++cnt].to = v;
edge[cnt].w = w;
edge[cnt].next = head[u];
head[u] = cnt;
}
int get_root(int u,int fa)//找出树的重心
{//siz[i]是指以i为根子树的大小,maxson[i]是指以i为根最大的子树的大小
//SIZE 为当前处理的这棵树的大小 maxx代表已经找到的最大子树的最小值
siz[u] = 1;maxson[u] = 0;
for(int i = head[u];i != 0;i = edge[i].next){
int v = edge[i].to;
if(vis[v] || v == fa) continue;
get_root(v,u);
siz[u] = siz[u] + siz[v];
maxson[u] = max(maxson[u],siz[v]);
}
maxson[u] = max(maxson[u],SIZE-siz[u]);
if(maxson[u] < maxx) root = u,maxx = maxson[u];
}
void get_dis(int u,int fa,int d)//从每一棵新建的子树求距离函数
{
dis[++num] = d;//d数组保存当前根节点到每一个点的距离
for(int i = head[u];i;i = edge[i].next){
int v = edge[i].to;
if(vis[v] || v == fa) continue;
get_dis(v,u,d+edge[i].w);
}
return ;
}
int calculate(int rt,int len)
{
num = 0;
memset(dis,0,sizeof(dis));
get_dis(rt,0,len);
sort(dis+1,dis+1+num);
int L = 1,R = num,res = 0;
while(L <= R){//双指针法 确定答案
if(dis[L] + dis[R] <= k){
res += R-L;+
L++;
}
else R--;
}
return res;
}
void Divide(int rt)//分治函数 核心部分
{
ans = ans + calculate(rt,0);
vis[rt] = 1;
for(int i = head[rt];i;i = edge[i].next){//对没棵子树进行分治
int v = edge[i].to;
if(vis[v]) continue;
ans = ans - calculate(v,edge[i].w);
SIZE = siz[v];//都重新换一下
maxx = inf;
root = 0;
get_root(v,rt);
Divide(root);//分治新的根节点
}
return ;
}
int main()
{
int n;
while(~scanf("%d%d",&n,&k)&&(n&&k)){
cnt = 0;
memset(head,0,sizeof(head));
for(int i = 1;i < n;i ++){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
addedge(a,b,c);
addedge(b,a,c);
}
ans = 0;
memset(vis,0,sizeof(vis));
maxx = inf;SIZE = n;
get_root(1,0);
Divide(root);
printf("%lld\n",ans);
}
return 0;
}