AtCoder ABC236G – Good Vertices – 题解 – 矩阵乘法优化DP

昨晚参与AtCoder ABC 236时认为G题似乎可做,直到看到数据范围才发现事情没有那么简单

由于最近学习离线数据结构,又见识过一道题意接近的题目,故想到整体二分,二分每一个节点第一次能恰好经过LL条边到达的时间,然后考虑DP求解。设计状态f(i,x)f(i, x)表示在该图上经过恰好ii次转移能否到达点xx。但很快发现每一次二分时均要重新在图上运行DP,时间复杂度无法承受。况且DP递推转移的次数LL高达10910^9,甚至不可能是线性的复杂度。

今天查看题解,发现有几个重要的,从未考虑过的trick:

  1. 既然要求能够经恰好LL次转移到达的最早的时间点,则我们将每条边的边权ww设为其加入图中的时间,则可以转化成恰有LL条边的最小瓶颈路
  2. 发现LL很大,所以考虑通过矩阵乘法加速递推,也即所谓倍增优化DP的一种。

重新定义状态f(i,x)f(i, x)为“恰好经过ii次转移,到达节点xx的所有路径中最大边权最小值(瓶颈路)”。设存在一些边ej:=ujvje_j:= u_j \rightarrow v_j,边权为wjw_j,则我们有f(i,vj)=min{max(f(i1,uj),wj)}f(i, v_j)=\min\{\max (f(i-1, u_j), w_j)\}

问题来了:一般的矩阵乘法递推只适用于只包含数乘×\times和数加++的运算,为什么取min,max\min, \max的运算也符合条件?

  1. 若在集合RR上有二元运算  ,+\space \cdot \space, +,满足以下条件:
    • (R,+)(R, +)是带有单位元0\mathbf{0}的交换幺半群:
      • 满足结合律:(a+b)+c=a+(b+c)(a+b)+c = a+(b+c)
      • 对于单位元有:0+a=a+0=a\mathbf{0}+a=a+\mathbf{0}=a
      • 满足交换律:a+b=b+aa+b=b+a
    • (R,)(R, \cdot)是带有单位元1\mathbf{1}的幺半群:
      • 满足结合律:a(bc)=(ab)ca\cdot(b\cdot c)=(a\cdot b)\cdot c
      • 对于单位元有:1a=a1=a\mathbf{1} \cdot a = a \cdot \mathbf{1} = a
    • 运算\cdot对运算++有分配律:
      • (a+b)c=ac+bc(a+b)\cdot c=a \cdot c + b \cdot c
    • 00可以吸收RR
      • 0a=a0=0\mathbf{0}\cdot a = a \cdot \mathbf{0} = \mathbf{0}

    则我们称集合RR为一个半环。

对于全面的关于“群”的性质,请移步群论

只要是一个半环,那么矩阵乘法对该集合的两种运算均成立。

考虑矩阵乘法的定义。两个矩阵A,BA, B的积为:(AB)ij=r=1nairbrj=ai1b1j+ai2b2j++ainbnj{\displaystyle (AB)_{ij}=\sum _{r=1}^{n}a_{ir}b_{rj}=a_{i1}b_{1j}+a_{i2}b_{2j}+\cdots +a_{in}b_{nj}}其中只包含有运算+,+, \cdotaba \cdot b可略作abab),则若所有元素a,ba, b处于一个(半)环中,根据定义可知,矩阵乘法可以应用于该集合中的元素。

现在我们回到这道题上来。我们定义运算(max,)(\max, \cdot)以及(min,+)(\min, +),它们在外层取min\min不调换顺序的情况下,满足交换律、结合律。可以发现,原DP转移方程可以写作f(i,vj)=min{max(f(i1,uj1),wj1),max(f(i1,uj2),wj2),}=f(i1,uj)wj\begin{align} f(i, v_j) &=\min\{\max (f(i-1, u_{j1}), w_{j1}), \max (f(i-1, u_{j2}), w_{j2}), \dots\} \newline &=\sum f(i-1, u_j)\cdot w_j \end{align}

