分治思想及树上点分治

分治思想在OI中是一种常见的思想。分治的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。即一种分目标完成程序算法,简单问题可用二分法完成。

当我们求解某些问题时,由于这些问题要处理的数据相当多,或求解过程相当复杂,使得直接求解法在时间上相当长,或者根本无法直接求出。对于这类问题,我们往往先把它分解成几个子问题,找到求出这几个子问题的解法后,再找到合适的方法,把它们组合成求整个问题的解法。如果这些子问题还较大,难以解决,可以再把它们分成几个更小的子问题,以此类推,直至可以直接求出解为止。这就是分治策略的基本思想。

总结来说,分治就是“分而治之”,可以把一个复杂的问题简单化,从全局变成局部,逐渐缩小问题的规模,更加高效的解决问题。

在我们计算机入门的过程中,其实分治思想已经接触得不少了。最基础又经典的例子便是快速排序,归并排序和快速幂。

不妨先来复习一下这三种“基本功”:

快速排序:

void qsort(int l,int r)
{
    if(r-l<=1)
    return;
    int x=l+rand()%(r-l);
    swap(a[r-1],a[x]);
    x=a[r-1];
    int i;
    int j=l;
    for(int i=l;i<r;i++)
    {
        if(a[i]<=x)
        swap(a[i],a[j++]);
    }
    qsort(l,j-1);
    qsort(j,r);
}

归并排序:

void msort(int l,int r)
{
    if(r-l<=1)
    return;
    int mid=(l+r)/2;
    msort(l,mid);
    msort(mid,r);
    int i=l;
    int j=mid;
    int k=l;
    while (i<mid&&j<r)
    {
        if(a[i]<a[j])
        b[k++]=a[i++];
        else
        b[k++]=a[j++];
    }
    while(i<mid)
    b[k++]=a[i++];
    while(j<r)
    b[k++]=a[j++];
    memcpy(a+l,b+l,(r-l)*sizeof(int));
}

快速幂(非递归):

int pow(int x,int y)
{
    if(y==0)
        return 1;
    else
    {
        int t=pow(x,y/2);
        if(y%2==0)
            return t*t;
        else
            return t*t*x;
    }
}

快速幂(递归):

int pow(int x,int y)
{
    int ans=1;
    while(y)
    {
        if(y&1)
            ans*=x;
        x*=x;
        y>>=1;
    }
    return ans ;
}

将这几段代码互相比较,便可以较为容易地加深对分治思想的理解。以后在一些高端的算法中也会经常用到分治思想。

例如:

矩阵乘法快速幂加速递推
CDQ分治
整体二分
快速傅里叶变换
快速沃尔什变换
莫比乌斯变换
辛普森积分等等。
 
以上的一些问题大多数都属于序列分治的范畴内。了解了分治思想之后,将其与树上问题结合起来,便是我们的树上分治了。由于树上的点分治在树的分治问题中操作相对简单,用途广泛,本文主要深入研究树上的点分治问题。
首先,树上点分治是用来干什么的呢?通常用来处理树上路径统计问题。先选取一个部分,统计经过这个部分的全部路径数,再递归地处理该部分的每一个子树,易知,“部分”的选取在树的点分治问题中十分重要。一般我们采取的方式是,对于一个节点,处理出其左子树信息,再处理出右子树信息,然后将两部分的信息结合起来,放在一起统计,从而达到分治处理树的统计问题的目的。那么我们上文也已经提到,“部分”,即分治中心不能随意选取,一般我们选取重心,原因与时间复杂度有关,论证较为复杂,在此不多赘述。重心的求法在以前树上问题的学习中应该已经明确,这里给出代码:
void getroot(int x,int fa)
{
    mx[x]=0;
    si[x]=1;
    for(int i=head[x];i;i=next[i])
    {
        if(to[i]!=fa&&!vis[to[i]])
        {
            getroot(to[i],x);
            si[x]+=si[to[i]];
            mx[x]=max(mx[x],si[to[i]]);
        }
    }
    mx[x]=max(mx[x],sn-si[x]);
    if(mx[root]>mx[x])
        root=x;
}

求出重心之后,利用树中所有点到重心的距离,统计满足条件的路径,然后递归子树,继续进行分治过程。在统计的过程中,一般情况下我们可以通过单步容斥原理,即用“从子树中任意选出两个点”的结果减去“从同一个儿子的子树中任意选出两个点”的结果,得到每次分治的结果。对于 x 的每一个儿子,用同样的方法便可得到正确统计结果。注意,在用单步容斥原理时需要满足一个条件,即统计结果可以逆运算,如果不存在逆运算,最典型的例子便是统计最大值和最小值,像这样的问题只能就题论题,通过特殊方法加以解决。

两道模板题:POJ 1741 BZOJ 2152

最裸的点分治练习题,初学者可以尝试一下。其中BZOJ 2152题解戳http://www.cnblogs.com/kanbokedeshiwoerzi/p/8809919.html

原文地址:https://www.cnblogs.com/kanbokedeshiwoerzi/p/8810112.html