点分治详细解析

来源:互联网 发布:sql server2012r2下载 编辑:程序博客网 时间:2024/06/01 07:30

点分治,是处理树上路径的一个极好的工具。
一般如果需要大规模处理树上路径,点分治是一个不错的选择。
这里我就来讲一讲我自己对于点分治的一点理解和感悟(帮助新手入坑……)
现在就开始吧!

1.点分治的基本思想
点分治,顾名思义就是基于树上的节点进行分治。
如果我们在深入一点呢?对于点的拆开其实就是对于树的拆开。
所以我认为点分治的本质其实是将一棵树拆分成许多棵子树处理,并不断进行。
这应该也是点分治的精髓。

2.分治点的选择
既然我们要将一个点进行分治,那么选点肯定最首要的。
思考下面一个问题:
如果树退化为一个链,
我们选取链首作为分治点,理论时间复杂度?
而如果选择链的中心,它的理论时间复杂度又是多少?
答案其实还是挺简单的。
选择链首:O( n )
选择链心:O( logn )
通过这个例子,我们不难发现:
如果选点后左右子树越大,递归层数越多,时间越慢,反之则越快。
所以我们的选点标准就出来了,而我们把有这个性质的点叫做:树的重心

3.重心的O( n )求解<求解分治点>
正式一点定义一下重心:找到一个点,其所有的子树中最大的子树节点数最少,那么这个点就是这棵树的重心,
先贴出找重心的代码(各位结合着理解下面的内容):

void getroot(int u,int fa){  sim[u] = 1; mxson[u] = 0;  for(int i = head[u];i;i = t[i].next)    {      int v = t[i].to;      if(vis[v]||v == fa)continue;      getroot(v,u);      sim[u] = sim[u] + sim[v];      mxson[u] = max(mxson[u],sim[v]);    }  mxson[u] = max(mxson[u],Smer - sim[u]);  if(mxson[u]<MX){root = u; MX = mxson[u];}}

其中sim[i]表示以i为根节点的子树的节点数量(即子树大小),mxson[i],表示i节点为根的子树中的最大子树。
Smer为这棵树(当前处理树)的大小。MX记录当前已经找到的最小 最大子树值。

那么究竟是怎么找的呢?其实也蛮简单的。
就是暴力(当然不是正真意义上的暴力啦,毕竟人家是O(n)的)搞出所有子树的大小,找出最大子树,与原来答案比较,更新答案。
但在这里要特别注意一下这一句:

mxson[u] = max(mxson[u],Smer - sim[u]);

这一句其实使用到了容斥原理的思想(后面还会用到),求出的是经过父亲节点的子树大小。
读者们细细体会一下代码,这部分应该还是很好懂~(≧▽≦)/~啦啦啦。

4.点分治的实现
有了前面的基础知识我们就可以正式进入点分治的探讨了。
点分治是用递归实现的,先给出代码:

void Divide(int tr){  ans = ans + solve(tr,0);  vis[tr] = true;  for(int i = head[tr];i;i = t[i].next)    {      int v = t[i].to;      if(vis[v])continue;      ans = ans - solve(v,t[i].len);      Smer = sim[v]; root = 0;      MX = INF; getroot(v,0);      Divide(root);    }  return;}

我们一点一点的分析。
首先对于当前点,求解经过此点的所有贡献(solve)。
ans = ans + solve(tr,0);

把当前点标记(防止陷入死循环)。
vis[tr] = true;

然后进行分治,分别处理此点的每一棵子树。
for(int i = head[tr];i;i = t[i].next)

