【暖*墟】 #数据结构进阶# 点分治

点分治,是一种处理树上路径问题的工具,适用于树上的求和问题。

给定一棵树和一个整数k,求 树上边数等于k的路径 有多少条。

做法1:枚举不同的两个点,用dfs算出距离、并统计求值。O(n^3)。

做法2:找根,求出每个点到根的距离,枚举两点求lcaO(n^2logn)

做法3:考虑形成路径的情况,假设一条满足条件的路径经过点x,

那么这条路径 ①在x的一个子树里(以x为端点) 或者 ②在x的两个不同的子树里。

dfs遍历子树中的每个点,依次处理每个点的子树答案。

 

点分治的算法流程:

  1. 对于当前的这一部分树,求出当前这一部分的重心;
  2. 维护当前重心为根时、对应的答案,统计求和;
  3. 去掉当前重心,将当前这部分树分成更多部分,重复如上操作。

 

点分治的一个简单性质:

  • 因为每次都以重心分割树,所以当前层每条经过重心的路径都是以前没枚举到过的。
  • 即:每个重心管辖范围内经过重心的路径条数之和,就是树上的所有路径

 

原理(左右子树分治)

假设我们选出一个根Root,那么答案路径肯定是:

  1. 被左子树或右子树完全包含;
  2. 两子树中各选一部分路径,从Root处拼起来。

思考发现情况1(被一个子树包含)中,答案路径上的某点可以看成新的根,即Root2。

Root为根的子树中存在答案(下方路径),可以看成以Root2为根的两棵子树存在答案。

此时情况1可以转化为情况2,说明可以使用分治的方法统计。这是点分治的基本原理。

 

选根(选重心为根)

选根不同会影响遍历的效率,下图中选x为根优于选y为根。

重心为根时,所有的子树的大小都不超过整个树大小的一半

可以发现,找树的重心(最大的子树最小的根节点)是最优的。

 

树形dp求树的重心:

void GET_ROOT(int x,int fa){ 
    f[x]=0,siz[x]=1; //f[i]表示此点为根的最大子树的大小
    for(int i=head[x];i;i=e[i].nextt){
        int y=e[i].ver; if(used[y]||y==fa) continue;
        GET_ROOT(y,x); //向下遍历
        f[x]=max(f[x],siz[y]); siz[x]+=siz[y];
    } f[x]=max(f[x],Siz-siz[x]);
//Siz表示【现在这棵子树中】点的总数,一开始求重心时Siz=n if(f[x]<f[rt]) rt=x; //更新root }

 

之后的分治过程还需要对子树单独找重心,所以代码中有used[ ],一开始无影响。

 

求距离(枚举+子树容斥)

找到重心作为根之后,求出重心到子树中各个点的距离。

枚举子树里的两个点,如果两个点到重心的距离和为k,答案+1,这是第二种情况。

第一种情况就让距离根为k的点跟重心配对就行了,因为重心到重心的距离为0。

 

统计答案(二分+子树容斥)

考虑枚举一个点,另一个点可以通过二分来求解。

将距离进行排序,转化为找距离为 k -枚举点的距离 的点的个数。

因为距离已经排序过,所以可以二分出相同值的左右边界l、r,ans+=r-l+1

int calc(int u,int d0){ 
//↑↑↑此时以u为根节点,统计子树中符合条件的点对个数
    cnt=0; dep[u]=d0; getdeep(u,0);
    sort(o+1,o+cnt+1); //排序,便于二分
    int l=1,r=cnt,res=0;
    while(l<r){ 
        if(o[l]+o[r]<=k) res+=r-l,l++;
        else r--; //二分求符合条件的点对个数
    } return res;
}

 

【poj 1741】Tree

  • n个点的树,每条边都有一个权值。
  • 两点路径长度就是路径上各边权值之和。
  • 求长度不超过K的路径有几条。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N=50019;

struct edge{ int ver,nextt,w; }e[N<<1]; //边集

int n,m,k,head[N],cnt; //head[]和cnt是边集数组的辅助变量 

int root,sum; //当前查询的根,当前递归的这棵树的大小 

int vis[N]; //某一个点是否被当做根过 

int sz[N]; //每个点下面子树的大小 
int f[N]; //每个点为根时,最大子树大小 

int dep[N]; //每个点的深度(此时是与根节点的距离) 
int o[N]; //每个点的深度(用于排序,进而用于二分)

int ans; //最终统计的答案 

int reads(){ //读入优化
    int x=0,w=1; char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-') ch=getchar();
    if(ch=='-') w=0,ch=getchar();
    while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return w?x:-x;
}

void getroot(int u,int fa){ //dfs求重心和子树大小
    sz[u]=1; f[u]=0;
    for(int i=head[u];i;i=e[i].nextt){
        int v=e[i].ver;
        if(v==fa||vis[v]) continue;
        getroot(v,u); sz[u]+=sz[v];
        f[u]=max(f[u],sz[v]);
    } f[u]=max(f[u],sum-sz[u]); //注意:可能是另外一半的树
    if(f[u]<f[root]) root=u; //更新重心
}

void getdeep(int u,int fa){ //dfs求出与根节点的距离
    o[++cnt]=dep[u]; //用于排序
    for(int i=head[u];i;i=e[i].nextt){
        int v=e[i].ver; if(v==fa||vis[v]) continue;
        dep[v]=dep[u]+e[i].w; getdeep(v,u);
    }
}

int calc(int u,int d0){ 
//↑↑↑此时以u为根节点,统计子树中符合条件的点对个数
    cnt=0; dep[u]=d0; getdeep(u,0);
    sort(o+1,o+cnt+1); //排序,便于二分
    int l=1,r=cnt,res=0;
    while(l<r){ 
        if(o[l]+o[r]<=k) res+=r-l,l++;
        else r--; //二分求符合条件的点对个数
    } return res;
}

void solve(int u){
    ans+=calc(u,0); vis[u]=1;
    //↑↑会产生非法路径(被u的某个子树完全包含,路径不能合并)
    for(int i=head[u];i;i=e[i].nextt){ //递归子树
        int v=e[i].ver; if(vis[v]) continue;
        ans-=calc(v,e[i].w); //容斥原理去除非法答案
        //↑↑在处理子树时,将初始长度设为连接边长e[i].w;
        //这样做就相当于给子树的每个组合都加上了u—>的路径。
        sum=sz[v]; root=0; //重设当前总树大小,寻找新的分治点
        getroot(v,0); solve(root); //递归新的分治点(重心)
    }
}

int main(){
    n=reads(); int u,v,w;
    for(int i=1;i<n;i++){
        u=reads(); v=reads(); w=reads(); //↓前向星
        e[++cnt]=(edge){v,head[u],w}; head[u]=cnt;
        e[++cnt]=(edge){u,head[v],w}; head[v]=cnt;
    } root=0; sum=f[0]=n; //一开始,root初始化为0,用于找重心        
    k=reads(); getroot(1,0); solve(root); //从重心开始点分治
    printf("%d\n",ans); return 0;
}

 

 

 推荐题:

模板题(雾)

模板题(求距离为k的点对个数)

求小于k的点对个数

权值和为k,求最小边数

最后一题题解

转载于:https://www.cnblogs.com/FloraLOVERyuuji/p/10149612.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值