【算法学习】树的重心与点分治

树的重心

树的重心也叫做树的质心。其本质是一个点,删除这个点后,形成的子树中最大的节点数目最小。

解法

一遍dfs即可。dfs的时候记录一下当前节点 u u size[u],同时记录他的所有儿子子树中的最大节点数目 mxchild m x c h i l d ,那么删除当前节点 u u 所形成的子树就是max(mxchild,nsize[u])。维护这个答案的最小值即可求出树的重心。

性质

  1. 树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么他们的距离和一样。

  2. 把两个树通过一条边相连得到一个新的树,那么新的树的重心在连接原来两个树的重心的路径上 。

  3. 把一个树添加或删除一个叶子,那么它的重心最多只移动一条边的距离 。

点分治

先说一些可以参考的资料。

  1. 国家队论文:分治算法在树的路径问题中的应用

  2. 算法竞赛入门指南P392页,路径统计问题

点分治用来解决树上的路径统计问题。复杂度为 nlogn n l o g n 。分治策略的思想是,因为对于一个确定的点 u u ,树上只有两种路径:要么经过u,要么不经过 u u 。每次选取树的重心,统计经过树的重心的路径,此时计算完成后有不合法的路径计算进去,把这些不合法的路径删去后,再递归处理每一个子树。处理子树的方法是上面的方法一致的,找到子树的重心,然后依次处理。

通过如上的方法,可以处理出树上的所有路径。

这里写图片描述

有几个问题说明一下

  1. 为什么选取重心来进行分治?因为重心有较好的性质,可以保证分治的复杂度不退化。想想一下,如果我们对一条链进行分治,那么可能会造成n2的复杂度,这样显然是不可以接受的。

    • 为什么会有重复的路径计算?设 dist[u] d i s t [ u ] 表示节点 u u 到当前分治质心的路径和,设belong[x]为去除质心后的子树的的归属。例如: dist[1]=0,dist[2]=1,dist[6]=2 d i s t [ 1 ] = 0 , d i s t [ 2 ] = 1 , d i s t [ 6 ] = 2 , 又如 belong[5]=3,belong[6]=2,belong[9]=4 b e l o n g [ 5 ] = 3 , b e l o n g [ 6 ] = 2 , b e l o n g [ 9 ] = 4 。当以1为根的时候,我们希望统计出的是经过以1为根的路径。当我们遍历出所有点到1的距离,然后对于其中的任意两个点 u,v u , v ,计算 dist[u]+dist[v] d i s t [ u ] + d i s t [ v ] 即为他们的路径值。这句话对 belong[u]!=belong[v] b e l o n g [ u ] ! = b e l o n g [ v ] 是没问题的,反之是有问题的。如 dist[6]+dist[7] d i s t [ 6 ] + d i s t [ 7 ] 并不会经过点1。如何删除这些答案呢?那么就是递归到质心的子树中,遍历这些子树,把以子树为根的节点统计出的答案全部删掉即可,这样计算出来的就是经过1的路径条数了。
    • 几个注意点

      1. 注意在找质心的时候,不要遍历已经进行过决策的顶点,所以要设置 visit v i s i t 数组。(本质是在除去质心的子树上找他们的质心)
      2. 找质心的时候,要设置全局的节点数目 subtreesize s u b t r e e s i z e ​ 。他表示的是当前要找子树上的节点数目。
      3. 统计答案的时候,具体问题具体分析。

      模板

      void getroot(int u, int f) {
          // 找质心
          sz[u] = 1;
          int mxchild = 0;
          for(int i = head[u]; i != -1; i = e[i].nxt ) {
              int v = e[i].to;
              if(v != f && !visit[v]) {
                  getroot(v, u);
                  sz[u] += sz[v];
                  mxchild = max(mxchild, sz[v]);
              }
          }
          int tmp = max(mxchild, subtreesize - sz[u]);
          if(tmp < nowmn) {
              nowmn = tmp;
              rt = u;
          }
      }
      void getdis(int u, int f, int dis) {
          // 算距离,dis是初始距离
          dist[dis % 3] ++ ;
          for(int i = head[u]; i != -1; i = e[i].nxt) {
              int v = e[i].to;
              if(v != f && !visit[v])
                  getdis(v, u, (dis + e[i].w) % 3);
          }
      }
      ll getans(int u, int initdis) {
          // 统计答案
          dist[0] = dist[1] = dist[2] = 0;
          getdis(u, u, initdis);
          return 2ll * dist[1] * dist[2] + 1ll * dist[0] * dist[0];
      }
      
      void solve(int u) {
          ans += getans(u, 0); // 统计进过u的答案
          visit[u] = true;
          for (int i = head[u]; i != -1; i = e[i].nxt) {
              int v = e[i].to;
              if (!visit[v]) {
                  ans -= getans(v, e[i].w); // 删除在一颗子树中的答案
                  nowmn = INF; rt = 0; subtreesize = sz[v];
                  getroot(v, u);
                  solve(rt);
              }
          }
      }
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值