容斥原理去除非法答案。
ans = ans - solve(v,t[i].len);
这里要着重讲一下。
对于以下这棵树:
这里写图片描述
显然A点是它的重心。
我们假设现在分治到了A点(当前点为A)
我们一开始求解贡献时,会有以下路径被处理出来:
A—>A
A—>B
A—>B—>C
A—>B—>D
A—>E
A—>E—>F (按照先序遍历顺序罗列)
那么我们在合并答案是会将上述6条路径两两进行合并。
这是注意到:
合并A—>B—>C 和 A—>B—>D 肯定是不合法的!!
因为这并不是一条树上(简单)路径,出现了重边,我们要想办法把这种情况处理掉。
处理方法很简单,减去每个子树的单独贡献。
例如对于以B为根的子树,就会减去:
B—>B
B—>C
B—>D
这三条路径组合的贡献
读者可能会有疑问,这与上面的6条路径并不一样啊。
我们再回过头来看一看这两句代码:
ans = ans + solve(tr,0);
ans = ans - solve(v,t[i].len);
注意到了吧,solve函数的第二个初始值并不相同。
我们在处理子树时,将初始长度设为连接边长,这样做就相当于个子树的每个组合都加上了A—>的路径,从而变得与上面一样。
个人认为这是点分治一个极其重要的地方,读者们一定要理解清楚。

重设当前总树大小,寻找新的分治点(重心)
Smer = sim[v]; root = 0;
MX = INF; getroot(v,0);

递归处理新的分治点
Divide(root);
5.具体的习题练习:

【例题1】【poj1741】tree
给一颗n个节点的树,每条边上有一个距离v。定义d(u,v)为u到v的最小距离。给定k值,求有多少点对(u,v)使u到v的距离小于等于k。
点分治最经典的题目了(笔者瞎猜这是不是点分治的起源)。
用上文的模板直接套,求出所有点对的距离,注意合并答案时用二分保证复杂度。

#include<cstdio>#include<cstdlib>#include<iostream>#include<cstring>#include<cmath>#include<algorithm>#define maxn 10002#define INF 2147483646#define ll long longusing namespace std;inline int gi(){  int date = 0,m = 1; char ch = 0;  while(ch!='-'&&(ch<'0'||ch>'9'))ch = getchar();  if(ch=='-'){m = -1; ch = getchar();}  while(ch>='0' && ch<='9')    {      date = date*10+ch-'0';      ch = getchar();    }return date*m;}inline void write(ll qw){  if(qw<0){putchar('-'); qw = -qw;}  if(qw>9)write(qw/10);  putchar(qw%10+'0');}struct node{int to,next,len;}t[2*maxn+5];int head[maxn+5];int sim[maxn+5],mxson[maxn+5];bool vis[maxn+5];int MX,root,dis[maxn+5],summar;ll ans;int n,k,cnt,S;void addedge(int u,int v,int l){  cnt ++;  t[cnt].to = v;  t[cnt].next = head[u];  t[cnt].len = l;  head[u] = cnt;  return;}void getroot(int u,int fa){  sim[u] = 1; mxson[u] = 0;  for(int i = head[u];i;i = t[i].next)    {      int v = t[i].to;      if(v == fa || vis[v])continue;      getroot(v,u);      sim[u] = sim[u] + sim[v];      mxson[u] = max(sim[v],mxson[u]);    }  mxson[u] = max(mxson[u],S-sim[u]);  if(mxson[u]<MX){root = u;MX = mxson[u];}}void getdis(int u,int fa,int dist){  dis[++summar] = dist;  for(int i = head[u];i;i=t[i].next)    {      int v = t[i].to;      if(vis[v]||v == fa)continue;      getdis(v,u,dist+t[i].len);    }  return;}int consolate(int sta,int len1){  summar = 0;  memset(dis,0,sizeof(dis));  getdis(sta,0,len1);  sort(dis+1,dis+summar+1);  int L = 1,R = summar,tep=0;  while(L<=R)    {      if(dis[R] + dis[L]<=k){tep = tep + R - L; L++;}      else R--;    }  return tep;}void Divide(int tr){  ans = ans + consolate(tr,0);  vis[tr] = true;  for(int i = head[tr];i;i = t[i].next)    {      int v = t[i].to;      if(vis[v])continue;      ans = ans - consolate(v,t[i].len);      S = sim[v]; root = 0;      MX = INF; getroot(v,0);      Divide(root);    }  return;}int main(){  freopen("Tree.in","r",stdin);  int u,v,l;  while(1)    {      n = gi(); k = gi();      if(n == 0 && k == 0)break;      cnt = 0; memset(head,0,sizeof(head));      for(int i = 1; i <= n - 1; i ++)    {      u = gi(); v = gi(); l = gi();      addedge(u,v,l); addedge(v,u,l);    }      ans = 0;      memset(vis,0,sizeof(vis));      MX = INF ; S = n; getroot(1,0);      Divide(root);      write(ans);putchar('\n');    }  return 0;}

