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