树的直径,树的重心,树的分冶

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow

也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!

               

主要是利用了反证法:

假设 s-t这条路径为树的直径,或者称为树上的最长路

现有结论,从任意一点u出发搜到的最远的点一定是s、t中的一点,然后在从这个最远点开始搜,就可以搜到另一个最长路的端点,即用两遍广搜就可以找出树的最长路

证明:

1    设u为s-t路径上的一点,结论显然成立,否则设搜到的最远点为T则

dis(u,T) >dis(u,s)     且  dis(u,T)>dis(u,t)   则最长路不是s-t了,与假设矛盾

2   设u不为s-t路径上的点

    首先明确,假如u走到了s-t路径上的一点,那么接下来的路径肯定都在s-t上了,而且终点为s或t,在1中已经证明过了

    所以现在又有两种情况了:

    1:u走到了s-t路径上的某点,假设为X,最后肯定走到某个端点,假设是t ,则路径总长度为dis(u,X)+dis(X,t)

    2:u走到最远点的路径u-T与s-t无交点,则dis(u-T) >dis(u,X)+dis(X,t);显然,如果这个式子成立,

    则dis(u,T)+dis(s,X)+dis(u,X)>dis(s,X)+dis(X,t)=dis(s,t)最长路不是s-t矛盾

    附上一张第二种情况的图

     

   

分类:  图论

树的“重心”的一些性质及动态维护  

2011-08-24 20:31:13|  分类: 程序|字号 订阅

还记得曾经提到过的树的“重心”吗?重心的定义是:以这个点为根,那么所有的子树(不算整个树自身)的大小都不超过整个树大小的一半。

树的重心的一个的性质:
树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么他们的距离和一样。
这也是“道路修建”带来的启发。(证明:调整法)

树的重心的另一个性质:
把两个树通过一条边相连得到一个新的树,那么新的树的重心在连接原来两个树的重心的路径上。
这个让“重心”名副其实了。。。(证明:。。。自己好好思考一下吧。。。)

还有一个性质:
把一个树添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
(证明:提示:张放想都没想说,要不然那两个不等式就矛盾了)

嗯,不错,有这么多性质,可以出不少恶心题了。。。

不过,我还是更关心一个事情:重心的动态维护。
如何动态呢?
情景1:添加一片叶子
根据已有性质,添加一片叶子之后新的重心要么不动要么向那片叶子的方向移动一下。这样,可以用一个link-cut tree来维护。
我们以重心为根建立这个动态树。每个节点维护它所在的子树的大小。添加叶子等于向一条路径上的维护值增加1,这个可以通过打标记实现。发现不得不移动的时候进行一次换根的操作。因为只可能移动1,所以换根的操作是可以完成的。
我们甚至还可以维护所有点到重心的距离和!这个只需给每个点加一个维护值:这个点为根的子树中所有点到这个点的距离和,通过稍微有点复杂的标记系统还是可以维护的。

情景2:删除一片叶子
只有删除操作?那么离线改成添加吧。。。
不允许离线?那么我们要换一个思路:
定义稍微广义的树的重心:每个点有一个非负权,每个边有个正的长度,到所有点的权乘以距离的和最小的点定义为重心。
注意:树的重心的位置和边的长度没有关系!。
在只有权值修改的情况下,我们可以利用树分治配合基本数据结构来维护树的重心:
注意到,我们可以维护一个子树内的点的权值和(利用dfs序)。这样给定一条边,我们就能够知道树的重心在这条边的哪边(看看哪边权值和大就行了)。
这样,我们可以先找一个比较靠近中心的边,问问应该向哪边走,再分治下去,就像树分治那样(类似二分查找?)。
当然,要想一下其他的技巧来对付”星型数据“,这个应该不难(通过拆点、拆边的技巧)。
利用这个广义一点的重心,我们发现,删除操作其实就是把权修改成0而已,可以在log^2N的时间内动态维护了。
如何处理多重心的情况?”抖动“一下权值使得只有一个重心不就行了。。。那另一个重心在哪里?这个只是个细节问题。。。
能否维护距离和?能否在logN的时间内维护?欢迎讨论(将子树和查询与树分治结合起来?。。。)。

情景3:移动一片叶子
把一个叶子移动到另一个地方。
这个怎么维护呢?其实,我们发现,新的重心应该在原来的重心和叶子新的位置的连线上(证明?应该是对的吧),移动距离很小。于是,也就可以维护了。

