树的点分治

START

       [参考资料:漆子超《分治算法在树的路径问题中的应用》+李煜东《算法竞赛进阶指南》]

       

    本片总结讨论点分治。

  1. 分治,指的是分而治之。即将一个问题分割成一些规模较小的相互独立的子问题,以便各个击破
  2. 点分治:首先选取一个点作为根结点使无根树变成有根树,再对每棵子树进行递归求解。为了缩小问题规模,这个点应为重心,即以它为根,最大子树的规模能够尽可能小的节点(可以证明,以重心为根时,最大子树的节点数不超过N/2)
  3. 这个算法可能的来由:假设我们要做树上的路径问题。朴素做法是从每个点出发,遍历它的所有子孙节点。但这样的时间复杂度是N^2的。为什么慢?以极限数据——一条链举例,每次都DFS,那么访问的子孙个数:N,N-1,N-2……1。也就是说,这样做,使的访问的子树规模不能快速地缩小,导致了许多节点被重复访问,于是T了。怎么解决呢?就从这里入手,我们想要使得子树规模小,那就要它们尽可能平均分配,使得每进入一层,“淘汰”掉的节点数尽可能多,效率就能高很多。这正是点分治的奇妙所在。
  4. 一点感想:第一次在寒假时看,明显理解得不透彻,代码也是半抄半写。暑假时重温,自己通过原理捋顺了思路,瞬间柳暗花明。所以,在发现不懂时,要从原理出发,自己想一遍,硬啃、硬背别人的代码没有任何用处!!!
  5. 在写例题1时,还是发现了很多问题,从一大堆注释+调试信息中就可以发现我的码力是有多么菜了。路越遥,码力越要提升。加油~

【例题1:Tree  poj1741】

题目大意:

给定一棵N个节点、边上带权的树,再给出一个K,询问有多少个数对(i,j)满足i<j,且i与j两点在树上的距离小于等于K。

多组测试数据,每组数据满足N≤10000,1≤边上权值≤1000,1≤K≤10^9。

       

分析:

     假如给你一个棵树,形如:

 

    目前运行到红点,不难分析出,路径存在以下两种:

1) 路径不经过根结点

