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

【例题1】poj 1741 Tree

  • n个点的树,每条边都有一个权值。
  • 两点路径长度就是路径上各边权值之和。
  • 求长度不超过K的路径有几条。

【分析】

本题中树的边是无向的,即是一个n个点,n-1条边构成的无向图。

这种树可以看成无根树,可以指定节点p为根。

对p而言,树上路径可以分为两类:

  1. 经过根节点p;
  2. 包含于p的某一棵子树中。

设点 i 的深度为 Depth[i],父亲为 Parent[i] 。

若 i 为根,则 Belong[i] = -1,若 Parent[i] 为根,则 Belong[i]=i,

否则 Belong[i] = Belong[ Parent[i] ]。可以通过一次BFS求得。

我们的目标是要统计,有多少对 (i,j) 满足:

i<j,Depth[i]+Depth[j]<=K,Belong[i]!=Belong[j]。

如果这样考虑问题会变得比较麻烦,我们可以考虑换一种角度:

设 X 为满足 i<j,Depth[i]+Depth[j]<=K 的数对 (i,j) 的个数

设 Y 为满足 i<j,Depth[i]+Depth[j]<=K 且 Belong[i]=Belong[j] 数对 (i,j) 的个数

那么我们要统计的量便等于X-Y。

求 X、Y 的过程均可以转化为以下问题:

已知 A[1],A[2],...A[m],求满足 i<j 且 A[i]+A[j]<=K 的数对 (i,j) 的个数。

对于这个问题,我们先将 A 从小到大排序。

设 B[i] 表示满足 A[i]+A[p]<=K 的最大的 p (若不存在则为0)。

我们的任务便转化为求出 A 所对应的 B 数组。

那么,若 B[i]>i,那么i对答案的贡献为 B[i]-i 。

显然,随着 i 的增大,B[i] 的值是不会增大的。

利用这个性质,我们可以在线性的时间内求出 B 数组,从而得到答案。

我们在每一棵子树中选择“最优”的点分割。

所谓“最优”,是指 [ 删除这个点后,最大的子树尽量小 ] 。

这个点就是重心,可以通过树形DP在O(N)时间内求出。

这样一来,即使是遇到一根链的情况时,L的值也仅仅是 O(logN) 的。

简单来说:点分治就是每次找到重心,然后把重心去掉。

对分成的每两棵树之间分别统计路径信息。

(以重心的每个相邻点为根,遍历整棵子树即可得到这个根到每个结点的统计信息)

就可以知道包含这个重心的所有路径的信息,

然后对于剩下的路径就是在子树里面进行同样的操作了,直到只剩一个点为止

(注意这一个点所构成的路径有时也要处理一下)。

边分治就是每次找到一条边,使得删掉这条边后分成的两棵子树大小尽可能平均,

然后以删掉的边的两端点为根,分别统计根到两棵树中的每个结点的路径信息,

最后合并算路径,即可得到包含这条边的所有路径的信息,剩下的路径在两棵树中递归处理。

【代码实现】

1.为了避免变量名指代不清的问题,我们先规定一下各变量的含义。

const int N = 10005;
struct edge{ int to,next,w; }a[N<<1]; //边集数组
int n,k,head[N],cnt; //n,k不解释,head[]和cnt是边集数组的辅助变量 
int root,sum; //当前查询的根,当前递归这棵树的大小 
int vis[N]; //某一个点是否被当做根过 
int sz[N]; //每个点下面子树的大小 
int f[N]; //每个点为根时的最大子树大小 
int dep[N]; //每个点的深度 
int o[N]; //每个点的深度(用于排序) 
//(这个是以poj1741为例,其他题目不一定要用到这个)
int ans; //最终统计的答案 

2.点分治的核心是找一个点作为根,而找出来的这个点就是我们所说的“重心”。

void getroot(int u,int fa){ //dfs寻找重心
    sz[u]=1; f[u]=0;
    for(int e=head[u];e;e=a[e].next){
        int v=a[e].to;
        if (v==fa||vis[v]) continue;
        getroot(v,u); //dfs
        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;
}

3.每次找出一个根以后,所有点对就只有两种可能了:

1)两个点都在根的某一棵子树中,即路径不过根;

2)两个点在根的不同子树中,或其中一个点就是根,此时路径必过根。

4.完整代码:

