点分治就是在一棵树上,对具有某些限定条件的路径静态的进行统计的方法。
前置知识:
- 树的重心
- 树的DFS序
- 数组模拟邻接表存树
对树的操作常见两种:边分治、点分治。对于点分治,顾名思义,就是要按照点进行划分,利用递归来实现分治。如果给定一棵树,以及树上的两个节点x和y,那么“序列上的区间”相对于的就是“两点之间的路径”。
例题:POJ1741Tree
给一颗有N个点的树,每条边都有一个权值。树上两个节点x与y之间的路径长度就是各条边的权值之和。求长度不超过k的路径有多少条。
本题所描述的由N个点及N-1条边构成的树称为“无根树”。若指定p为根,那么对于p来说,树上的路径只有两类:
- 经过根节点p。
- 包含于p的某一棵子树中(不经过根节点)。
对于第二类,我们可以依据分治思想,将p的子树看作子问题求解,递归处理。
对于第一类,我们可以将节点分“x-p"与"p-y"两段。然后我们可以对于根节点p,求出每个子节点x到p的距离d[x]。再想办法从p的子节点寻找数对(x,y),使得d[x] + d[y] <= k。设calc§表示在以p为根的树中统计上述点对的个数。
整个算法的过程为:
- 任选一个根节点p,
- 从p出发进行一次DFS,求出d数组
- 执行calc§
- 删除根节点p,对p的每颗子树,递归1~4步
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 30100;
int n,k;
//以下两行用来存储图
int d[MAXN],g[MAXN],size[MAXN];
//d用来记录点到根距离,g用来记录最大子树的大小,size为当前节点子树大小
int head[MAXN],ver[MAXN],nex[MAXN],w[MAXN];
//头节点,目标节点,下一条边的编号,边权
int tot ,cnt;
bool vis[MAXN];
inline void addEdge(int x,int y,int val){
ver[++cnt] = y;w[cnt] = val;
nex[cnt] = head[x];head[x] = cnt;
}
inline void dfs_root(int x,int p){
//此函数计算出每个节点子树大小size[x],并计算若删去该节点,最大子树大小g[x]
size[x] = 1;g[x] = 0;
for(int i = head[x];~i;i = nex[i])
if(!vis[ver[i]] && i != p){
dfs_root(ver[i],i^1);
size[x] += size[ver[i]];
g[x] = max(g[x],size[ver[i]]);
}
}
inline int find_root(int x,int p,int root){
//此函数返回与x联通的无根树的重心 编号
int minx = n,ret = 0;
for(int i = head[x];~i;i = nex[i]){
if(!vis[ver[i]] && i != p){
int tmp = find_root(ver[i],i^1,root);
if(g[tmp] < minx){
ret = tmp,minx = g[tmp];
}
}
}
g[x] = max(g[x],size[root] - size[x]);
if(g[x] < minx) ret = x;
return ret;
}
inline int get_root(int x){
//通过调用上述两个函数,求得重心
dfs_root(x,-1);
return find_root(x,-1,x);
}
inline void get_dis(int x,int dis,int p){
//该函数求出以x为根的子树的所有节点到根的距离,并存放在d数组
d[++tot] = dis;
for(int i = head[x];~i;i = nex[i]){
if(!vis[ver[i]] && i != p)
get_dis(ver[i],dis+w[i],i^1);
}
}
inline int calc(int x,int dis){
//此函数返回以x为根的树的答案个数
tot = 0;
int res = 0;
get_dis(x,dis,-1);
sort(d+1,d+tot+1);
int j = tot;
for(int i = 1;i < j;i++){
while(d[i] + d[j] > k && i < j) j--;
res += j-i;
}
return res;
}
inline int dfs(int x){
//dfs(x)返回以x为根的子树中答案个数
int ans = 0;
int root = get_root(x);//找到无根树的重心
vis[root] = true;
ans += calc(root,0);//计算以root为根的子树有多少答案
for(int i = head[root];~i;i = nex[i]) if(!vis[ver[i]]){
ans -= calc(ver[i],w[i]);//减去在同一棵子树中的点对
ans += dfs(ver[i]); //加上子树中的答案
}
return ans;
}
int main(){
while(scanf("%d%d",&n,&k) && (n + k)){
int x,y,z;
cnt = 1;//初始化
memset(head,-1,sizeof head);
memset(vis,false,sizeof vis);
for(int i = 1;i < n;i++){
scanf("%d%d%d",&x,&y,&z);
addEdge(x,y,z);addEdge(y,x,z);
}
printf("%d\n",dfs(1));
}
return 0;
}
参考书目:
- 《算法竞赛进阶指南》李煜东.P221