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.

Heapとdisjoint setについて

57 views

Published on

https://webhack.connpass.com/event/54700/

Published in: Engineering
  • Be the first to comment

  • Be the first to like this

Heapとdisjoint setについて

  1. 1. HeapとDisjoint Setについて uwi 1 WebHack Meetup #1
  2. 2. Self Introduction uwi (有為) エンジニア language : Java, Python twitter : @uwitenpen interest : 競プロ・強化学習・IoT Facebook Hacker Cup 2016 Finalist CodeChef Snackdown 2016 Finalist Codeforces: International Grandmaster TopCoder Algorithm: ● 2
  3. 3. Agenda ◎Heap ◎Disjoint Set 3
  4. 4. Heapとは 「子要素は親要素より常に大きいか等しい (または常に小さいか等しい)」 という制約を持つ木構造 (wikipedia) 4
  5. 5. Heapとは Priority Queue(優先度つきキュー)の機能を実装するた めに使われる。 ・値を挿入する。 ・最小値を返す。 ・最小値を削除する。 だいたいの言語には標準的に実装されている。 5
  6. 6. Binary Heap 6 2 5 4 5 10 6 2分木になっていて、 ルートが最小値。 層は端から埋まる。 一層が埋まるまで 次の層には行かない。
  7. 7. Binary Heap - 挿入 7 2 5 4 5 10 6 3
  8. 8. Binary Heap - 挿入 8 2 5 3 5 10 6 4 親より小さければ親と位 置を入れ替える。 できなくなるまで繰り返 す。
  9. 9. Binary Heap - 削除 9 2 5 3 5 10 6 4
  10. 10. Binary Heap - 削除 10 4 5 5 10 6 3 最後の項を暫定的なルー トとして持ってくる。
  11. 11. Binary Heap - 削除 11 自分と2個の子を比較し、 最小のものと交換する。 これを繰り返す。 3 5 5 10 6 4
  12. 12. Binary Heap - 削除 12 3 5 5 10 6 4
  13. 13. 実際の実装 13 3 5 4 5 10 6a 配列として持つ。先頭がルートで、深さごとに順に並べる。
  14. 14. 実際の実装 14 初期化 class SimpleMinHeap: INFTY = 9999999999999999999 def __init__(self, n): self.a = [self.INFTY] * (n+1) self.pos = 1 ∞が詰まった配列を用意する。posは次に値を追加するときの位置を表す。 ここを1にするか0にするかは好み:)
  15. 15. 実際の実装 15 サイズ・最小値取得 def min(self): return self.a[1] def size(self): return self.pos - 1 最小値はルート(1)の値、サイズはpos-1.
  16. 16. 実際の実装 16 値の追加 def add(self, x): self.a[self.pos] = x c = self.pos p = c>>1 while p >= 1 and self.a[c] < self.a[p]: self.a[c], self.a[p] = self.a[p], self.a[c] c >>= 1 p >>= 1 self.pos += 1 現在をc, 親をpとする。 親より小さければ親と位置を入れ替える。できなくなるまで繰り返す。
  17. 17. 実際の実装 17 最小値の削除 def poll(self): if self.pos == 1: return self.INFTY self.pos -= 1 ret = self.a[1] self.a[1] = self.a[self.pos] c = 1 while 2*c < self.pos: smaller = 2*c if self.a[2*c] < self.a[2*c+1] else 2*c+1 if self.a[smaller] < self.a[c]: self.a[c], self.a[smaller] = self.a[smaller], self.a[c] c = smaller else: break return ret
  18. 18. 計算量 18 O(n+mlog n) m : 操作回数, n : 要素数 1回の操作につき最大木の高さ分だけのswapが行われる。
  19. 19. Heapの種類 19 - Binary Heap - Fibonacci Heap (計算量的には最速) minもmaxも取り出せる - Interval Heap - MinMax Heap - (Hogloid Heap) Heap同士のマージを高速に行える(Meldable Heap) - Binomial Heap - Pairing Heap - Skew Heap (実装が一番軽い) - Leftist Heap (Skew Heapにbalancingをかけている) 最小値以上のものしか挿入できない - Radix Heap (Dijkstra用)
  20. 20. 使いみち 20 Priority Queueとして - Dijkstra法 - 給油問題 (Gas Station Problem) - 区間スケジューリング - 区間と点のマッチング 等々、貪欲法でよく使われる。 機能的な上位互換(平衡二分探索木)があるので、これがないと解けな いみたいなのは少ない。 が、Heapは制約が緩い分非常に高速に動く。
  21. 21. 使いみち(給油問題) 直線上に街が2個と、その間に、リッターあたりの値段が異なるガソリンス タンドが並んでいる。車で一方の街から他方の街に移動するとき、消費する 値段の最小値を求めよ。 1. Heap(PriorityQueue)を用意。 2. 車を、残り燃料が続く限り走らせる。途中通り過ぎたガソリンスタンド のリッターあたりの値段をHeapに入れる。 3. 車の燃料が尽きたら、Heapから最小値を取り出し、そのガソリンスタ ンドまで戻って満タン(or ゴールの街に着けるぎりぎり)まで補給したこ とにする。 4. 2,3をゴールの街に着くまで行う。 21
  22. 22. 使いみち(区間スケジューリング) m個の時間割が与えられ、n人でこなす。ただし1人は同時に複数の時間割を こなすことはできない。このとき、全体でこなせる時間割の個数を最大化せ よ。 1. MinMaxHeapを用意。これには時間割の終了時刻が入る予定。 2. 時間割を、開始時刻昇順にソートする。 3. 時間割を順番に見ていく。 a. 現在の時間割の開始時刻以前に終わっている時間割をMinMaxHeap から除く。これは最小値を見て取り出せば良い。 b. 現在の時間割の終了時刻をMinMaxHeapに挿入する。このとき、 MinMaxHeapのサイズがnをこえたら、終了時刻が一番長いものを 除く。これは最大値を見れば良い。 4. m-(3bで取り除いた個数)が答え。 22
  23. 23. より実用的な実装 23 - ラベルを付与 値と一緒にラベルも持たせておくと、ラベルを指定して削除・更新が できる。ラベル↔配列内の現在位置のマップを持っておく。 - 更新 値を書き換えた後、挿入時のように木をのぼり、その後削除時のよう に木を下る。削除+挿入より高速化できる。 - 永続化 (任意の時点のHeapにアクセスできる) Meldable Heapで1回の操作がO(log n)でできるものは永続化できる。 Leftist Heapの場合は1行追加するだけでできる。
  24. 24. agenda ◎Heap ◎Disjoint Set 24
  25. 25. Disjoint Setとは disjoint-set forest 互いに素な集合の森 ( ∈ disjoint-set data structure ) 別名"Union-Find"と呼ばれる。 25
  26. 26. Disjoint Setとは 26 a d c b e 集合A 集合B
  27. 27. Disjoint Setとは 27 a d c b e 集合A 集合B 代表元
  28. 28. Disjoint Setとは 28 a dc b e 集合A 集合B 森として表現 ルート ノード
  29. 29. 実際の実装 29 a b c d e 0 1 2 3 4 2 3 2 3 1 0 1 1 2 0 parent rank a dc b e parentは親。ルートの場合は自分自身 を表す。初期値は自分自身。 rankは高さの上界。初期値は0。
  30. 30. 実際の実装 30 初期化 class DisjointSet: def __init__(self, n): self.rank = [0] * n self.parent = list(range(n)) rankの初期値は0。parentの初期値は自分自身。
  31. 31. 実際の実装 31 ルート def root(self, x): if self.parent[x] != x: self.parent[x] = self.root(self.parent[x]) return self.parent[x] 自分自身が親なら自分自身を返す。 それ以外の場合は、親に丸投げして結果を親としてから返す(経路圧縮)。
  32. 32. 実際の実装 32 同じ木に属するか def equiv(self, x, y): return self.root(x) == self.root(y) ルートが等しいかどうかで判定。
  33. 33. 実際の実装 33 合併 def unite(self, x, y): x = self.root(x) y = self.root(y) if x == y: return True if self.rank[x] < self.rank[y]: x, y = y, x if self.rank[x] == self.rank[y]: self.rank[x] += 1 self.parent[y] = x return False x,yのrootを得た後、rankの大きい方に、rankの小さい方を子としてマージする。 rankが等しければrankを1増やす(ランクによる合併)。
  34. 34. 実際の実装 34 a b c d e 0 1 2 3 4 2 3 2 3 1→3 0 1 1 2 0 parent rank a dc b e root(4) = 3
  35. 35. 実際の実装 35 a b c d e 0 1 2 3 4 2 3 2→3 3 1 0 1 1 2 0 parent rank a d c b e unite(0, 1) = False
  36. 36. 計算量 O(n+mα(n)) m : 操作回数, n : 要素数 α : アッカーマン関数の逆数。実用上はα(n)≦4としてよい。 α(n)=5となるためにはnが1080以上必要 36
  37. 37. 使いみち グラフ・グリッド・論理状態の連結成分の管理 ◎ 最小全域木 ◎ 粗いサイクル検出 ◎ 連結成分の最小列 ◎ 簡易版2-SAT 等々 37
  38. 38. 使いみち(最小全域木) 辺重みつき無向グラフの部分グラフで、連結であり、かつ辺の重みの合 計を最小化するようなもの。 1. 辺を重み昇順にソート。 2. Disjoint Setを用意。 3. 辺を順に見ていき、両端が連結(equiv)でなければ uniteする。 38 a b c 1 2 1
  39. 39. 使いみち(粗いサイクル検出) 無向グラフがサイクルを持つかどうか判定。 1. Disjoint Setを用意。 2. 辺を順に見ていき、両端が連結(equiv)ならば サイクルを持つ。それ以外の場合uniteする。 39 a b c e d
  40. 40. 使いみち(連結成分の最小列) 右のグリッドの各'#'からなる連結成分について最小列番 号を求めよ。 1. Disjoint Setを用意。このとき同時にmincol配列を用 意しておき、uniteのときに mincol[x] = min(mincol[x], mincol[y]) とする処理を加える。 1. '#'同士で隣り合ったセルをuniteする。 2. '#'であり、かつ自身がルートであるセルについて、 mincolの値を出力。 40 012345 .##... .#..#. ##..#. ...### .###..
  41. 41. 使いみち(簡易版2-SAT) n人の正直者or嘘つきがいる。"人iがjを(正直者or嘘つき)と言った"という 情報がいくつか与えられるので、正直者の人数の最大値を求めよ。 1. "iが正直者"というノードと"iが嘘つき"というノードを用意する。 2. Disjoint Setを用意。 3. iが"jは正直者"と言っている場合、"iが正直者"ノードと"jが正直者"ノードをunite, "iが嘘つき"ノードと"jが嘘つき"ノードをuniteする。 1. iが"jは嘘つき"と言っている場合、"iが正直者"ノードと"jが嘘つき"ノードをunite, "iが嘘つき"ノードと"jが正直者"ノードをuniteする。 1. "iが正直者"というノードと"iが嘘つき"というノードが連結の場合矛盾。 2. 同じ人が属する連結成分は"正直者a人嘘つきb人"または"正直者b人嘘つきa人"の いずれかになるので、各連結成分についてmax(a,b)を合計する。 41
  42. 42. より実用的な実装 ・parentのルートのところを-(rank+1)とすることによりrank分のメモリ が要らなくなる。 ・実はrankではなく、連結成分のサイズの大小によって行うようにして も、同じ計算量が達成できる(上のテクニックも使える)。 42
  43. 43. 操作を巻き戻す Restorable Disjoint Set (勝手に命名) Disjoint Setの計算量O(n+mα(n))は"経路圧縮"と"ランクによる合併"のお かげでなっているが、これは"ならし計算量"であり、1回の操作にO(n)だ けかかる場合がある。 "経路圧縮"をしないDisjoint Setの計算量はO(n+mlog n)になるが、1回の 操作がO(1)で済む。これにより、記録する操作の個数もO(1)になるので、 uniteする直前のrank, parentの状態を記録しておき、あとでO(戻す操作 回数)で復元することができる。 43
  44. 44. 結合を引き剥がすことはできない? できる。 あらかじめすべての結合クエリが与えられている問題(オフライン)なら 後述する方法でできる。 次にどの結合クエリが来るかわからない問題(オンライン)であれば、 Euler Tour Treeを複数個重ねた構造によりできる(重い)。 44
  45. 45. Offline Dynamic Connectivity 無向辺を追加・無向辺を削除・2点が同じ連結成分に属するか 同じ辺について、時系列で生きている時間区間を得て、下記の図の長方形のところに 辺をまぶす。 その後矢印の通りにたどると、末端のところでは その時刻でのDisjoint Setができあがっている。 橙色は辺をRestorable Disjoint Setに追加、 青色は辺を削除(復元)する。 45
  46. 46. 永続化 操作途中のどの時点のDisjoint Setも再現できるデータ構造。 ・半永続Disjoint Set uniteは最新の時点にしかできない。 各ノードごとにrank,parentの変更履歴をリストで管理するだけで OK. ・全永続Disjoint Set どの時点からでもuniteできる。 経路圧縮をやめた上で、rank, parentを永続配列にする。 46
  47. 47. Thank you! 47

×