#include <bits/stdc++.h> //poj不让用的
using namespace std;
typedef long long ll;
const int N = 10005;
struct edge{ int to,next,w; }a[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 e=head[u];e;e=a[e].next){
        int v=a[e].to;
        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 e=head[u];e;e=a[e].next){
        int v=a[e].to;
        if(v==fa||vis[v]) continue;
        dep[v]=dep[u]+a[e].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;
    for(int e=head[u];e;e=a[e].next){
        int v=a[e].to;
        if(vis[v]) continue;
        ans-=calc(v,a[e].w); //???
        sum=sz[v]; root=0;
        getroot(v,0); solve(root);
    }
}

int main(){
    while(1){
        n=reads(); k=reads();
        if(n==0&&k==0) return 0;
        memset(head,0,sizeof(head));
        memset(vis,0,sizeof(vis));
        int u,v,w; cnt=0; ans=0;
        for(int i=1;i<n;i++){
            u=reads(); v=reads(); w=reads(); //↓前向星
            a[++cnt]=(edge){v,head[u],w}; head[u]=cnt; //poj也不让这样写
            a[++cnt]=(edge){u,head[v],w}; head[v]=cnt;
        }
        root=0; sum=f[0]=n;
        getroot(1,0); solve(root); //从重心开始点分治
        printf("%d\n",ans);
    }
    return 0;
}

【例题2】Luogu 3806 点分治模板

  • 询问距离为k的点对是否存在。
#include <bits/stdc++.h>
using namespace std;

struct data{
    int nextt,to,w;
}p[20010];  //邻接表存边

int head[10010],tot;
int sum;   //子树的总点数 
int root;  //当前子树的根 
int f[10010],son[10010];  
//↑↑↑ f为除去根时得到的最大连通块,son为以i为根的子树的节点

bool vis[10010];  //是否访问(标记) 
int d[10010];  //i到当前根的距离 
bool okk[10000100];  //bool型数组,k是否存在 

struct node{
    int dis,which;
}rp[1001000];
int tt;

void add(int x,int y,int w){ //建边
    p[++tot]=(data){head[x],y,w};
    head[x]=tot;
}

void getroot(int u,int papa){ //找重心 
    son[u]=1; f[u]=0;
    for(int i=head[u];i;i=p[i].nextt){
        int v=p[i].to;
        if(vis[v]||v==papa) continue;
        getroot(v,u);
        son[u]+=son[v];
        f[u]=max(f[u],son[v]);  //注意:f存的是最大连通块 
    }
    f[u]=max(f[u],sum-son[u]);
    if(f[u]<f[root]) root=u;  //更新重心 
}

int pp;  //以同一点为根的子树编号 

void getdeep(int rearoot,int u,int fa,int ro){ //得到点到根的距离 
    for(int i=head[u];i;i=p[i].nextt){
        int v=p[i].to;
        if(vis[v]||v==fa) continue;  //去重 
        if(u==rearoot) pp++; //子树编号
        d[v]=d[u]+p[i].w;
        if(u==rearoot) rp[++tt]=(node){d[v],pp};
        else rp[++tt]=(node){d[v],ro};
        okk[d[v]]=1;  //更新k的可能值 
        if(rearoot==u) getdeep(rearoot,v,u,pp);
        else getdeep(rearoot,v,u,ro);  //下一个 
    }
}

void getans(int u)  {
    d[u]=0; tt=0; pp=0;
    getdeep(u,u,0,0); vis[u]=1;
    for(int i=1;i<=tt;i++) //!!!!!注意,十分重要 
        for(int j=i+1;j<=tt;j++)
            if(rp[i].which!=rp[j].which) 
                ko[rp[i].dis+rp[j].dis]=1;
    for(int i=head[u];i;i=p[i].nextt){
        int v=p[i].to;
        if(!vis[v]){ //去重 
            root=0;  //  !! 
            sum=son[v]; //  !! 
            getroot(v,0);//得到子树的重心 
            getans(root);  //以重心建树 
        }
    }
}

int main(){
    int n,m,a,b,c; scanf("%d%d",&n,&m);
    
    for(int i=1;i<=n-1;i++){ //树的特点:n个点,n-1条边
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c); add(b,a,c);  //建边
    }
    
    sum=f[0]=n; root=0; //【注意】sum、root必须初始化 
    getroot(1,0); getans(root); //找重心,以重心建树找k值

    for(int i=1;i<=m;i++){ //判断是否存在 
        int kk; scanf("%d",&kk);
        if(okk[kk]==1) printf("AYE\n");
        else printf("NAY\n");
    }

    return 0;
}

                                               ——时间划过风的轨迹,那个少年,还在等你。

猜你喜欢

转载自blog.csdn.net/flora715/article/details/81944827
今日推荐