AtCoder Regular Contest 030 解説
Upcoming SlideShare
Loading in...5
×

Like this? Share it with your network

Share

AtCoder Regular Contest 030 解説

  • 316 views
Uploaded on

AtCoder Regular Contest 030 解説

AtCoder Regular Contest 030 解説

More in: Education
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
No Downloads

Views

Total Views
316
On Slideshare
267
From Embeds
49
Number of Embeds
1

Actions

Shares
Downloads
2
Comments
0
Likes
1

Embeds 49

https://twitter.com 46

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. ARC030 解説 解説スライド担当: 三上 和馬 (@kyuridenamida)
  • 2. 問題A – 閉路グラフ
  • 3. 問題概要 • n = 6 k = 2 の例 答えは「YES」
  • 4. 解法&考察 • 取り出せる最大の連結成分の数を求めたい • とりあえず頂点を1つ取り除いてみると直線グラフになる 1 2 3 4 5 ● この状態でサイズ1の連結成分を作りまくるのが最適そう したがって端から偶数番目の頂点を取り除いていけばよい ●奇数番目の頂点の数=((n-1)/2を切り上げたもの)が作れる連結成分の最 大数であり,kがこれ以下であればYES,そうでなければNOを出力 •
  • 5. 備考 • 閉路グラフはよく列を扱う問題の派生として出題されます. • 「頂点を1つ○○したら列の問題に帰着できる」等の発想は,方針を 考える上で手助けになるかと思われます.
  • 6. 問題B – ツリーグラフ
  • 7. 問題概要 • x=1のケース
  • 8. 考察&解法 ●与えられたグラフから出発地点を根とした根付き木を構築する ● ある辺をたどるべきかどうかだが,「辺を辿った先の部分木の頂点が 1つでも宝石を含んでいるときに限り通り,それ以外のとき通らなくて 良い」ということが分かる.部分木が宝石を含んでいるかを判定する 関数を作っておく ● ある辺を使うときは行きと帰りで必ず2回通る ● なので,判定関数で,行く必要のない頂点に遷移しないような再帰関 数を書き,辺をたどる毎に答えに2を加算するプログラムを書けば良 い ●任意のケースで同じ辺を3回以上辿る必要はなく,上記の戦略が最 適.
  • 9. 問題C – 有向グラフ
  • 10. 問題概要 1 a 2 b 4 a 3 b ①a回収 ②移動 ④a回収 ⑤移動 ③移動 ⑥b回収 頂点4からスタート k=3のケース aabを出力
  • 11. 強連結成分分解 ●有向グラフにおいて,相互に行き来できる頂点同士の集合 (強連結成分)を検出し,グループ分けするアルゴリズム ●適当な未訪問の頂点を始点とし,深さ優先探索を行い,帰り がけ順で頂点を列挙することを,未訪問頂点がなくなるまで 繰り返す(トポロジカルソート). ●次に,元のグラフの逆辺グラフを考え,上記で得られた頂点 の順番に,未訪問の頂点を始点とし再度深さ優先探索を繰 り返し行う.このとき,この一度の深さ優先探索でたどり着け る頂点集合が1つの強連結成分となっている
  • 12. 考察&解法 • どこの頂点のアルファベットを回収したかを覚えて探索は無理. • なので,与えられたグラフに対して強連結成分分解を行う • 同じ強連結成分に属する頂点の文字は,自由な順番で取ることがで きる→ある強連結成分にある文字をhoge個使うことを考えたとき,強 連結成分に含まれる文字群をソートした文字列の先頭hoge個を使え ば良いということが分かる • こういった遷移を考えればDAGになり,回収した頂点を覚えなくて良 いDPに帰着できる • DPは「どの強連結成分か」「今何文字か」をキーに辞書順最小の文 字列を格納するものを行う • 強連結成分分解の計算量はO(N+M),文字列比較に最悪O(K)かか るので,動的計画法の計算量はO((NK+M)K)
  • 13. 問題D – グラフではない
  • 14. 問題概要 • 長さNの数列Xが与えられ,その後Q個のクエリが飛んでくる.種類は • 1 a b v ― Xの区間 [a,b] に一様に値 v を加える. • 2 a b c d ― Xの区間 [a,b] を,クエリが呼ばれた時点での区間 [c,d] の値に書き換える(b−a=d−c). • 3 a b ― Xの区間 [a,b] の総和を出力. • 1 ≦ N ≦ 200,000 • 1 ≦ Q ≦ 200,000
  • 15. 方針 今回のクエリはタイプ2が曲者なので,そのため 区間に対する操作は平衡二分木等を使うと万能なのでそれを使う.遅 延評価が使えると良い.   segtreeは使えるか…? → 今回はタイプ2があり,使いづらい. ● タイプ2のクエリ([c,d]→[a,b])がやっかい このクエリは,区間[c,d]のコピーを区間[a,b]に上書きするクエリ 平衡二分木のある区間のコピーを作成するには,永続データ構造が 必要そう. 遅延評価機能付き永続型平衡二分木をつくろう.
  • 16. 永続化のための平衡二分木選び • ✖ treap (同じ乱数値を持つノードがたくさん発生するため木が偏る) • ○ 赤黒木 (いける...けど実装重い) • ◎ RandomizedBinarySearchTree (マージ/スプリットするときに乱数を 用いるのでtreapみたいなことは起きない)  ↑計算量解析は難しいらしいが実際偏らず各操作O(log n).実装も 比較的軽くおすすめ!今回はこれを実装する.
  • 17. まずは普通の平衡二分木 • 今回はキーに順序関係が全くないので,「平衡二分探索木」ではなく 「平衡二分木」と呼ぶが,対数計算量実現のためにやってる操作は 一緒. • 区間の問題に対処するためには, – ある位置の頂点を追加する/削除するというinsert/eraseベース の実装 • よりも – ある位置で木を2つに分割する/2つの木を順番を保って併合す るsplit/mergeベースの実装 のほうが都合が良いのでそれを実装していく
  • 18. 各ノードが持つべき情報 ●左の子へのポインタ/右の子へのポインタ ● そのノードの持つ値 ● そのノードを根とした部分木のすべてのノードの値の総和 ● そのノードを根とした部分木のすべてのノードの数 ● そのノードが持つ評価遅延中の値 が持つべき情報 struct T{ T *l,*r; long long v; long long sum; long long lazy; int c; };
  • 19. 作っておくと便利な関数 ●子の情報に基づきノードの情報を更新し,自身のポインタを返す関数 T *update(T *c){ if( c == NULL ) return c; if( c->lazy ){ // 評価遅延しているものを評価し子に伝搬する c->v += c->lazy; if(c->l){ c->l->lazy += c->lazy; c->l->sum += count(c->l) * c->lazy; } if(c->r){ c->r->lazy += c->lazy; c->r->sum += count(c->r) * c->lazy; } c->lazy = 0; } c->c = count(c->l) + count(c->r) + 1; c->sum = sum(c->l) + sum(c->r) + c->v; return c; } int count(T *c){ if( !c ) return 0; else return c->c; } long long sum(T *c){ if( !c ) return 0; else return c->sum; }
  • 20. マージ関数の実装 ● T *merge(T *a,T *b) := aの右の子にbを結合するか,bの左の子にa を結合するかを乱数で決める関数 「aの部分木サイズ>bの部分木のサイズ」のときはだいたいの確率 でaの右の子にbをくっつける」などを繰り返すイメージ ● つなげたあとは,つなげたときに取り除いた子を考慮して再帰 T *merge(T *a,T *b){ if(a == NULL) return b; if(b == NULL ) return a; if( rand() % ( count(a) + count(b) ) < count(a) ){ a = update(a); // たどったノードはとりあえず更新しておく a->r = merge(a->r,b); return update(a); }else{ b = update(b); // たどったノードはとりあえず更新しておく b->l = merge(a,b->l); return update(b); } }
  • 21. スプリット関数の実装 ● pair<T*,T*> split(T *c,int k) := cの部分木を[0,k) [k,n)に分割したもの のそれぞれの根をpairで返す pair<T*,T*> split(T *c,int k){ if(!c) return make_pair(c,c); c = update(c); //たどったノードはとりあえず更新しておく if(k <= count(c->l)){ pair<T*,T*> s = split(c->l,k); c->l = s.second; return make_pair(s.first,update(c)); }else{ pair<T*,T*> s = split(c->r,k - count(c->l) - 1); c->r = s.first; return make_pair(update(c),s.second); } }
  • 22. これらの関数を永続化に対応させるには ● 基本的に全てのノードがconstなものであると考え,子を書き換えた り,遅延情報を伝搬したり,ありとあらゆる変更する際はコピーノー ドを作成し,それを書き換えるという発想に基づく.書き換わってい ないノードは既存のものを使うようにする. ● ここで自身を返す実装が初めて生きてくる ● すると,一度の操作で増えるノードの数はたかだかO(log N)個
  • 23. これらの関数をこの問題に適用していく ● 基本的に,区間に対する操作は, – 区間だけを切り取った木をsplitで作り,根を操作し,そのあとそれをmergeする で対処する. ● たとえば区間[a,b)に対するタイプ1(一様加算)クエリは今の列の[a, b)を取り出して根の遅延評価情報にvを加算し,くっつける ● タイプ2(コピー)クエリに関しては,[0,a) [b,n) そして[c,d) をコピーし た木を作り,[0,a) [c,d) [b,n)の順番でマージする(実際の問題は閉 区間で与えられるので注意) ● タイプ3のクエリは,同様に区間を切り出し根のsumを出力するだけ
  • 24. メモリが溢れることに対する対処 ● 実際は,一回クエリを処理するたびに,ノードが結構生成される ● 適当にやっているとMLEしかねないが,メモリーがやばくなったら一 回列の情報を全てどこかに出力し,永続情報を破棄し再構築する という方法で簡単にごまかすことができる.
  • 25. 計算量 ● 時間計算量 – 一回の操作でO(log N) なので O( (Q+N) log N) – ただし定数倍は重い ● 空間計算量 – O( (N+Q) log N)個のノードがクエリ処理の過程で生成される – 遅延評価するときに,たどったノードに隣接している子ノードの分もコピーするので,こちら も定数倍は重い.前スライドのような方法で対処.