情景4:移动一个子树(被移动的子树小于原树的一半,并且保持它的根不变)
这个可以维护吗?
新的重心在原来重心和新子树的接合点的连线上吗?(证出来的欢迎留言)
有一个可以和link-cut tree配合使用的工具,叫做Euler-tour tree,它维护树的欧拉回路,基本元素是边。利用它,可以方便的完成子树的移动,并且给定一条有向边,可以回答这条边指向的子树的大小(Eurler-tour tree中没有”根“以及”父亲“这个概念!)。如果上面的那个论断是对的,那么这个应该可以维护了(复杂度?log^2N吧。。。)
另一个思路是树块划分,把树划分成若干联通块,并且在查询的时候合并相邻的联通块使得每个联通块的大小都是sqrt(N)的级别。这个东西对维护是否有帮助?欢迎交流。。。

情景5:开始N个一个点的树,每次用一条边合并两个树,要求回答新的树的重心
离线?在线?logN?log^2N?sqrt(N)?
等待你去探索

POJ 1655 - DP 树的重心,经典 #P

分类: #P DP   119人阅读  评论(0)  收藏  举报
POJ 1655 - DP 树的重心,经典 #P
 
题意:求树的重心。
树的重心:删去重心后,生成的多棵树尽可能平衡。
重心的意义,在对树进行分治的时候可以避免N^2的极端复杂度(从退化链的一端出发),保证NlogN的复杂度。
 
解法:
一开始想到的是模仿求树的直径那样子去Dp,两次DFS。
son[i] - 结点i的儿子结点数目
第一遍求出son;
h[i] - 结点i向上的结点数目
h[i] = h[k] + son[k] - son[i] - 1;
blance = max(son[j] , h[i])
第二遍求出h,和blance;
 
后来去看题解,才发现有更简单的方法。
应用一个性质,h[i] = n - son[i] -1;
blance = max(son[j] , n - son[i] -1);
这样只需要一次DFS。
 
 
[cpp]  view plain copy
  1. #include <cstdio>  
  2. #include <iostream>  
  3. #include <fstream>  
  4. #include <cstring>  
  5. #include <string>  
  6. #include <vector>  
  7. #define OP(s) cout<<#s<<"="<<s<<" ";  
  8. #define PP(s) cout<<#s<<"="<<s<<endl;  
  9. using namespace std;  
  10. int n;  
  11. vector <int> adj[20010];  
  12.   
  13. int son[20010];  
  14. bool vd[20010];  
  15. int ans,asize = 1<<29;  
  16. void DFS(int s)  
  17. {  
  18.     vd[s] = 1;  
  19.     son[s] = 0;  
  20.     int blance = 0;  
  21.     int size = adj[s].size();  
  22.     for (int j = 0;j < size;j++)  
  23.     {  
  24.         int u = adj[s][j];  
  25.         if (vd[u]) continue;  
  26.         DFS(u);  
  27.         son[s] += son[u]+1;  
  28.         blance = max(blance,son[u]+1);  
  29.     }  
  30.     blance = max(blance,n - son[s] - 1);  
  31.     if (blance < asize || blance == asize && s < ans)  
  32.         ans = s,asize = blance;  
  33. }  
  34.   
  35. int main()  
  36. {  
  37. //    freopen("test.txt","r",stdin);  
  38.     int T;  
  39.     cin>>T;  
  40.     while(T--)  
  41.     {  
  42.         cin>>n;  
  43.         for (int i = 1;i <= n;i++) adj[i].clear();  
  44.         for (int i = 1;i <= n-1;i++)  
  45.         {  
  46.             int u,v;  
  47.             scanf("%d%d",&u,&v);  
  48.             adj[u].push_back(v);  
  49.             adj[v].push_back(u);  
  50.         }  
  51.   
  52.         memset(vd,0,sizeof(vd));  
  53.         asize = 1<<29;  
  54.         DFS(1);  
  55.         cout<<ans<<" "<<asize<<endl;  
  56.     }  
  57.   
  58.     return 0;  
  59. }  
 

poj 1741 (树的分治)

分类: 数据结构   80人阅读  评论(0)  收藏  举报

