ei1333の日記

ぺこい

QTREE LCT + Dynamic Distance Sum

前の記事 (Link-Cut木と最遠点クエリ - ei1333の日記) の続き

SPOJにQTREE(Query on a tree)の問題群があります。

木に対するクエリの問題で、全部で7問あります。

これらは全部LCT(Link-Cut-Tree)を使って解くことができます。(QTREE LCTとかでぐぐると中国語でたくさんでてくる)

2019/07/17 更新 あとゆきこだの Dynamic Distance Sum もLCTを使って解けます。log^2 なんですが

はじめに

突然ですがLink-Cut-Treeって知っていますか? 僕は知っています(イキり)。知らない人は、がんばって

今回使ったLCTを以下に示しました。 部分木に対するクエリを処理できるように微妙に抽象化してあります。

テンプレート引数でSUMとKEYの2つの型を渡す必要があります。SUM については次に挙げる  {4} つのメンバ関数を実装する必要があります。

  • toggle():= 左部分木と右部分木を入れ替える
  • merge(key, parent, child):=左部分木 parent と右部分木childを現在の頂点の値 key を使ってマージする
  • add(child):= 現在の頂点に Light-Edge で child を繋げる
  • erase(child):= Light-Edgeで繋がっていた child を、現在の頂点から削除する

TODO 説明

QTREE

SPOJ.com - Problem QTREE

問題概要

辺に正の重みがついている. 以下のクエリを処理せよ.

  •  {i} の重みを  {t_i} に変更する
  • 頂点  {a, b} 間の辺の重みのうち最大重みを求める

解法

evert(a) したあとに expose(b) をします。すると、 {a} から  {b} のパス上の頂点だけからなる  {b} を根とする Splay 木ができます。

Splay木の各頂点には、max(左の子のmax, 右の子のmax, key) を持っておけばよいことがわかります(add()とかerase()とかはまだ関係ない)。

実装例

QTREE2

SPOJ.com - Problem QTREE2

問題概要

辺に正の重みがついている. 以下のクエリを処理せよ.

  • 頂点  {a, b} 間の距離を求める
  • 頂点  {a} から頂点  {b} のパスで  {k} 番目に現れる頂点を求める

解法

さっきの問題と同様に、evert(a) したあとに expose(b) をします。すると、 {a} から  {b} のパス上の頂点だけからなる  {b} を根とする Splay 木ができます。

距離クエリに答えるためにSplay木の各頂点には、左の子のsum+右の子のsum+keyを持っておけばよいことがわかります。

 {k} 番目の頂点については、Splay木上を二分探索すると求めることが可能です。

実装例

QTREE3

SPOJ.com - Problem QTREE3

問題概要

各頂点は白または黒の状態をとれて, 最初は全部白である. 以下のクエリを処理せよ.

  • 頂点  {i} の色を反転
  • 頂点  {1} から頂点  {v} のパスで最初に現れる黒頂点を求める

実装例

これもさっきの問題と同様に、evert(1) したあとに expose(b) をします。すると、 {1} から  {b} のパス上の頂点だけからなる  {b} を根とする Splay 木ができます。

(ホントは根は頂点  {1} 固定なので evert() しなくてもなんとかなるんですが、QTREE4以降がむずかしいので一応ここで) 今回の evert() では toggle() で何らかの処理が必要そうです。

Splay木の各頂点には、(もとの木で)もっとも浅い黒頂点の idx (l_idxとする) と(もとの木で)もっとも深い黒頂点の idx (r_idxとする) を持っておきます。Splay木上では浅い方の頂点はより左側に、深い方の頂点はより右側にあることになります。そのような頂点が存在しないとき、-1とします。

merge(key, parent, child)で、l_idx について考えます。まずparentのl_idxが-1でなければそれになります。-1かつkeyが-1ではなければkeyになって、keyもだめならchildのkeyになります。r_idxは逆に見ればよいです。

