分治思想及树上点分治
分治思想在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 ; }
将这几段代码互相比较,便可以较为容易地加深对分治思想的理解。以后在一些高端的算法中也会经常用到分治思想。
例如:
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