- できるだけ毎週土曜日更新.次回更新は2月16日.
- ネタ切れ深刻なので記事リクエスト募集中.twitterないしはコメント欄まで.
- 貼っているコードはあんまり検証していないので,信頼性はアレです.アレ.
- twitter: @tmaehara フォローはお気軽に.
2012-09-10
値の差分表現
昨日のは,まるっきり既出でした(iwiさんに教えてもらいました.ありがとうございます): http://poj.org/problem?id=3580
一般的なので,同じ構造を使う問題があるとは思っていましたが,まさかそのままデータ構造を問う形での出題があるとは思っていませんでした.解説もあるよ:http://poj.org/showcontest?contest_id=1290
しかも,この問題に対する主流の解法が,自分の予定していた解法よりも実装容易だったので,来週まで待たずに解説を張ってしまいます.これから解答を作る予定だった方は,すみません.来週はストックを前倒して別のものを書きます.
想定解法
「値の差分表現」とよばれる技法を使うのが想定でした.これはリンク・カット木の実装の際によく使われるもので,たとえば以下の資料にかいてあります.
- http://planarity.org/Klein_splay_trees_and_link-cut_trees.pdf
- http://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-854j-advanced-algorithms-fall-2008/lecture-notes/lec8.pdf
平衡二分探索木(挿入・削除がない場合は,区分木でOK)を使います.ノードにもたせる値を
struct Node { Node *ch[2]; int δcost; int δmin; };
のようにします.ここでδcost が値の差分表現と呼ばれるもので,本来のノードのもつ値が根までのδcostの経路和になるように定めます: cost(u) = Σ[v=n~root] δcost(v) .区間への値の足しこみは,δcostに値を加えれば,そこを経路としてもつ点すべてに値をくわえたことになります.cost(u) の取得は,経路を手繰りながら値を足していきます.
δmin は,そのノード以下の最小値 min(u) に対して min(u) = δmin(u) - cost(u) となるように定めます.min(u) = min { cost(u), min(left), min(right) } の関係式に δmin の定義を入れると δminの更新式が得られます:δmin(u) = min { 0, δmin(left)+δcost(u), δmin(right)+δcost(u) (符号は定義の仕方でいろいろ),あとは,これにしたがって更新していくだけです.
平衡二分探索木の回転が問題で,δcost の更新に工夫が要ります.回転時に関わるノードについて,回転前後で cost(u) 不変の条件から δcost の更新式が得られるので,それにしたがって更新します.
別解法
冒頭で述べた問題に対する講評を見ると,こちらの解法が主流のようで,実際,実装も容易なので紹介します.一言でいえば,addクエリを遅延評価します.
add(u, x) = { cost(u) += x; add(left,x); add(right,x); }
として定義できますが,これを(普通の言語で)そのまま書くと u 以下のノード数に比例した時間がかかってしまいます.しかし,実際に cost の値が必要になるまで add の伝播を遅延させれば計算量の問題がなくなります.
実際の実装では
struct Node { Node *ch[2]; int cost; int min; int δcost; };
のように,通常の値 cost, min を保持しつつ,さらに遅延評価用に差分の値 δcost を保持します.そして必要な場合だけ δcost を反映させていきます.
こちらの実装では,回転操作で悩むことがありません.関係ありそうなところのδcostをすべて評価してしまえば普通の回転と同じです.この点で差分表現を用いる解法よりも若干実装容易といえます.
- 71 https://topcoder-g-hatena-ne-jp.jag-icpc.org/
- 5 http://hootsuite.com/dashboard
- 5 http://t.co/PmgRGHMA
- 2 http://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0CCUQFjAA&url=https://topcoder-g-hatena-ne-jp.jag-icpc.org/spaghetti_source/&ei=j_1NUNfnA_CVmQWHrYGQCg&usg=AFQjCNGi75k7NqE3ZV0oifUPLMsOv7aLiA
- 2 http://longurl.org
- 2 http://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0CCUQFjAA&url=https://topcoder-g-hatena-ne-jp.jag-icpc.org/spaghetti_source/&ei=K_NSUJ74JejJmAWU-oGIDA&usg=AFQjCNGi75k7NqE3ZV0oifUPLMsOv7aLiA&sig2=uQhAh8SpEE36_nuAvNo65g
- 1 http://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0CCUQFjAA&url=https://topcoder-g-hatena-ne-jp.jag-icpc.org/spaghetti_source/&ei=rKFNUJb3ELHzmAX1ooHwBQ&usg=AFQjCNGi75k7NqE3ZV0oifUPLMsOv7aLiA&sig2=CAu0cLhU32azKoJoAYt79g
- 1 http://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0CCUQFjAA&url=https://topcoder-g-hatena-ne-jp.jag-icpc.org/spaghetti_source/&ei=tQJOUJm-C4aSiAeX9YGoCQ&usg=AFQjCNGi75k7NqE3ZV0oifUPLMsOv7aLiA
- 1 http://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0CCUQFjAA&url=https://topcoder-g-hatena-ne-jp.jag-icpc.org/spaghetti_source/&ei=iANOUIbBB8ujiAfl5YHIBg&usg=AFQjCNGi75k7NqE3ZV0oifUPLMsOv7aLiA
- 1 http://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=5&ved=0CEoQFjAE&url=https://topcoder-g-hatena-ne-jp.jag-icpc.org/spaghetti_source/20120908/1347059626&ei=rE9OUKebHYXKhAf87oHYCQ&usg=AFQjCNEVRXCVuox55x2gV4mSi6Dij7ZdWQ&sig2=AVHexbX7bT8WI_Uxq8_UFA
iwiwi 2012/09/11 00:00 区分木では,値の差分表現の方が僕は好きでそっちをよく使います.平衡二分探索木だと,仰る通り回転で訳が分からなくなるので遅延評価を使います.余談ですが,そういったクエリを抜きにしても回転は頭がこんがらがるので,回転抜きで平衡二分探索木が作れると友人から聞いて以降それをよく使っています.(ここで触れています http://www.slideshare.net/iwiwi/2-12188757)スケープゴート木も回転が要りませんが,あれは split/merge が出来ないのでコンテストでは使用場所が制限されてしまいます.
spaghetti_source 2012/09/11 10:22 merge-splitベースのtreapですね.あれは素晴らしい.自分は上からのアクセスで済む場合はmerge-split,下からアクセスする必要があれば回転ベースでやっています.scapegoatはそれらと比べてたいして実装が軽くないし,そもそも平衡できる程度の木に使うのは負けた気がして嫌です.笑