【例题2】【luogu P2634】聪聪可可
聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃、两个人都想玩儿电脑(可是他们家只有一台电脑)……遇到这种问题,一般情况下石头剪刀布就好了,可是他们已经玩儿腻了这种低智商的游戏。他们的爸爸快被他们的争吵烦死了,所以他发明了一个新游戏:由爸爸在纸上画n个“点”,并用n-1条“边”把这n个“点”恰好连通(其实这就是一棵树)。并且每条“边”上都有一个数。接下来由聪聪和可可分别随即选一个点(当然他们选点时是看不到这棵树的),如果两个点之间所有边上数的和加起来恰好是3的倍数,则判聪聪赢,否则可可赢。聪聪非常爱思考问题,在每次游戏后都会仔细研究这棵树,希望知道对于这张图自己的获胜概率是多少。现请你帮忙求出这个值以验证聪聪的答案是否正确。

计算答案的方法:利用点分治在计算所有路径长度,把路径长度对3取模,用t[0],t[1],t[2]分别记录模为0、1、2的情况,那么显然答案就是t[1]*t[2]*2+t[0]*t[0]。

#include<cstdio>#include<cstdlib>#include<iostream>#include<cstring>#include<cmath>#include<algorithm>#define maxn 20002#define INF 2147483646#define mod 3#define ll long longusing namespace std;inline int gi(){  int date = 0,m = 1; char ch = 0;  while(ch!='-'&&(ch<'0'||ch>'9'))ch = getchar();  if(ch=='-'){m = -1; ch = getchar();}  while(ch>='0' && ch<='9')    {      date = date*10+ch-'0';      ch = getchar();    }return date*m;}inline void write(ll qw){  if(qw<0){putchar('-'); qw = -qw;}  if(qw>9)write(qw/10);  putchar(qw%10+'0');}struct node{int to,next,len;}t[2*maxn+5];int head[maxn+5],cnt;int sim[maxn+5],mxson[maxn+5],sum[4];ll ans;int Sumer,MX,dec1,root,n;bool vis[maxn+5];void addedge(int u,int v,int l){  cnt ++;  t[cnt].to = v;  t[cnt].next = head[u];  t[cnt].len = l;  head[u] = cnt;  return;}void getroot(int u,int fa){  sim[u] = 1;mxson[u] = 0;  for(int i = head[u];i;i = t[i].next)    {      int v= t[i].to;      if(v == fa || vis[v])continue;      getroot(v,u);      sim[u] = sim[u] + sim[v];      mxson[u] = max(mxson[u],sim[v]);    }  mxson[u] = max(mxson[u],Sumer - sim[u]);  if(mxson[u]<MX){MX = mxson[u]; root = u;}}void query(int u,int fa,int lon){  sum[lon%mod]++;  for(int i = head[u];i;i = t[i].next)    {      int v = t[i].to;      if(vis[v]||v == fa)continue;      query(v,u,(lon+t[i].len)%mod);    }return;}ll solve(int st,int prim_len){  sum[0] = sum[1] = sum[2] = 0;  query(st,0,prim_len);  ll tep = 2*sum[1]*sum[2] + sum[0]*(sum[0]-1) + sum[0];  return tep;}void Divide(int temproot){  ans = ans + solve(temproot,0);  vis[temproot] = true;  for(int i = head[temproot];i;i = t[i].next)    {      int v = t[i].to;      if(vis[v])continue;      ans = ans - solve(v,t[i].len);      MX = INF; root = 0; Sumer = sim[v];      getroot(v,0);      Divide(root);    }  return;}void gcd(ll a,ll b){  if(b == 0)dec1 = a;  else gcd(b,a%b);}int main(){  freopen("game.in","r",stdin);  n = gi();  for(int i = 1; i <= n-1 ; i ++)    {      int u = gi(),v= gi(),l = gi()%mod;      addedge(u,v,l);addedge(v,u,l);    }  MX = INF; root = 0;  Sumer = n; ans = 0;  getroot(1,0);  Divide(root);  ll tep1 = n*n; gcd(ans,tep1);  write(ans/dec1); putchar('/'); write(tep1/dec1);  return 0;}