题意:给定一棵N(1<= N <=10000)个结点的带权树,定义dist(u,v)为u,v两点间的最短路径长度,路径的长度定义为路径上所有边的权和。再给定一个 K  ,如果对于不同的两个结点a,b,如果满足dist(a,b) <=K,则称(a,b)为合法点对。求合法点对个数。

思路:看了论文《分治算法在树的路径问题中的应用》,里面讲解的很清楚,一条路径要么过根节点,要么在一颗子树中,所以用分治算法。找到树的重心作为根节点,这样每次树的节点数至少减少一半。处理经过当前根节点路径<=k的点对数,然后把根节点去掉后就把原来的树分成几颗子树了,再处理子树。我们在求经过一个根节点的路径时,里面还包含了点对属于同一颗子树的情况,所以要去掉这部分的点。

dis(i)+dis(j)<=k(i,j的父节点不为根节点的同一个儿子)

=dis(i)+dis(j)<=k-dis(i)+dis(j)<=k(i,j的父节点属于根节点的同一儿子).





[cpp]  view plain copy
  1. #include <algorithm>  
  2. #include<stdio.h>  
  3. #include<string.h>  
  4. const int N=10010;  
  5. using namespace std;  
  6. int head[N],num,f[N],son[N],n,D,root,size,ans,dis[N],d[N],cum;  
  7. bool vis[N];  
  8. #define max(a,b) (a<b?b:a)  
  9. struct edge  
  10. {  
  11.     int st,ed,w,next;  
  12. }e[N*2];  
  13. void addedge(int x,int y,int w)  
  14. {  
  15.     e[num].st=x;e[num].ed=y;e[num].w=w;e[num].next=head[x];head[x]=num++;  
  16.     e[num].st=y;e[num].ed=x;e[num].w=w;e[num].next=head[y];head[y]=num++;  
  17. }  
  18. void getroot(int u,int father)//求树的重心  
  19. {  
  20.     int i,v;  
  21.     f[u]=0;son[u]=1;  
  22.     for(i=head[u];i!=-1;i=e[i].next)  
  23.     {  
  24.         v=e[i].ed;  
  25.         if(vis[v]||v==father)continue;  
  26.         getroot(v,u);  
  27.         son[u]+=son[v];  
  28.         f[u]=max(f[u],son[v]);  
  29.     }  
  30.     f[u]=max(f[u],size-son[u]);  
  31.     if(f[u]<f[root])root=u;  
  32. }  
  33. void getdis(int u,int father)//求节点到根节点的距离  
  34. {  
  35.     int i,v;  
  36.     son[u]=1;//更新子树的节点的子节点数,不更新也能ac  
  37.     d[cum++]=dis[u];//将点到根节点的距离加入数组  
  38.     for(i=head[u];i!=-1;i=e[i].next)  
  39.     {  
  40.         v=e[i].ed;  
  41.         if(vis[v]||v==father)continue;  
  42.         dis[v]=dis[u]+e[i].w;  
  43.         getdis(v,u);  
  44.         son[u]+=son[v];  
  45.     }  
  46. }  
  47. int cont(int u,int mit)  
  48. {  
  49.     int res=0,L,R;  
  50.     dis[u]=mit;  
  51.     cum=0;  
  52.     getdis(u,0);  
  53.     sort(d,d+cum);//将点到根节点的距离排序  
  54.     for(L=0,R=cum-1;L<R;)  
  55.     {  
  56.         if(d[L]+d[R]<=D)//如果d[L]+d[R]<=D,L代表的节点可以与(R-L)个节点成对  
  57.             res+=(R-L++);  
  58.         else R--;  
  59.     }  
  60.     return res;  
  61. }  
  62. void work(int u)  
  63. {  
  64.     int i,v;  
  65.    vis[u]=true;  
  66.    ans+=cont(u,0);//路径经过该根节点的点对数  
  67.    for(i=head[u];i!=-1;i=e[i].next)  
  68.    {  
  69.       v=e[i].ed;  
  70.       if(vis[v])continue;  
  71.       ans-=cont(v,e[i].w);//减去属于v子树的点对数  
  72.       root=0;f[root]=size=son[v];  
  73.       getroot(v,0);//求v子树的根节点  
  74.       work(r

    给我老师的人工智能教程打call!http://blog.csdn.net/jiangjunshow
    这里写图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值