树上点分治

点分治就是在一棵树上,对具有某些限定条件的路径静态的进行统计的方法。
前置知识:

  • 树的重心
  • 树的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
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 程序猿惹谁了 设计师:白松林 返回首页