木に関するDP手法の一種。
詳細なアルゴリズムは、他の記事が多く出てくるのでそちら参照。
「木で、ある頂点を中心に考えたときの何らかの値」を「全ての頂点について」求めたいときに使うことがある。
①を根として木DPを行うことで、①を中心に考えたときの値を求められるような問題があったとする。
このとき、頂点②を中心に考えたいとき、最初からまた木DPを行うのでなく、 ①を計算したときの結果を上手くずらして使うことで、計算量を削減できる。
① ② / \ ⇙⇓\ ②から④,⑤方向への部分木 および ② ③ → ④ ⑤ ① ①から③方向への部分木は /\ /\ ⇓ ①を根としたDPで求まっている(二重矢印) ④⑤ ⑥⑦ ③ ⇙⇘ あとは、親子関係を①→②から②→①に ⑥ ⑦ 付け替えたときの差分を計算すればよい。 (DP[1] から DP[2] に相当する分を除き、 DP[2] にはその除いた分を加える)
1つ頂点を遷移するたびに、1本の辺の付け替えさえ計算すればいい。
これを $O(1)$ でできるなら、根からのDFSを2回行うことで、$O(N)$ で計算できて嬉しい。
DPに乗せる値と、その値同士をマージする演算が可換モノイドであればよい。
つまり、DPに乗せる値 $a,b,c,...$ とその間の演算 $\cdot$ があって、
が成り立てばよい。
厳密には「可換である」は必要条件では無い。
ただ、順番に意味があるなら、それぞれの頂点に対して子の順序が決まっているということなので、ちょっと特殊である。
実装が面倒くさくなるので、ここでは可換を前提とする。
骨組みをライブラリ化して、必要な関数のみ外部注入することで、コーディング量を減らしたい。
木DPのアルゴリズムを、頂点 $v$ を中心に考える。
問題によっては省略できる処理もあるが、だいたい求められるのは以下のような流れかと思う。
ここで、「加工」「マージ」「反映」の具体例については、例えば以下の例題で
ⓟ DP1[u] = 15, DP1[w] = 12, Av=9 とする。 ↓2 ⓥ DP1[v]を求めるには、各子の値に辺のコストを加算して ↙3 5↘ 15+3, 12+5 とする処理がいる。これを「加工」とする。 ⓤ ⓦ ↓... ↓... 今回は最大値を求めたいので、max(18, 17) が「マージ」となる。 さらに、v自身の値も加算する処理がいる。18+9 これを「反映」とする。 以上より、DP1[v]=27 となる。
★1~★4のざっくり4つの関数が、抽象化のために必要になる。
しかし、よく考えるとDP1には★4の結果を置く必要性があまりない。
どっちみち2回目のDPで、★3の結果と $p$ をマージしなおして、改めて★4を行うため。
なので、1回目のDPの★4については★2と一緒にしてしまえる。
(DP1には★3までの結果を置き、次に親に伝播させるタイミングで★4と★2の処理を同時に行う)
「DP1は、子をマージするところまでの結果で、自身の頂点の値は反映前」とした方が無駄が無い。
そうすると2回目のDPで $u$ に伝播させる際の★4→★2もまとめることができる。
こうなる。
ここで、2回目のDPの★4(単独)の部分は★4→2の関数の引数に加工フラグを持たせ、 Falseの場合は★2に相当する処理を行わないとすれば、結局、定義すべき関数は3つになる。
DPに乗せる値の型を T
として、
leaf(v) → T
apply(dp_u:T, u, v, edge, flag:bool) → T
merge(a:T, b:T) → T
さらに、DPの値をマージする元となる、単位元を返す関数 unit() → T
もあると実装しやすくなる。
この4つを定義することで、全方位木DPを抽象化できる。
ただ、applyに関しては、問題によってはもう少し何らかの引数を増やす必要が生じるかも知れない?