【学习笔记】支配树

【前言】

  • 本文为博主的转载,由于博主看到的文章同样是转载的,无法注明原文出处。
  • 博主在原文的基础上修改了格式、措辞和一些小错误,并适当添加了一些自己的理解。

【支配树简介】

  • 对于一个单源有向图上的每个点 w ,都存在点 d 满足去掉 d 之后起点无法到达 w ,我们称作 d 支配 w d w 的一个支配点。

  • 支配 w 的点可以有多个,但是至少会有一个。显然,对于起点以外的点,它们都有两个平凡的支配点,一个是自己,一个是起点。

  • 在支配 w 的点中,如果一个支配点 i w 满足 i w 剩下的所有支配点支配,则这个 i 称作 w 的最近支配点 ( i m m e d i a t e   d o m i n a t o r ) ,记作 i d o m ( w )

  • 定理 1 :我们把图的起点称作 r ,除 r 以外每个点均存在唯一的 i d o m

    证明:如果 a 支配 b b 支配 c ,则 a 一定支配 c ,因为到达 c 的路径都经过了 b 所以必须经过 a 。如果 b 支配 c a 支配 c ,则 a 支配 b (或者 b 支配 a ),否则存在从 r b 再到 c 的路径绕过 a ,与 a 支配 c 矛盾。这就意味着支配定义了点 w 的支配点集合上的一个全序关系,所以一定可以找到一个“最小”的元素使得所有元素都支配它。

  • 于是, i d o m ( w ) w   ( w r ) 的边,就能得到一棵树,其中每个点支配它子树中的所有点,它就是支配树。

  • 下文介绍了构建支配树的 L e n g a u e r T a r j a n 算法,其时间复杂度为 O ( N L o g N + M ) ,空间复杂度为 O ( N + M )

  • 为了能够求出支配树,我们下面来推导一下需要用到的一些定理。