这满足半环的定义,所以我们就可以使用矩阵乘法愉快地递推了。更具体地,我们定义状态矩阵AA,有Ai,j={wk,k,ek:=jiinf,otherwiseA_{i, j}= \begin{cases} w_k, & \exists k, e_k:= j \rightarrow i \newline \inf, &\text{otherwise} \end{cases},然后由矩阵乘法的结合律可知,计算AlA^l后将其与原状态矩阵FF相乘即可。

更一般的,我们发现以下集合中的运算均满足半环的性质:

  1. 定义在实数集R\mathbb{R}上的数乘×\times和数加++
  2. 定义在集合A:=R{}A := \mathbb{R}\cup \{-\infty \}上的运算(max,+)(\max, +)和普通数乘(×,)(\times, \cdot)
  3. 定义在集合A:=Z[1,2n)A := \mathbb{Z} \cup [1, 2^n)的运算(逻辑异或,+)(逻辑异或 \oplus, +)(逻辑与,)(逻辑与, \cdot)
  4. (upd 04.28) 定义在实数集R{+}\mathbb{R}\cup \{+ \infty\}上的运算(min,+)(\min, +)和数加(+,)(+, \cdot)

总而言之,发现递推次数极大,且发现其包含的运算满足结合律和交换律时,一般考虑矩阵乘法加速递推

Submission #28768750

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
  74. 74
  75. 75
  76. 76
  77. 77
  78. 78
  79. 79
  80. 80
  81. 81
  82. 82
  83. 83
  84. 84
  85. 85
  86. 86
  87. 87
  88. 88
  89. 89
  90. 90
  91. 91
  92. 92
  93. 93
#include <bits/stdc++.h> using namespace std; template <typename T> inline bool read (T &x) { x = 0; int f = 1; char c = getchar (); for (; c != EOF && !isdigit (c); c = getchar ()) if (c == '-') f = -1; if (c == EOF) return 0; for (; c != EOF && isdigit (c); c = getchar ()) x = (x<<1) + (x<<3) + (c^48); x *= f; return 1; } template <typename T, typename... Targs> inline bool read (T &x, Targs&... args) { return read (x) && read (args...); } template <typename T> void print (T x) { if (x < 0) putchar ('-'), x = -x; if (x > 9) print (x / 10); putchar ('0' + x % 10); } template <typename T, typename... Targs> inline void print (T x, Targs... args) { print (x), putchar (' '), print (args...); } #define reint register int #define inl inline #define newl putchar('\n') typedef long long ll; typedef unsigned long long ull; // typedef __int128 lll; typedef pair <int, int> pint; constexpr int N = 105, INF = 0x3f3f3f3f; int n, t, l, w[N][N], x, y; struct matrix { int **mat, h, w; matrix () {} matrix (int x, int y) { h = x, w = y; mat = new int*[h + 1]; for (int i = 1; i <= h; ++i) { mat[i] = new int[w + 1]; for (int j = 1; j <= w; ++j) mat[i][j] = INF; } } inl int * operator [] (int x) { return mat[x]; } friend matrix operator * (matrix &a, matrix &b) { matrix c (a.h, b.w); for (int i = 1; i <= a.h; ++i) for (int j = 1; j <= b.w; ++j) for (int k = 1; k <= a.w; ++k) c[i][j] = /* + 运算 */ min (c[i][j], /* · 运算 */max (a[i][k], b[k][j])); return c; } inl matrix & operator *= (matrix &b) { matrix c = (*this) * b; return (*this) = c; } }; matrix trans, f; inl matrix fpow (matrix a, int b) { matrix res; bool flag = 0; for (; b; b >>= 1) { if (b & 1) { if (flag) res *= a; else res = a, flag = 1; } a *= a; } return res; } int main () { /* ABC236G Good Vertices 吴秋实 */ read (n, t, l); for (int i = 1; i <= t; ++i) read (x, y), w[x][y] = i; trans = matrix (n, n); for (int i = 1; i <= n; ++i) for (int j = 1; j <= n; ++j) if (w[i][j]) trans[j][i] = w[i][j]; trans = fpow (trans, l); f = matrix (n, 1); f[1][1] = 0; f = trans * f; for (int i = 1; i <= n; ++i) print (f[i][1] != INF ? f[i][1] : -1), putchar (' '); return 0; }
  • 2022年1月24日