toggle() が来たら親(Splay木上での左部分木)と子(Splay木上での右部分木)が入れ替わるので swap(l_idx, r_idx) をすればよいです。

QTREE4

SPOJ.com - Problem QTREE4

問題概要

各頂点は白または黒の状態をとれて, 最初は全部白である. 辺に重みがついている. 重みは負もとりうる. 以下のクエリを処理せよ.

  • 頂点  {a} の色を反転
  • すべての白同士の頂点ペア  {a, b} のうち, 最大距離を求める

解法

特に全部白の場合について考えると木に対する直径クエリなので、それよりも難しそうです。また負の重みもあるのでうくちゃんです。

Splay木の各頂点  {v} には、まずSplay木の部分木内で完結する直径のmax(diameterとする)が必要です。また部分木の重みの総和も欲しいです。  {v} の親の更新を考えると、 {v} の部分木のうちどこかの白頂点から  {v}Splay木の最も右の頂点に向かうパスの重みのmax(p_lenとする)と、Splay木の最も左の頂点から  {v} の部分木のどこかの白頂点までのパスの重みのmax(c_lenとする)も欲しいです(ほしいので)。ここでparent.p_lenとchild.c_lenとkeyがあると、左部分木から  {v} を通って右部分木に行くようなパスのうちmaxを得ることが可能です。

p_lenの更新は、max(child.p_len, parent.p_len+child.all) とすればよいです。前者は右部分木の結果で、後者は左部分木のどこかの頂点からchild.allを加算して一番右の頂点へ向かっています。c_lenでは逆をします。

また現在の頂点に Light-Edge で child を繋げたり切ったりするときに、child内の部分木で完結する直径のmaxとlight-edgeを(必ず)使って木を降りた時に可能なパスの重みのmaxも欲しいです。

えーこれらがあるとマージができるので勝ちます(コード見てね)。

直径は、 {v} からのLight-Edgeを2本使うやつ(top + key + md1.top2())、左部分木のmax(parent.diameter)、右部分木のmax(child.diameter)、左部分木から  {v} を通って  {v} からの Light-Edge を使って終わるやつ、 {v} からのLight-Edgeを使って  {v} を通って右部分木へ向かうやつ、 {v} からのLight-Edgeの直径のmax(md2.top()) とかを組み合わせるとできます。常に頂点  {v} で直径の終端となれるとは限らない(白頂点のみ) (気づいたんですがカッコを多用しすぎじゃないか)ので、そのへんの場合分けをすると良いです。

実装例

時間制限があまりにも厳しすぎる う し た ぷ に き あ さ ん

Light-Edgeのうち最大値を管理するフェーズは、 priority_queue を 2 つ持って Light-Edgeの削除を遅延させることにすると定数倍が軽くてよさげ.

QTREE5

SPOJ.com - Problem QTREE5

問題概要

各頂点は白または黒の状態をとれて, 最初は全部黒である. 辺の重みはすべて1である. 以下のクエリを処理せよ.

  • 頂点  {i} の色を反転
  • 頂点  {v} から最も近い白頂点までの距離を求める

解法

QTREE4よりはちょっと簡単です。

Splay木の各頂点に、部分木内の頂点すべての重みの和(all)と、どこかから一番右の頂点へ向かうパスのmin(p_len)と一番左の頂点からどこかへ向かうパスのmin(c_len)を持ってやります。

実装例

QTREE6

SPOJ.com - Problem QTREE6

問題概要

各頂点は白または黒の状態をとれて, 最初は全部黒である. 以下のクエリを処理せよ.

  • 頂点  {u} が所属する連結成分に含まれる頂点の個数を求める.  {u} {v} のパス上のすべての頂点がすべて同じ色なら, 同じ連結成分に所属する.
  • 頂点  {u} の色を反転

解法

同じ色の連結成分だけからなる木を取得できれば、あとは適当に頂点数を求めるだけなので勝ちます。

普通にやるとたぶんできないんですが、頭いいことをするとできます。