2) 路径经过根结点

    感觉上这两种需要单独处理,其实不然。1)虽然不经过目前的根结点,但是可能经过子树,子子树的根结点……也就是说,其实道理是一样的,那么解决1),只需通过递归,进行与2)的操作即可。

    那么,如何求解2)呢?

    根据题目对路径的要求,合法点对(i,j)必须满足i到根的距离+j到根的距离小于等于k并且i,j不属于同一棵子树中。

    于是,定义dis[x]表示节点x到目前的根节点的距离belong[x]表示节点x所属的子树。

    如果直接考虑得到目标解,显然不易求。换个“大减小”的思路。

    目标解=

    dis[i]+dis[j]<=k的点对数 -

    dis[i]+dis[j]<=k并且i,j属于同一棵子树中的点对数。

    先考虑前者怎么求。自然不能用N^2大暴力。但是有序的话就好做很多了。因此,可以对根结点的所有子孙节点到它的距离排序,然后利用单调性,不难YY出一个O(N)的做法(双指针扫描/尺取法)。

    至于后者呢?需要另外想做法吗?其实不用的。仍是化异为同思想。实际上就是在特定的子树中进行类似操作。不过值得注意的是,需要初始化该子节点的dis值为该边的权值,问题就能够成功转化。

    但是,如果只是任选一个点作为根进行上述操作,很显然会TLE。因为当树是一条链时,复杂度是N^2的。

    因此,为了使得子树规模快速缩小,每次处理一棵树时,就要先找出它的重心作为它的根。然后才能进行一系列的操作。

    至于重心的求法,其实并不难。加入我们在一棵树中找重心,该树总的大小为all。假定我们选定x为根,那么当它为根时,各子树大小就为,  x的各儿子的子树大小 以及all-x的子树大小。要做到这些,只要在求重心前,对该树进行一次dfs,预处理出各子树大小即可。

   整理一下该题算法流程:

  1. 找重心
  2. 计算各子孙与重心的距离
  3. 计算所有点对
  4. 逐个访问儿子,先减去不合法点对,再递归求解儿子

    时间复杂度怎么求?根据找重心的方法,以及每棵子树大小不超过整棵树大小的一半这条定理,因此最多共logN层。每层会对dis进行汇总+排序,故总的时间复杂度为O(N*logN*logN)  

    另外,由于这里涉及多个函数,使用全局变量更能使代码简洁美观。但是注意,递归中一定要对全局变量进行回溯的处理,否则就会对答案造成重大影响,甚至是RE!!!

    还有,有时候因为懒,会在输入时定义局部变量。但是,并非所有的变量都可以这样。例如本题,如果输入时定义局部变量k,就会导致递归函数中访问到的k=0,造成严重的错误。

    最后,对于deep[i]写成dis[i],能够说明两点,一是不要使用意思太相近的数组名,容易搞混;再者写之前想好其表示的意义!

    这一轮调试,太酸爽了。

    本题编码中给我的最大警醒,就是对变量要求的仔细思考!!!!!

 

    代码如下:

 

  1 #include<iostream>
  2 #include<cstdio>
  3 #include<algorithm>
  4 #include<vector>
  5 #include<cstring>
  6 using namespace std;
  7 const int MAXN=1e4+5;
  8 int dis[MAXN],f[MAXN],v[MAXN],deep[MAXN];
  9 int stand,last,k,inf=0x7fffffff,ans,newroot,n; 10 //n,k定义全局变量!!! 
 11 struct wyy
 12 {
 13     int to,va;
 14 };
 15 vector<wyy>edge[MAXN];
 16 void init()
 17 {
 18     memset(v,0,sizeof(v));
 19     memset(f,0,sizeof(f));
 20     memset(dis,0,sizeof(dis));
 21     memset(deep,0,sizeof(deep));
 22     for(int i=1;i<MAXN;i++)
 23         edge[i].clear();
 24     ans=0;
 25 }
 26 void dfs(int now,int fa)//计算各子树大小。。。。。。。。 
 27 {
 28     int size=edge[now].size();
 29     f[now]=1;
 30     for(int i=0;i<size;i++)
 31     {
 32         int to=edge[now][i].to,va=edge[now][i].va;
 33         if(to!=fa&&v[to]==0)//不能搜到上头,不能往回搜。。。。。。 
 34         {
 35             dfs(to,now);
 36             f[now]+=f[to];            
 37         }        
 38     }
 39     //cout<<now<<" "<<f[now]<<endl;
 40 }
 41 void findroot(int now,int fa,int all)//找重心 
 42 {
 43     int size=edge[now].size();
 44     int nowmax=all-f[now];//nowmax表示,选择当前点为根时的最大子树大小 
 45     for(int i=0;i<size;i++)
 46     {
 47         int to=edge[now][i].to,va=edge[now][i].va;
 48         if(to!=fa&&v[to]==0)//不能搜到上头,不能往回搜。。。。。。 
 49         {
 50             nowmax=max(nowmax,f[to]);
 51             findroot(to,now,all);
 52         }        
 53     }    
 54     if(nowmax<stand)
 55     {
 56         stand=nowmax;
 57         newroot=now;
 58     }
 59 } 
 60 void getdis(int now,int fa)//计算以该重心为根的树,其余各点到根的距离 
 61 //边求,边存入数组!!!!!!!!!!!!!!! 
 62 {
 63     int size=edge[now].size();
 64     //if(dis[now]!=0)不要自以为是地去掉零, 
 65     //0也要算进去!!!!!! 因为存在根到节点的距离 
 66     deep[++last]=dis[now];
 67     for(int i=0;i<size;i++)
 68     {
 69         int to=edge[now][i].to,va=edge[now][i].va;
 70         if(v[to]==0&&to!=fa)
 71         {
 72             dis[to]=dis[now]+va;
 73             getdis(to,now);
 74         }
 75     }
 76 }
 77 int calc(int nowroot,int basic)//每个距离的基准值 
 78 {
 79     int i,j,ans2;
 80     dis[nowroot]=basic;
 81     last=0;//从头开始 
 82     getdis(nowroot,0);
 83     sort(deep+1,deep+1+last);
 84 //    for(i=1;i<=last;i++)
 85     //    cout<<deep[i]<<" ";
 86     
 87     //尺取法要对着数列模拟,把单调性想清楚。
 88     i=1,j=last,ans2=0;    
 89     while(i<j) 
 90     {
 91     //    cout<<"i="<<i<<endl;
 92     //    cout<<"j="<<j<<endl;
 93         //if(dis[i]+dis[j]<=k)//数组名错啦。。。。。。
 94         if((deep[i]+deep[j])<=k)//对于重要的变量,如k,必须使用全局变量。输入的时候就要考虑清楚。 
 95         {
 96             ans2+=j-i;
 97             //cout<<ans2<<" ";
 98             i++;
 99         }
100         else    j--;
101     }
102 //    cout<<"ans2="<<ans2<<endl;
103     return ans2;//局部变量和全局变量不可重名…………………………………… 
104 }
105 void work(int froot)//工作总流程,falseroot为假的根 
106 //计算假根,以及搜索时,不能搜到上头,不能往回搜。。。。。。 
107 {
108     dfs(froot,0);//求各子树大小。    
109     stand=inf;// 各子树最大值  的最小值
110     findroot(froot,0,f[froot]); //找重心     
111     //做自己!
112     v[newroot]=1;//有改动 
113     ans+=calc(newroot,0);//计算总结果 
114     //cout<<froot<<" "<<newroot<<" "<<ans<<endl;    
115     int size=edge[newroot].size();
116     
117     for(int i=0;i<size;i++)
118     {
119         int to=edge[newroot][i].to,va=edge[newroot][i].va;
120         if(v[to]==0)
121         {
122             ans-=calc(to,va);
123             //cout<<to<<" "<<ans<<endl;
124             int nowlast=last,nowroot=newroot,nowstand=stand;
125             
126             work(to);
127             //递归中,不要乱用全局变量,实在要用,必须回溯。。。。 
128             //全局变量要回溯!!!!!!!!!!!!!!! 
129             last=nowlast;
130             newroot=nowroot;
131             stand=nowstand;
132         }            
133     }
134 }
135 int main()
136 {
137     ios::sync_with_stdio(false);
138     while(1)
139     {
140         cin>>n>>k;
141         if(n==0)
142             break;
143         init();//最后看着数组重新补充 
144         for(int i=1;i<n;i++)
145         {
146             int u,v,l;
147             cin>>u>>v>>l;
148             edge[u].push_back((wyy){v,l});
149             edge[v].push_back((wyy){u,l});
150         }
151         work(1);
152         printf("%d\n",ans);
153     }
154     return 0;
155 }

 

posted @ 2018-02-03 11:17  littlewyy  阅读(156)  评论(0编辑  收藏  举报