【例题3】【luogu P3806】点分治1
给定一棵树,询问长度为k的树上路径是否存在。
对于每个k每行输出一个答案,存在输出“AYE”,否则输出”NAY”。(k<=10000000)
注意到k的大小在数组可开范围之内,开一个数组sum[i]记录长度为i的路径条数。
点分治计算所有路径长度时注意容斥原理去重,然后在线直接O(1)回答询问即可。

#include<cstdio>#include<cstdlib>#include<iostream>#include<cstring>#include<cmath>#include<algorithm>#define maxn 100100#define INF 2147483646#define ll long longusing namespace std;inline int gi(){  int date = 0,m = 1; char ch = 0;  while(ch!='-'&&(ch<'0'||ch>'9'))ch = getchar();  if(ch=='-'){m = -1; ch = getchar();}  while(ch>='0' && ch<='9')    {      date = date*10+ch-'0';      ch = getchar();    }return date*m;}int sum[10000010];struct node{int to,next,len;}t[maxn+5];int head[maxn+5],cnt;int sim[maxn+5],mxson[maxn+5],dis[maxn+5];int n,m,MX,root,sumary,Smer;bool vis[maxn+5];void addedge(int s,int f,int l){  cnt ++;  t[cnt].to = f;  t[cnt].next = head[s];  t[cnt].len = l;  head[s] = cnt;  return;}void getroot(int u,int fa){  sim[u] = 1; mxson[u] = 0;  for(int i = head[u];i;i = t[i].next)    {      int v = t[i].to;      if(vis[v]||v == fa)continue;      getroot(v,u);      sim[u] = sim[u] + sim[v];      mxson[u] = max(mxson[u],sim[v]);    }  mxson[u] = max(mxson[u],Smer - sim[u]);  if(mxson[u]<MX){root = u; MX = mxson[u];}}void query(int u,int fa,int lon){  dis[++sumary] = lon;  for(int  i = head[u];i;i = t[i].next)    {      int v = t[i].to;      if(vis[v]||v == fa)continue;      query(v,u,lon+t[i].len);    }  return;}void solve(int st,int prim,int jud){  sumary = 0;  query(st,0,prim);  if(jud == 1)    {      for(int i  = 1;i <= sumary-1; i ++)        for(int j = i+1;j <= sumary; j ++)      sum[dis[i]+dis[j]]++;    }  else if(jud == 0)    {      for(int i = 1; i <= sumary-1; i ++)    for(int j = i+1; j <= sumary; j ++)      sum[dis[i]+dis[j]]--;    }return;}void divide(int tr){  vis[tr] = true;  solve(tr,0,1);  for(int i = head[tr];i;i = t[i].next)    {      int v = t[i].to;      if(vis[v])continue;      solve(v,t[i].len,0);      MX = INF; root = 0; Smer = sim[v];      getroot(v,0);      divide(root);    }  return;}void give_ans(){  for(int i = 1; i <= m ; i ++)    {      int k = gi();      if(sum[k])printf("AYE\n");      else printf("NAY\n");    }  return;}int main(){  freopen("divide.in","r",stdin);  n = gi(); m = gi(); cnt = 0;  for(int i = 1; i <= n-1 ; i++)    {      int x = gi(),y = gi(),l = gi();      addedge(x,y,l); addedge(y,x,l);    }  root = 0; MX = INF; Smer = n;  getroot(1,0);  divide(root);  give_ans();  return 0;}

总之,点分治其实就是一种高级的暴力(至少我是这么觉得的),它可以用O(nlogn)的时间求解出需要O(n^2)~O(n^3)才能解决的路径问题。点分治的应用还有很多,更深层次的东西还需要读者自己去理解体会,我这里也只是抛砖引玉,希望能够帮助到各位读者。