点分治

Posted wozaixuexi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了点分治相关的知识,希望对你有一定的参考价值。

1 概述

点分治是一种对树上符合某种条件的路径进行静态统计的算法。

当然,动态点分治(点分治树)是有的,但此篇文章暂不涉及。

这种算法的思想就是:对于一棵树,可以把树上的路径分为两类,一类是经过根结点的,一类是不经过根结点的。对于第二类路径,我们可以通过将根节点的每棵子树作为子问题递归处理,这样,我们只需要考虑如何处理第一种路径即可,使问题简化。

 

2 例题

2.0 题目链接

Poj1741 Tree

 

2.1 题目大意

给出一棵有N个点的树,树上的每条边都有一个权值,求长度不超过K的路径有多少条。

 

2.2 思路

经典的点分治问题。

我们先选一个结点作为根,然后开始处理上文中提到的“第一类路径”,即经过根节点的路径,其余路径递归处理。

设结点x到根的距离为dis[x],我们先对当前这棵树dfs一遍,将遍历到的结点都存在一个数组que中,然后按dis的值从小到大排序。接着,我们定义两个指针i和j。其中i从左到右扫描这个数组,j从右到左扫描这个数组。对于每一个i,我们都找出一个最靠右的j使得dis[que[i]]+dis[que[j]]≤K,此时可将j-i累加到答案中,直到i≥j为止。可以发现,j的位置只会越来越靠左,故这个统计答案过程可以在O(Nlog2N+N)的时间内实现。

值得注意的是,我们在选择让哪个点作为根时一定要选树的重心。这样的话,在每一次的递归分治中,结点的规模至少减半,可以保证最多只会进行log2N层点分治。倘若不选树的重心作为根,则点分治进行的层数无法保证,时间复杂度也极易退化。

每次选择树的重心作为根后,总时间复杂度即为O(Nlog22N)。

 

2.3 代码实现

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
    using namespace std;
struct edge
{
    int last;
    int end;
    int weight;
}e[20005];
    //vis记录每个点是否被访问过 
    //que为存放结点的数组
    //dis为每个结点到根的距离
    //siz为每个结点的子树大小
    //msiz为每个结点的所有子树中,最大的子树的大小 
    //siz和msiz都是用来求树的重心的
    int n=0,k=0,ne=0,head=0,tail=0,note[10005],vis[10005],que[10005],dis[10005],siz[10005],msiz[10005];
void csh()//多组数据,记得初始化 
{
    ne=head=tail=0;
    for(int i=0;i<20005;i++) e[i].last=e[i].end=e[i].weight=0;
    for(int i=0;i<10005;i++) note[i]=vis[i]=que[i]=dis[i]=siz[i]=msiz[i]=0;
}
void make_edge(int u,int v,int w)
{
    ne++;
    e[ne].last=note[u];
    e[ne].end=v;
    e[ne].weight=w;
    note[u]=ne;
}
bool cmp(int x,int y)
{
    return dis[x]<dis[y];
}
void dfs(int x)
{
    siz[x]=vis[x]=1,msiz[x]=0;
    que[++tail]=x;//存储遍历到的结点 
    for(int i=note[x];i;i=e[i].last)
        if(!vis[e[i].end])
        {
            dis[e[i].end]=dis[x]+e[i].weight;
            dfs(e[i].end);
            siz[x]+=siz[e[i].end];
            msiz[x]=max(msiz[x],siz[e[i].end]);
        }
    vis[x]=0;
}
int calc(int l,int r) 
{ 
    sort(que+l,que+r+1,cmp);//先按dis的值从小到大排序 
    int ans=0;
    for(int i=l,j=r;i<j;i++)//两个指针一起扫 
    {
        while(i<j&&dis[que[i]]+dis[que[j]]>k) j--;
        ans+=j-i;//累加答案 
    }
    return ans;
}
int work(int x)
{
    head=1,tail=0;
    dfs(x);//先dfs一遍(用于找树的重心) 
    int sum=siz[x],misiz=msiz[x];
    for(int i=1;i<=tail;i++)//找树的重心 
        if(max(msiz[que[i]],sum-msiz[que[i]])<misiz) misiz=max(msiz[que[i]],sum-msiz[que[i]]),x=que[i];
    vis[x]=1;//标记这个点已被计算过,防止程序对重复的点进行点分治 
    int ans=0;
    head=1,tail=0;
    for(int i=note[x];i;i=e[i].last)
        if(!vis[e[i].end])
        {
            dis[e[i].end]=e[i].weight;
            dfs(e[i].end);//收集结点 
            ans-=calc(head,tail);//先减去路径两端在同一棵子树的不合法路径对答案的贡献 
            head=tail+1;
        }
    dis[x]=0,que[++tail]=x;
    ans+=calc(1,tail);//计算路径两端在不同子树的路径对答案的贡献 
    for(int i=note[x];i;i=e[i].last)
        if(!vis[e[i].end]) ans+=work(e[i].end);//递归分治 
    return ans;
}
int main()
{
    for(;;)
    {
        scanf("%d%d",&n,&k);
        if(n==0&&k==0) break;
        csh();
        for(int i=1;i<=n-1;i++)
        {
            int u=0,v=0,w=0;
            scanf("%d%d%d",&u,&v,&w);
            make_edge(u,v,w);
            make_edge(v,u,w);
        }
        printf("%d
",work(1));
    }
    return 0;
}

 

2.4 小结

每道题计算路径对答案的贡献的方式都不一样,但点分治的总体框架与思想是不变的。

分治这种算法还是要多做题,多总结才行。

 

3 练习题

Luogu P2634 [国家集训队]聪聪可可

Luogu P4149 [IOI2011]Race

 

4 参考资料

《算法竞赛进阶指南》(李煜东著)第0x45节。

LZZ的言传身教。

以上是关于点分治的主要内容,如果未能解决你的问题,请参考以下文章

基于点分治的树分治

POJ 1741 Tree ——点分治

bzoj2599 [ IOI2011] -- 点分治

算法有关点分治的一些理解与看法

POJ 3714 分治/求平面最近点对

分治——最近点对