Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

プログラミングコンテストでのデータ構造 2 ~動的木編~

36,340 views

Published on

前編 (平衡二分探索木編) はこちら http://www.slideshare.net/iwiwi/2-12188757

Published in: Technology
  • Be the first to comment

プログラミングコンテストでのデータ構造 2 ~動的木編~

  1. 1. 2012/3/20 NTTデータ駒場研修所 (情報オリンピック春合宿) プログラミングコンテストでの データ構造2 ~動的木編~ 東京大学情報理工学系研究科 秋葉 拓哉 1
  2. 2. 動的木• 実は動的木にもいくつかある – 「順位キュー」的な言葉 (⇔ 二分ヒープ, フィボナッチヒープ)• ポピュラーな動的木 – Link-Cut 木 – Euler-Tour 木 (動的グラフで内部的に使う)• 今日は Link-Cut 木 の話をします 2
  3. 3. Link-Cut 木• 木を処理する神がかり的なデータ構造!! (木で木を処理するのでややこしい…)• 以下が 𝑂(log 𝑛) 時間でできる. – 頂点の親を変更 (link) / 削除 (cut) – 木の根を求める (root) / 木の根を変更 (evert) – パスに対する頂点・枝の値のクエリ (sum・max・更新など) 3
  4. 4. Link-Cut 木は現実 そんなのどうせ実装できっこない… (´・_・`) IOI は Link-Cut 木で常勝!! Fan Haoqiang 氏 ※発言はフィクションです (中国)Fan Haoqiang 氏は IOI’11 の Elephants で Link-Cut 木を実装!• これに関して特別賞を受賞• 599 点で準優勝 4
  5. 5. …でもやっぱその前に!• 動的木は本当に必要?いつものじゃダメ?• やはり,実装が面倒,避けられたら避けたい• 実際のとこ,本当に必要になる問題は皆無(特別賞を狙いたいのであればこの限りではない) 5
  6. 6. 例 木上の距離クエリ • 2 頂点間の枝を作成・削除してください • 2 頂点間の距離を答えてください ただしグラフは常に森. ツリーを処理するデータ構造なんて他に知らない… 動的木じゃないとできっこないよ…(´・_・`) 平方分割でもできるよ (重要!!)( ・`д・´) 6
  7. 7. クエリを平方分割クエリ • クエリを 𝐵 個ごとのブロックに分割 終わった • 各ブロック内で操作に関わる頂点は 部分 高々2𝐵個 • それ以外の頂点は興味が無いので, 今やる 部分 ブロックを処理する最初に,縮約 – 𝑂(𝐵) 頂点の森になる 7
  8. 8. クエリを平方分割 クエリ 縮約 • すると各クエリは 𝑂(𝐵) で処理可 ← 𝑂(𝐵) 𝑂(𝑁) – 普通に 𝑂(𝐵) 頂点の木を探索するだけ … 縮約 ← 𝑄 𝑂(𝑁) 𝑂(𝐵) 𝑂 𝑁 + 𝑄𝐵 = 𝑂 𝑄 𝑁 時間 𝐵 … ↑ 縮約 𝐵= 𝑁 とした ← 𝑂(𝐵) 𝑂(𝑁) … ただし,クエリが先読みできないと無理類似した問題と,より詳しい解説:http://acm-icpc.aitea.net/index.php?plugin=attach&refer=2010%2FPractice%2F%B2%C6%B9%E7%BD%C9%2F%B9%D6%C9%BE&openfile=2d.pdf 8
  9. 9. レベル別 Link-Cut 木オプション link, cut, evert, 辺質問・更新 頂点質問 頂点更新 (実装そこそこ) (実装一瞬) (実装少し)ベース expose (実装そこそこ)• オプションは独立に選んで実装可• 「頂点質問」 = ある頂点から根までのパスにおける頂点の属 性の sum とか max とかのこと• 「頂点更新」 =とはパス上の頂点全部に x 足すとかのこと 9
  10. 10. 基本アイディア• ツリーをパスの集合みたいに表現• パスを平衡二分探索木で管理 10
  11. 11. 基本アイディアパスへの分解は決まってない + 固定じゃない こうなってるかもしれないし こうなってるかも 11
  12. 12. 核となる操作 expose(𝒗) 頂点 v から根へのパスを繋げる ←切れるv v 切 れ ↑ る 平衡二分探索木の split / merge を使う 12
  13. 13. link, cut の雰囲気• cut(𝑣): 𝑣 から親への辺を削除• link(𝑣, 𝑤): 𝑣 の親を 𝑤 にする 平衡二分探索木を切ったり繋げたりするだけ w w cut v v link 13
  14. 14. 頂点クエリ• sumv(𝑣): 頂点 v から根までの頂点たちに書いて ある数の和 (あるいは minv, maxv, …)やり方• 平衡二分探索木に,部分木の和を持たせる• expose(𝑣) して,和を見るだけ 14
  15. 15. 各操作の計算量結論: スプレー木を用いるとならし 𝑂 log 𝑛 時間𝑂 log 2 𝑛 時間の略証:• expose の計算量だけ考えれば良い (他は余裕)• 平衡二分探索木で管理されるパスに入る・出る枝の本数 をならし 𝑂 log 𝑛 本に抑えられれば良い 1 本 “入って” v v 2 本 “出た” 15
  16. 16. • 出るためには入る必要がある,入る回数を抑えれば OK• 元の木の Heavy-Light Decomposition を考える• Light-Edge の本数を抑える – 各頂点から根までの Light Edge はそもそも 𝑂 log 𝑛 本 – よって,入る Light-Edgeは 𝑂 log 𝑛 本• Heavy-Edge の本数を抑える – 1 度にいじる Heavy-Edge の本数は多いかも,でも, – Heavy-Edge が入るということは Light-Edge が出る – Light-Edge が出るためには Light-Edge が入ってるはず – それはさっき数えた! 各クエリ 𝑂 log 𝑛 本! – よってならし 𝑂 log 𝑛 本になる• よって 𝑂 log 𝑛 ならし本.よって 𝑂 log 2 𝑛 ならし時間. 16
  17. 17. 各操作の計算量𝑂 log 𝑛 時間:• スプレー木のポテンシャルに踏み入って解析する• 今日は省略 Link-Cut 木を実装していこう! (気合い!) (といってもスプレー木が出来れば殆ど終わり) 17
  18. 18. 実装:ノードの構造体struct node_t { node_t *pp, *lp, *rp; // 親,左の子,右の子// このノードは木の根?bool is_root() { return !pp || (pp->lp != this && pp->rp != this);}…Is_root は何故 !pp だけじゃない?→ pp をパスの親を表すのにも使うため (後述) (こうすると実装が凄い楽になります)親でありながら子でないことがある 18
  19. 19. 実装:ノードの構造体 (続き)void rotr() { // 右回転 void splay() { // スプレー操作 node_t *q = pp, *r = q->pp; while (!is_root()) { if ((q->lp = rp)) rp->pp = q; node_t *q = pp; rp = q; q->pp = this; if (q->is_root()) { if ((pp = r)) { if (q->lp == this) rotr(); if (r->lp == q) r->lp = this; else rotl(); if (r->rp == q) r->rp = this; } else { } node_t *r = q->pp;} if (r->lp == q) { if (q->lp == this) { q->rotr(); rotr(); }void rotl() { // 左回転 else { rotl(); rotr(); } node_t *q = pp, *r = q->pp; } else { if ((q->rp = lp)) lp->pp = q; if (q->rp == this) { q->rotl(); rotl(); } lp = q; q->pp = this; else { rotr(); rotl(); } if ((pp = r)) { } if (r->lp == q) r->lp = this; } if (r->rp == q) r->rp = this; } } }} スプレー操作を Bottom-Up の方式で実装する (普通に平衡二分探索木でスプレー木を使うときは Top-Down の方式の方が良いが, 今回はノードのポインタを知ってるところから splay したいので Bottom-Up) (参考:Top-Down の実装例 http://www.prefield.com/algorithm/container/splay_tree.html) 19
  20. 20. 実装:パスの親 NULL 3 1 2 5 2 表現の例 1 4 a 3 b 4 a 5 c c b• pp (親へのポインタ) を 2 通りの使い方をすると楽 – 普通に二分探索木内での親へのポインタ(緑・青の両向き) – パスからの親へのポインタ (赤色の片方向) 20
  21. 21. expose(c) 1 1 2 2 3 a 3ab 4 b 4c 5 5 c NULL NULL 2 21 3 1 3 5 5 4 4 a a c c b b ノード 2 の右の子をこうなってたら 3 から a にするだけ! 21
  22. 22. 実装:exposeやること: node_t *expose(node_t *x) { node_t *rp = NULL;1. 今いる頂点を splay for (node_t *p = x; p; p = p->pp) { p->splay();2. 右側にさっきまで居た木を p->rp = rp; rp = p; くっつける } x->splay(); // しとくと便利3. 上の木に進む return x; } 3 3 2 2 3 2 5 2 5 1 1 c 51 4 1 4 4 a b a c c c a a 3 5 b b b 4 さいしょ Splay(c) 2 に移動,splay(2) 2 の右に c をつける 22
  23. 23. 実装:link, cutexpose まで出来ていればもう簡単void cut(node_t *c) { expose(c); cut p node_t *p = c->lp; • c を expose c c->lp = NULL; p->pp = NULL; • c の左を切断} link cutvoid link(node_t *c, node_t *p) { expose(c); link expose(p); • c, p を expose p c->pp = p; • p の右に c を c p->rp = c; つける} 23
  24. 24. 実装:evertevert(𝑣):𝑣 を根にする木のパスの向きを反転すればよい v void evert(node_t *p) { node_t *r = expose(p); r->rev = true; v } + スプレー木に 反転の機能を実装 24
  25. 25. 実装:辺属性• 木の辺に情報をつける• 回転とか張替えでポインタと一緒に情報を保つ (注意深く)• 部分木に関する情報も保つ NULL 2 3 3 1 1 1 2 4 5 2 2 5 1 5 4 a 3 3 6 a b 4 4 6 7 5 c c b 7 25
  26. 26. 応用:Elephants (IOI’11)• 象が 𝑁 匹並んでます• クエリが大量にくる – Update(𝑖, 𝑥) – i 匹目の象を場所 x に移動• クエリの度に答える – 何台のカメラで全員写る? – カメラ:幅 𝐿 26
  27. 27. 応用:Elephants (IOI’11)• 愚直な解法:クエリ毎に計算する – 貪欲法の典型的問題 – 一番左の象を覆う,を繰り返せば良い• 動的木を使う解法: – 似たような感じのことを木で表現する – 移動は木のちょっとした更新になる – よってクエリに爆速で答えられる 27
  28. 28. 応用:Elephants (IOI’11)• 象の場所に ● を書く• そこから L 先に ○ を書き,●から辺を張る• ○からはすぐ次の●か○に辺を張る• これはツリー!• 一番左から辿って,●の個数が答え! 28
  29. 29. Link-Cut 木まとめ• まずはもっと容易な道具を検討! – クエリの平方分割• 実装しよう (下に行くほど大変) – expose, link, cut, root, 頂点の情報に関する質問 – evert, 頂点の情報の更新 – 辺の情報に関する質問・更新 29
  30. 30. 全体まとめと私見話したこと• 平衡二分探索木 – 本当に必要か検討,必要な場所だけ作るセグメント木 – Treapのアイディア,楽な実装法の議論,応用例 – その他の平衡二分探索木の紹介• 動的木 – 本当に必要か検討,クエリの平方分割 – Link-Cut Tree のアイディア,楽な実装法の議論,応用例私見• これらは難易度が高く,想定解法としての出題頻度は低い• よって,習得の優先度は高くない• ただし,非常に強力な道具なので,武器にできれば得をするかも? 30

×