【定理推导】

  • 首先,我们会使用一棵 D F S 树来帮助我们计算。从起点出发进行 D F S 就可以得到一棵 D F S 树。

  • 原图中的边被分为了以下两类:在 D F S 树上出现的边称作树边,剩下的边称为非树边。非树边也可以分为几类:从祖先指向后代(前向边),从后代指向祖先(后向边),从一棵子树內指向另一棵子树内(横叉边)。树边是我们非常熟悉的,所以着重考虑一下非树边。

  • 我们按照 D F S 到的先后顺序给点从小到大编号(在下面的内容中我们通过这个比较两个节点),那么前向边总是由编号小的指向编号大的,后向边总是由大指向小,横叉边也总是由大指向小。现在在 D F S 树上我们要证明一些重要的引理:

  • 引理 1 (路径引理):如果两个点 v , w 满足 v w ,那么任意 v w 的路径经过 v , w 的公共祖先。(注意这里不是说 L C A

    证明:如果 v , w 其中一个是另一个的祖先显然成立。否则删掉起点到 L C A 路径上的所有点(这些点是 v , w 的公共祖先),那么 v w 在两棵子树内,并且因为公共祖先被删去,无法通过后向边到达子树外面,前向边也无法跨越子树,而横叉边只能从大到小,所以从 v 出发不能离开这颗子树到达 w 。所以如果本来 v 能够到达 w ,就说明这些路径必须经过 v , w 的公共祖先。

  • 在继续之前,我们先约定一些记号:

    V 代表图的点集, E 代表图的边集。 a b 代表从点 a 直接经过一条边到达点 b a b 代表从点 a 经过某条路径到达点 b a ˙ b 代表从点 a 经过 D F S 树上的树边到达点 b a b D F S 树上的祖先), a + b 代表 a ˙ b a b

  • 定义:半支配点 ( s e m i d o m i n a t o r ) :对于 w r ,它的半支配点定义为 s d o m ( w ) = min { v   |   ( v 0 , v 1 , , v k 1 , v k ) , v 0 = v , v k = w , 1 i k 1 , v i > w }

    这个定义可以理解为从 v 出发,可以绕过小于 w 的所有点到达 w (只能以大于 w 的点作为落脚点)的最小的 v

    注意这只是个辅助定义,并不是真正的支配点。

  • 引理 2 :对于任意 w r ,有 i d o m ( w ) + w

    证明:如果不是这样的话就可以直接通过树边不经过 i d o m ( w ) 就到达 w 了,与 i d o m 定义矛盾。

  • 引理 3 :对于任意 w r ,有 s d o m ( w ) + w

    证明:对于 w D F S 树上的父亲 f a w f a w w 这条路径只有两个点,所以满足 s d o m 定义中的条件,于是它是 s d o m ( w ) 的一个候选。所以 s d o m ( w ) f a w 。在这里我们就可以使用路径引理证明 s d o m ( w ) 不可能在另一棵子树,因为如果是那样的话就会经过 s d o m ( w ) w 的一个公共祖先,公共祖先的编号一定小于 w ,所以不可行。于是 s d o m ( w ) 就是 w 的真祖先。

  • 引理 4 :对于任意 w r ,有 i d o m ( w ) ˙ s d o m ( w )

    证明:如果不是这样的话,按照 s d o m 的定义,就会有一条路径是 r ˙ s d o m ( w ) w 不经过 i d o m ( w ) 了,与 i d o m 定义矛盾。

  • 引理 5 :对于满足 v ˙ w 的点 v , w v ˙ i d o m ( w ) i d o m ( w ) ˙ i d o m ( v )

    直观地理解就是 i d o m ( w ) w 的路径两两之间边不相交或者存在完全包含关系。

    证明:如果不是这样的话,就说明 i d o m ( v ) + i d o m ( w ) + v + w ,那么存在路径 r ˙ i d o m ( v ) v + 不经过 i d o m ( w ) 到达了 w (因为 i d o m ( w ) i d o m ( v ) 的真后代,一定不支配 v ,所以存在绕过 i d o m ( w ) 到达 v 的路径),矛盾。

  • 上面这 5 条引理都比较简单,但是非常重要的性质。接下来我们要证明几个定理,它们揭示了 i d o m s d o m 的关系。证明会比上面的复杂一点。

  • 定理 2 :对于任意 w r ,如果所有满足 s d o m ( w ) + u ˙ w u 也满足 s d o m ( u ) s d o m ( w ) ,那么 i d o m ( w ) = s d o m ( w )

    证明:条件可以写为 s d o m ( w ) ˙ s d o m ( u ) + u ˙ w

    由上面的引理 4 知道 i d o m ( w ) ˙ s d o m ( w ) ,所以只要证明 s d o m ( w ) 支配 w 就可以保证是最近支配点了。对任意 r w 的路径,取上面最后一个编号小于 s d o m ( w ) x (如果 s d o m 就是 r 的话显然定理成立),它必然有个后继 y 满足 s d o m ( w ) ˙ y ˙ w (否则 x 会变成 s d o m ( w ) ),我们取最小的那个 y 。同时,如果 y 不是 s d o m ( w ) ,根据条件, s d o m ( y ) s d o m ( w ) ,所以 x 不可能是 s d o m ( y ) ,这就意味着 x y 的路径上一定有一个 v 满足 x + v + y ,因为 x 是小于 s d o m ( w ) 的最后一个,所以 v 也满足 s d o m ( w ) ˙ v ˙ w ,但是我们取的 y 已经是最小的一个了,矛盾。于是 y 只能是 s d o m ( w ) ,那么我们就证明了对于任意路径都要经过 s d o m ( w ) ,所以 s d o m ( w ) 就是 i d o m ( w )

  • 定理 3 :对于任意 w r ,令 u 为所有满足 s d o m ( w ) + u ˙ w u s d o m ( u ) 最小的一个,那么 s d o m ( u ) s d o m ( w ) i d o m ( w ) = i d o m ( u )

    证明:条件可以写为 s d o m ( u ) ˙ s d o m ( w ) + u ˙ w

    由引理 5 ,有 i d o m ( w ) ˙ i d o m ( u ) u ˙ i d o m ( w ) ,由引理 4 排除后面这种。所以只要证明 i d o m ( u ) 支配 w 即可。类似定理 2 的证明,我们取任意 r w 路径上最后一个小于 i d o m ( u ) x (如果 i d o m ( u ) r 的话显然定理成立),路径上必然有个后继 y 满足 i d o m ( u ) ˙ y ˙ w (否则 x 会变成 s d o m ( w ) ),我们取最小的一个 y 。类似上面的证明,我们知道 x y 的路径上不能有点 v 满足 i d o m ( u ) ˙ v + y ,于是 x 成为 s d o m ( y ) 的候选,所以 s d o m ( y ) x 。那么根据条件我们也知道了 y 不能是 s d o m ( w ) 的真后代,于是 y 满足 i d o m ( u ) ˙ y ˙ s d o m ( w ) 。但是我们注意到因为 s d o m ( y ) x ,存在一条路径 r ˙ s d o m ( y ) y ˙ u ,如果 y 不是 i d o m ( u ) 的话这就是一条绕过 i d o m ( u ) 的到 u 的路径,矛盾,所以 y 必定是 i d o m ( u ) 。所以任意到 w 的路径都经过 i d o m ( u ) ,所以 i d o m ( w ) = i d o m ( u )

  • 完成了上面两个定理的证明,我们就能够通过 s d o m 求出 i d o m 了。

  • 推论 1 :对于 w r ,令 u 为所有满足 s d o m ( w ) + u ˙ w u s d o m ( u ) 最小的一个,有

        

    i d o m ( w ) = { s d o m ( w ) ( s d o m ( u ) = s d o m ( w ) ) i d o m ( u ) ( s d o m ( u ) < s d o m ( w ) )

    推论 1 可以通过定理 2 和定理 3 可以直接得到。这里一定有 s d o m ( u ) s d o m ( w ) ,因为 w 也是 u 的候选。

  • 接下来我们的问题是,直接通过定义计算 s d o m 很低效,我们需要更加高效的方法,所以我们证明下面这个定理:

  • 定理 4 :对于任意 w r

    s d o m ( w ) = m i n ( v   |   ( v , w ) E , v < w s d o m ( u )   |   u > w , ( v , w ) E , u ˙ v )

    证明:令右侧为 x ,显然右侧的点集中都存在路径绕过 w 之前的点,所以 s d o m ( w ) x 。然后我们考虑 s d o m ( w ) w 的绕过 w 之前的点的路径,如果只有一条边,那么必定满足 ( s d o m ( w ) , w ) E s d o m ( w ) < w ,所以此时 x s d o m ( w ) ;如果多于一条边,令路径上 w 的上一个点为 l a s t ,我们取路径上除两端外满足 p ˙ l a s t 的最小的 p (一定能取得这样的 p ,因为 l a s t p 的候选)。因为这个 p 是最小的,所以 s d o m ( w ) p 的路径必定绕过了 p 之前的所有点,于是 s d o m ( w ) s d o m ( p ) 的候选,所以 s d o m ( p ) s d o m ( w ) 。同时, s d o m ( p ) 还满足右侧的条件( p 在绕过 w 之前的点的路径上,于是 p > w ,并且 p ˙ l a s t ,同时 l a s t 直接连到了 w ),所以 s d o m ( p ) x 的候选, x s d o m ( p ) 。所以 x s d o m ( p ) s d o m ( w ) x s d o m ( w ) 。综上, s d o m ( w ) x x s d o m ( w ) ,所以 x = s d o m ( w )

  • 现在最困难的步骤已经完成了,我们得到了 s d o m 的一个替代定义,而且这个定义里面的形式要简单得多。这种基本的树上操作我们是非常熟悉的,所以没有什么好担心的了。接下来就可以给出我们需要的算法了。

【构造流程】

  • 以下是算法的简要流程

    1 、初始化、跑一遍 D F S 得到 D F S 树和标号
    2 、按标号从大到小求出 s d o m (利用定理 4
    3 、通过推论 1 求出所有能确定的 i d o m ,剩下的点记录下和哪个点的 i d o m 是相同的
    4 、按照标号从小到大再跑一次,得到所有点的 i d o m

  • 以下是算法的具体实现细节

  • 大致要维护的东西:
    p ( x ) 标号为 x 的点 u
    b ( u ) 有边直接连到 u 的点集
    c ( u ) s d o m 为点 u 的点集
    f a t h e r ( u ) u D F S 树上的父亲 f a u
    以及 i d o m s d o m 数组

  • 这里多说一句,由于我们上文的推导中我们经常会用一个点的 D F S 序来代替一个点来论述,我们有时会无法分清一个数组的下标和数值究竟代表一个点 x 在原图中的标号还是它的 D F S 序。

  • 为了统一,我们不妨规定数组的下标一律使用点在原图中的标号,除了计算过程中的 i d o m s d o m 数组记录点的 D F S 序标号以外,其余数组一律记录点在原图中的标号。

  • 算法的第 1 步没什么特别的,规规矩矩地 D F S 一次即可,同时初始化 s d o m 为自己(这是为了实现方便)。

  • 2 、第 3 步可以一起做。通过一个辅助数据结构维护一个森林,支持加入一条边 ( l i n k ( u , v ) ) 和查询点到根路径上的点的 s d o m 的最小值对应的点 ( h o m e ( u ) ) 。那么我们求每个点的 s d o m 只需要对它的所有直接前驱 h o m e 一次,求得前驱中的 s d o m 最小值即可。因为定理 4 中的第一类点编号比它小,它们还没有处理过,所以自己就是根, h o m e 就能取得它们的值;对于第二类点, h o m e 查询的就是满足 u ˙ v u s d o m ( u ) 的最小值。所以这么做和定理 4 是一致的。

  • 然后把该点加入它的 s d o m c 里,连上它与父亲的边。现在它父亲到它的这棵子树中已经处理完了,所以可以对父亲的 c 里的每个点求一次 i d o m 并且清空 c 。对于 c 里的每个点 v ,求出 h o m e ( v ) ,此时 f a t h e r ( w ) + h o m e ( v ) ˙ v ,于是直接按照推论 1 ,如果 s d o m ( h o m e ( v ) ) = s d o m ( v ) ,则 i d o m ( v ) = s d o m ( v ) = f a t h e r ( w ) ;否则可以记下 i d o m ( v ) = i d o m ( h o m e ( v ) ) ,实现时我们可以写成 i d o m ( v ) = h o m e ( v ) ,留到第 4 步处理。

  • 最后从小到大扫一遍完成第 4 步,对于每个 u ,如果 i d o m ( u ) = s d o m ( u ) 的话,就已经是第 3 步求出的正确的 i d o m 了,否则就证明这是第 3 步留下的待处理点,令 i d o m ( u ) = i d o m ( i d o m ( u ) ) 即可。

  • 至于这个辅助数据结构,我们可以选择并查集。不过因为我们需要查询到根路径上的信息,所以不方便按秩合并,但是我们仍然可以路径压缩,压缩时保留路径上的最值就可以了。这样做的话,最终的时间复杂度是 O ( N L o g N + M )

【代码】


#include<bits/stdc++.h>

using namespace std;
const int MAXN = 50005;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
  x = 0; int f = 1;
  char c = getchar();
  for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
  for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
  x *= f;
}
template <typename T> void write(T x) {
  if (x < 0) x = -x, putchar('-');
  if (x > 9) write(x / 10);
  putchar(x % 10 + '0');
}
template <typename T> void writeln(T x) {
  write(x);
  puts("");
}
int n, m, timer, root, dfn[MAXN], p[MAXN], father[MAXN];
int idom[MAXN], sdom[MAXN], f[MAXN], home[MAXN];
long long ans[MAXN]; vector <int> a[MAXN], b[MAXN], c[MAXN];
void dfs(int pos) {
  dfn[pos] = ++timer, p[timer] = pos;
  for (unsigned i = 0; i < a[pos].size(); i++)
      if (dfn[a[pos][i]] == 0) {
          father[a[pos][i]] = pos;
          dfs(a[pos][i]);
      }
}
int F(int x) {
  if (f[x] == x) return x;
  int tmp = f[x];
  f[x] = F(f[x]);
  if (sdom[home[tmp]] < sdom[home[x]]) home[x] = home[tmp];
  return f[x];
}
int gethome(int x) {
  F(x);
  return home[x];
}
void work(int pos, long long sum) {
  ans[pos] = sum;
  for (unsigned i = 0; i < a[pos].size(); i++)
      work(a[pos][i], sum + a[pos][i]);
}
int main() {
  while (scanf("%d%d", &n, &m) != EOF) {
      for (int i = 1; i <= n; i++) {
          a[i].clear();
          b[i].clear();
          c[i].clear();
      }
      for (int i = 1; i <= m; i++) {
          int x, y; read(x), read(y);
          a[x].push_back(y);
          b[y].push_back(x);
      }
      memset(dfn, 0, sizeof(dfn));
      timer = 0; dfs(root = n);
      for (int i = 1; i <= timer; i++) {
          sdom[p[i]] = i;
          idom[p[i]] = 0;
          f[p[i]] = home[p[i]] = p[i];
      }
      for (int i = timer; i >= 2; i--) {
          int tmp = p[i];
          for (unsigned j = 0; j < b[tmp].size(); j++)
              if (dfn[b[tmp][j]]) chkmin(sdom[tmp], sdom[gethome(b[tmp][j])]);
          c[sdom[tmp]].push_back(tmp);
          f[tmp] = father[tmp];
          tmp = dfn[father[tmp]];
          for (unsigned j = 0; j < c[tmp].size(); j++) {
              int tnp = gethome(c[tmp][j]);
              if (sdom[tnp] == tmp) idom[c[tmp][j]] = tmp;
              else idom[c[tmp][j]] = dfn[tnp];
          }
      }
      for (int i = 1; i <= n; i++)
          a[i].clear();
      for (int i = 2; i <= timer; i++) {
          int tmp = p[i];
          if (sdom[tmp] == idom[tmp]) idom[tmp] = p[idom[tmp]];
          else idom[tmp] = idom[p[idom[tmp]]];
          sdom[tmp] = p[sdom[tmp]];
          a[idom[tmp]].push_back(tmp);
      }
      memset(ans, 0, sizeof(ans));
      work(root, root);
      for (int i = 1; i <= n; i++) {
          write(ans[i]);
          if (i == n) putchar('\n');
          else putchar(' ');
      }
  }
  return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_39972971/article/details/82382169