疲れたので解説をこどふぉに投げるんですが、Codeforces Round #564 Editorial - CodeforcesのEのTutorialの図を見てみましょう。

黒のみからなる連結成分に加えて、その親に白色の頂点を  {1} 個加えた木を持っています。特に白色の頂点は根です。このように管理することで、linkとcutが効率的にできます。ある頂点が白から黒になったときその頂点を親と繋ぐ(link)、ある頂点が黒から白になったときその頂点を親から切り離す(cut)をすればよいです。

連結成分の個数クエリを処理することを考えます。頂点  {u} が今黒色だとするとこの木の根は白色です。白色を挟んで別の黒色の連結成分が繋がっている可能性があるので困ります。

expose( {u}) をすることで白色の頂点から  {u} までのパス上の頂点からなるSplay木を得ることができます。このSplay木の  {1} 番左の頂点が白色の頂点です。 {2} 番目の頂点は、(もともとの木で)白色の頂点を直接指す黒色の頂点のハズです。

このへん

      auto p = white[x] ? v_w[x] : v_b[x];
      lct.expose(p);
      auto q = lct.get_kth(p, p->sz - 2);
      auto r = lct.get_root(p);
      lct.cut(q);

この頂点を探して(log nとかで探せる)cutすると求めたい連結成分のみからなるSplay木を得ることができます。

実装例

QTREE7

SPOJ.com - Problem QTREE7

問題概要

各頂点は白または黒の状態をとれ, 最初は全部黒である. また, 頂点には重みが割り当てられている. 以下のクエリを処理せよ.

  • 頂点  {u} が所属する連結成分の頂点のうち, 最大重みを求める.  {u} {v} のパス上のすべての頂点がすべて同じ色なら, 同じ連結成分に所属する.
  • 頂点  {u} の色を反転
  • 頂点  {u} の重みを  {w} に変更

解法

QTREE6とほとんど同じ

実装例

Dynamic Distance Sum

No.772 Dynamic Distance Sum - yukicoder

解法

値が1の頂点までの距離の総和は, 1の頂点数と距離を持っておけば比較的容易にできる.

Light-edgeを考慮しない場合, 適当な頂点でexpose()してsplay木上を探索するとなんかできる. 具体的には, 1の頂点数が過半数を超える子に潜る操作をシミュレーションすればよい. まず子(右部分木)をみて, 自分を見て, それでもダメなら親(左部分木)に移動すれば良い. 自分および親に移動するときには, 子の部分木の1の頂点数を足し込みたい気持ちになるので, 再帰の引数でどれだけたされたかを持っておく必要がある.

2024/5/5 追記:以下の実装では冗長なことをしているので つよい LCT - ei1333の日記 を参照してください

実はLight-edgeにも潜ることができる. multisetでLight-edgeでつながるそれぞれのSplay全体の1の頂点数を管理しておき, 可能性があるのは頂点数が最大のものなのでこれはすぐ求まる. Light-edgeがSplay木のどこに繋がっているかについてはSplay木の根へのポインタを持っていれば良いが注意が必要. 下のSplay木からsplay()したときにその木の根がこわれるので破滅する. これはLight-edgeに番号を振っておくことで対処できて, Splay木の根が変わったときにはLight-edgeの番号を参照して更新してあげれば良い.

controlをLight-edgeの番号と根を結びつける配列として, splayしてtを根にする前に以下の処理を挟む.

    Node *rot = t;
    while(!rot->is_root()) rot = rot->p;
    t->belong = rot->belong;
    if(~t->belong) control[t->belong] = t;

新しくLight-edgeを生やす操作は以下のような感じ. curとcur->rをLight-edgeで繋ぐ.

cur->r->belong = (int) control.size();
control.emplace_back(cur->r);
cur->sum.add(cur->r->sum, cur->r->belong);      

さいごに

いやこのせつめいみてもわかんなくない?(本末転倒)

たぴちゃんたべてやる