電波ビーチ

☆(ゝω・)v

重みつきUnion-Find木を解説

これを解こうとしてできなかったのでまとめた。

judge.u-aizu.ac.jp

重み付きUnion-Find

英語名はよくわかんなくてweighted union-find とか weighted union heuristic、weighted disjoint set dataみたいに言われてるっぽい。Union-Find木に「あるノードから別のノードへのコスト」を加えたもの。Union-Find木自体の説明についてはAtcoderのTypical ContestかAOJのDSLに基本問題と解説があるのでチェケラ

atc001.contest.atcoder.jp

judge.u-aizu.ac.jp

ググった

ありがとうございます。

qiita.com

at274.hatenablog.com

喜び勇んで読んだが、いまいちなにをやっているのかわからんので整理する。

経路圧縮

f:id:or3:20190110004529j:plain - 木の葉を直接根にくっつけちまえ、という発想。これにより再帰的に遡ると最悪O(n)だったのがO(logn)となる(計算の理屈は知らん)。 - よくみかける「根を遡る処理の過程で親を根に直接つなぎかえる」メソッドだと、そのメソッドが呼ばれたあとに併合すると高さが増える。よくみかける実装だとその次のクエリでメソッドを呼ぶ際につなぎかえる

ランク付け

f:id:or3:20190110004804j:plain

  • 根に木の高さの情報をもたせておく。「各ノード」じゃなくて根のノードだけにもたせるのは木の根同士だけを比較すれば充分だから、と思っている。実装が楽だし
  • 併合の際に低いほうの根の親を木の高いほうの根にすることで、計算の前後で木の高さを保つことができる
    • 木の高さが低いとその分再帰が浅くなるので計算が早くなる
  • これもO(logn)らしいが計算方法など知る由もない

この二つはUnion-Findの高速化としていろいろな場所で書かれているが、なぜか重み付きUnon-Findはこれらのテクニックを二つとも使った実装しか見当たらない。どうせ「Union-Findくらいみんなライブラリにもってて当然のように圧縮もランク付けも実装してるんだろうし、それらを流用するのが前提で説明してやんよ」ということなのだろう。余計なお世話だ。

重み

さきほどggった資料のうちqiitaのほうにこのような記述がある。

最初の

w += weight(x); w -= weight(y);

と補正するところが少しわかりにくいかもしれません。merge(x, y) 操作では、元々の x と y との間に辺を繋ぐのではなく、root(x) と root(y) の間をつなぐので、つなぐべき辺の重みは w ではなく修正が必要になります。

これを105回くらい読んでも式の意味がわからなかったので書いた。

まず、各ノードにもたせるコスト(重さ)とはなんなのか。これはノードと、そのノードと直上の親とのコストの差分である。

f:id:or3:20190108221254j:plain

併合のクエリx y wとは、ノードxからノードyへのコストがwということである。 f:id:or3:20190108222336j:plain したがって上図で5から4までのコストは、通るパスの重さをすべて足し合わせたもので、6+3 = 9ということになる。

xからyへのコストがwということは、逆を言えばyからxへのコストが-wということなので、 f:id:or3:20190108223153j:plain したがって、たとえばx=8, y=5のコストを求めると、

8から4のノードのコスト  + 4から2のノードのコスト + 2から5のノードのコスト
= (-3) + (-3) + (-6)
= -12

になる。

よくある実装ではこれを併合のときに計算していて、異なる木に属する2つのノードどうしを結ぶとき、

w = (xからxの根までのコスト) + (xの根からyの根までのコスト) + (yの根からyまでのコスト)

とやっている。図で示すとこんな感じ f:id:or3:20190108225754j:plain

求めるのは新しくエッジを貼った「xの根からyの根までのコスト」なので、先程の式は

(xの根からyの根までのコスト) = w - (xからxの根までのコスト + yの根からyまでのコスト)

であり、

yの根からyへのコスト = - (yからyの根へのコスト)

なので、結局

(xの根からyの根までのコスト) = w - (xからxの根までのコスト - yからyの根までのコスト)

というわけ。ランク付けを使うと高さの低い木の根から高い木の根にエッジを貼るので、

木xのほうが低い場合:
x -> xの根 -> xの根からyの根 -> yの根からy
木yのほうが低い場合:
y -> yの根 -> yの根からxの根 -> xの根からx

となり、後者の累積和は単純に前者の符号を反転したものになる。なのでランクの大小の比較でそれを判定しているわけ。


これで骨子は分かったと思うので適当に実装していけばいいです。

高速化なしの実装

提出先はAOJのDSL。通らないのは知っててやる。世のサンプルコードすべてが正しくAC通るものと思うなよ(自戒)。「私はわざとジャッジサーバを無駄に回しました」という札を首から下げながら提出。

import sys
sys.setrecursionlimit(10**9)

class WeightedUnionFindTree:
  def __init__(self, n):
    self.par = list(range(n))
    self.weight = [0 for _ in range(n)]
  
  def root(self, t):
    if t == self.par[t]:
      return t
    # 根を遡るので自分の親を返す
    return self.root(self.par[t])
  
  def rec_weight(self, t):
    # 根までの重さの累積和
    if t == self.par[t]:
      return 0
    return self.weight[t] + self.rec_weight(self.par[t])
  
  def same(self, x, y):
    return self.root(x) == self.root(y)
    
  def unite(self, x, y, w):
    rootx = self.root(x)
    rooty = self.root(y)
    if rootx != rooty:
      self.weight[rootx] += w - self.rec_weight(x)
      self.par[rootx] = y

  def diff(self, x, y):
    if self.same(x, y):
      # xからxの根への重さ - (yからyの根への重さ)
      return self.rec_weight(x) - self.rec_weight(y)
    else:
      return "?"
      
n, q = list(map(int, input().split()))
wuft = WeightedUnionFindTree(n)
for _ in range(q):
  query = list(map(int, input().split()))
  if query[0] == 0:
    x, y, w = query[1], query[2], query[3]
    wuft.unite(x, y, w)
  elif query[0] == 1:
    x, y = query[1], query[2]
    print(wuft.diff(x, y))
    

でこれが当然のようにTLEとなる。ので、高速化テクニックのいずれかを実装する。世のサンプルが両方とも使っているのしか見つからなかったことはさきほど述べたが、天の邪鬼なのとちゃんと動くのかのテストもかねて、経路圧縮だけを実装する。

重みつきUnion-Find(path compression)

root()と、併合処理の場合分けをしただけ。

import sys
sys.setrecursionlimit(10**9)
"""
重み付きで経路圧縮
ランクでの比較は考えず、圧縮だけする
"""
class WeightedUnionFindTree:
  def __init__(self, n):
    self.par = list(range(n))
    self.weight = [0 for _ in range(n)]
  
  def root(self, t):
    if t == self.par[t]:
      return t
    # 経路圧縮してみる
    r = self.root(self.par[t])
    self.weight[t] += self.weight[self.par[t]]
    self.par[t] = r
    return r
  
  def rec_weight(self, t):
    if t == self.par[t]:
      return 0
    return self.weight[t] + self.rec_weight(self.par[t])
  
  def same(self, x, y):
    return self.root(x) == self.root(y)
    
  def unite(self, x, y, w):
    rootx = self.root(x)
    rooty = self.root(y)
    if rootx != rooty:
      if y != rooty:
        # 直接yにつなげる
        self.weight[rootx] = w - self.rec_weight(x)
      else:
        # 根につなげる
        self.weight[rootx] = w - (self.weight[x] - self.weight[y])
      self.par[rootx] = y

  def diff(self, x, y):
    if self.same(x, y):
      # xからxの根への重さ - (yからyの根への重さ)
      return self.rec_weight(x) - self.rec_weight(y)
    else:
      return "?"
      
n, q = list(map(int, input().split()))
wuft = WeightedUnionFindTree(n)
for _ in range(q):
  query = list(map(int, input().split()))
  if query[0] == 0:
    x, y, w = query[1], query[2], query[3]
    wuft.unite(x, y, w)
  elif query[0] == 1:
    x, y = query[1], query[2]
    print(wuft.diff(x, y))
    

併合するときにyが葉かどうかで処理を分けているのは、葉だと

w = xからxの根までのコスト + xの根からyの根までのコスト
xの根からyの根までのコスト =  w - xからxの根までのコスト

でyの木のコストを考えずにつなげればいいだけなので。葉以外だと↑で既にあげている感じでいい。

例題 ABC087 D - People on a Line

ということでqiitaのほうの参考でも挙げられていたこれをやる。めんどくさいのでクラスで管理はせず、配列だけでやった。

n, m = list(map(int, input().split()))
par = list(range(n+1))
dis = [0] * (n+1)

def root(x):
  if x == par[x]:
    return x
  r = root(par[x])
  dis[x] += dis[par[x]]
  par[x] = r
  return r

def cost(x):
  # xからxの根までのコスト
  if x == par[x]:
    return 0
  return dis[x] + cost(par[x])

for _ in range(m):
  l, r, d = list(map(int, input().split()))
  # 根が違ってればマージ
  # 同じなら入力されたdistanceがこれまでの入力データからなる
  # 現在の木の構造から計算した数値とあっているか判断
  rootl, rootr = root(l), root(r)
  if rootl != rootr:
    if r != rootr:
      # 直接yにつなげる
      dis[rootl] = d - cost(l)
    else:
      # 根につなげる
      dis[rootl] = d - (cost(l) - cost(r))  # d - (dis[l] - dis[r])
    # ここで親を更新
    par[rootl] = r
  else:
    if d != cost(l) - cost(r):
      print("No")
      exit()

print("Yes")

はい。よくできました。

 

----- 追記 2020-06-24 -----

 

AOJ DSL 1_Bgolangで解いたが、参考にこの記事を見返したら自分で書いたというのに分かりにくくて絶望した。解読したらちょっと簡略化できた。cost再帰的に回すのもめんどくさいので、weight[i]iの直接の親へのコストではなく、iから根へのコストとしてみた。(はてなブログのバグか?なんかしらんけどmarkdown再編集できないんだが...)

 

 

package main
import "fmt"
func main(){
	var N, Q int
	fmt.Scan(&N, &Q)
	parent := make([]int, N)
	weight := make([]int, N)
	for i:=0; i<N; i++{
		parent[i] = i
	}

	var root func(i int)int
	var union func(i, j, k int)
	root = func(x int)int{
		if x == parent[x]{
			return x
		}
		y := root(parent[x])
		weight[x] += weight[parent[x]]
		parent[x] = y
		return y
	}
	union = func(a, b, w int){
		ra, rb := root(a), root(b)
		if ra != rb{
			if b == rb{
				// w = aからaの根+aの根からbの根
				weight[ra] = w - weight[a]
			}else{
				// w = aからaの根+aの根からbの根+bの根からb
				// w = aからaの根+aの根からbの根-bからbの根
				weight[ra] = w  - (weight[a]-weight[b])
			}
			parent[ra] = rb	// これポイント
		}
	}

	for i:=0; i<Q; i++{
		var query int
		fmt.Scan(&query)
		if query == 0{
			var x, y, z int
			fmt.Scan(&x, &y, &z)
			union(x, y, z)
		}else{
			var x, y int
			fmt.Scan(&x, &y)
			if root(x) != root(y){
				fmt.Println("?")
			}else{
				fmt.Println(weight[x]-weight[y])
			}
		}
	}
}

 verify

macで画面キャプチャするときメインディスプレイ以外を撮りたい人のためのAppleScript・Automater

tumblrtwitterで洋画字幕のおもしろキャプチャとか流れてくるのが面白いので真似っこしてストックしている。PC・macで動画を見るときは21インチモニタにつないで観ているのだが、ここぞという場面で画面を止めて⌘+shift+3するとすべてのスクリーンでキャプチャされる。

f:id:or3:20181229171248p:plain
さすがに恥ずかしいので劣化させた
デフォルトだとデスクトップに溜まっていくが、当然メイン(mac本体側)のディスプレイのキャプチャは不必要なのでちまちま消すことになる。⌘+shift+4だとウインドウを選択するのが面倒だ。 タブレットで観るとNetflix on iOSではキャプチャ対応されててクソ。プライムビデオもDLしたものじゃないと撮れなくてクソ

f:id:or3:20181229140039p:plain
google photosにアルバム作って放り込んでいる
そういえばmacにはAppleScriptとかいうのがあってmac上の操作をスクリプト制御できると聞いたことがあるので初めて触ってみた。

スクリプト

ドキュメントすら読んでないしチュートリアルも触ってない、ただのコピペマンです。なので特に説明するべき知識もないのでそのまま書いちゃう。ご覧の通りひどい有様ですが動けばいいんだよ動けば

github.com

Automator

完全に知識がなくて、AppleScriptで完結すると思ったら違くて、ショートカットキーに割り当てるにはAutomaterを使うのが常套手段らしい。これも初めて触る(そもそもmacで日常的なルーチン作業なんて無いし知らんかった)が2,3ググりくらいでなんとかした

Automaterを起動してライブラリ -> AppleScriptを実行 で上記のスクリプトをコピペする。てっきりapplescriptファイルを指定するのかと思ったらふつうにエディタに書き込むタイプだった。管理できなくね?f:id:or3:20181229135257p:plain 適当に名前を決めて保存。

ショートカットキー割当

ふだんまったくキーバインドをいじらないのでこれも初めてだった。システム環境設定 -> キーボード -> ショートカットから↑で保存したAutomaterがあるはずなので選択する。画像の一番下のScreenShotSubmonitorとかいうやつです。そんで適当に(ユニークな)ショートカットキーをつけておく。なんとなくうまくいっていればうまくいく f:id:or3:20181229140342p:plain

よかったね〜〜

以下はただのメモです。おつかれさまでした。良いお年を。

screencaptureコマンド

terminalからscreencaptureコマンドでスクショできる。これに-R<x,y,w,h>オプションで(x, y)を原点として幅w高さhの矩形範囲をキャプチャできるらしいのでやってみたが、認識されているディスプレイの解像度が異なっているとうまく動作しないのか計算方法が違っているのか、ピンポイントでサブモニタだけを撮るのがめんどくさかった。なので仕方なくすべてのディスプレイをキャプチャしてメインディスプレイだけあとで消す、という回りくどいことをしている。試してないけど↑のスクリプトだとメイン除いて4画面までは撮れると思う

"亡霊を残さないために数秒待つ"

とかスクリプト内で書いてますがまあ単に実ファイルは削除したけど参照が残ってるみたいな感じで消そうとするとこういうメッセージが f:id:or3:20181229141545p:plain 出るってやつ。screencaptureコマンドしたあとすぐにゴミ箱ポイーするとこうなるのでちょっとだけディレイかませただけです。亡霊ってなんだよ(正式名称知らない)

Automaterでほかのやり方

なんかAutomaterにはシェルスクリプトも実行できるぽいのでふつうにそれでもイケたと思われる。シェルスクリプトからpythonなりを呼び出してもいい。めんどくさいので別解は書かない(書けない) あとappも起動したりできるし、AppleScriptはapp書き出しができる(って初めて知ったんですがめちゃくちゃ便利じゃん)のでそれでもいい(かもしれない。試してない)

AOJ ALDS1_6_C Quick Sort

ソートアルゴリズムの実装自体やったことないぜ!!!!!!!!!メリークリスマスイヴ!!!!!!!!!

n 枚のカードの列を整列します。1枚のカードは絵柄(S, H, C, またはD)と数字のペアで構成されています。これらを以下の疑似コードに基づくクイックソートで数字に関して昇順に整列するプログラムを作成してください。partition は ALDS1_6_B の疑似コードに基づくものとします。

また、与えられた入力に対して安定な出力を行っているかを報告してください。ここでは、同じ数字を持つカードが複数ある場合、それらが入力で与えられた順序であらわれる出力を「安定な出力」とします。

安定な出力かどうか とは

クイックソートは与えられた配列の要素同士を交換するので安定でない配列ということらしい。安定な出力をするソートにはマージソートがある。

クイックソートマージソートを実装してそれぞれに配列を投げて得られた結果が同じならば安定、異なれば非安定ということにする。

マージソート

分割統治してソートする感じ。半分ずつに分割する部分とそれを統合(merge)する部分がある

  • 半分ずつに分ける
  • 分けたうちの左側と右側を比較
    • 先頭から比較していって小さい方を選択
  • 比較してできた結果からさらに大きい単位で比較

ソート済のLとRをみたときにそれぞれ上の処理をやるとできた結果Aは全体でソートされている。これを小さい単位(2数の比較)から大きくしていく感じ

def merge(a, l, m, r)
  l_arr = a[l, m-l] << 10000000000000000000
  l_i = 0
  r_arr = a[m, r-m] << 10000000000000000000
  r_i = 0

  (l...r).each do |idx|
    # 小さい方から入れていく
    if l_arr[l_i] <= r_arr[r_i]
      a[idx] = l_arr[l_i]
      l_i+=1
    else
      a[idx] = r_arr[r_i]
      r_i+=1
    end
  end
end

def merge_sort(a, left, right)
  if 1 < right-left
    mid = (right+left)/2
    merge_sort(a, left, mid)
    merge_sort(a, mid, right)
    merge(a, left, mid, right)
  end
end

これのmergeメソッドのとこが統合に当たる。あるいは配列ごと作ってぶちこむ手もある。LLならこっちのが簡単そう*1。どっちもやってるこたぁおんなじ

def merge(left, right)
  ret = []
  loop do
    break if left.size.zero? && right.size.zero?
    # 先頭の小さい方から入れていく
    if left.empty? || (!right.empty? && right.first <= left.first)
      ret << right.shift
    elsif right.empty? || (!left.empty? && left.first < right.first)
      ret << left.shift
    else
      p "fuck"
      exit
    end
  end 
  ret
end

def merge_sort(a)
  return a if a.size == 1
  mid = a.size/2
  left = a[0, mid]
  right = a[mid, mid+1]
  left = merge_sort(left)
  right = merge_sort(right)
  return merge(left, right)
end

クイックソート

これも分割統治。配列から適当なpivotを決めて、pivotより左側に「pivotより小さい要素」、右側に「pivotより大きい要素」がくるようにする。この操作をpartitionと呼ばうらしい。partitionによって配列の更新と、更新後のpivotのインデックスを返すようにすれば、pivotより小さい部分の配列と大きい部分の配列に分割できるのでそれらに対して同じように処理していくのだ。partitionの部分はなんか説明しにくいので見てくれ

def partition(a, l, r)
  pivot = a[r-1]
  j = l-1
  (l...r).each do |i|
    if a[i] <= pivot
      j+=1
      a[j], a[i] = a[i], a[j]
    end
  end
  return j
end

def quick_sort(a, l, r)
  if 1 < r-l
    q = partition(a, l, r)
    quick_sort(a, l, q)
    quick_sort(a, q+1, r)
  end
end

だいたいの言語のsort関数なんかはサイズが小さいときはマージソートで大きいときはクイックソート使うらしいみたいなのを聞いたことがある。知らんけど

実装

入力される要素に合わせて上で書いたやつを書き換えたりなんかしたり

def partition(a, l, r)
  pivot = a[r-1][1]
  j = l-1
  (l...r).each do |i|
    if a[i][1] <= pivot
      j+=1
      a[j], a[i] = a[i], a[j]
    end
  end
  return j
end

def quick_sort(a, l, r)
  if 1 < r-l
    q = partition(a, l, r)
    quick_sort(a, l, q)
    quick_sort(a, q+1, r)
  end
end


def merge(a, l, m, r)
  l_arr = a[l, m-l] << ['', 10000000000000000000]
  l_i = 0
  r_arr = a[m, r-m] << ['', 10000000000000000000]
  r_i = 0
  
  (l...r).each do |idx|
    # 小さい方から入れていく
    if l_arr[l_i][1] <= r_arr[r_i][1]
      a[idx] = l_arr[l_i]
      l_i+=1
    else
      a[idx] = r_arr[r_i]
      r_i+=1
    end
  end
end

def merge_sort(a, left, right)
  if 1 < right-left
    mid = (right+left)/2
    merge_sort(a, left, mid)
    merge_sort(a, mid, right)
    merge(a, left, mid, right)
  end
end

n = gets.to_i
a = []
n.times do
  a << gets.chomp.split.map{|x| x.match?(/\d/) ? x.to_i : x}
end
b = a.clone
quick_sort(a, 0, n)
merge_sort(b, 0, n)

puts a == b ? "Stable" : "Not stable"
a.each do |av|
  puts "#{av[0]} #{av[1]}"
end

はい よくできました えらいね。クリスマスプレゼントは職がいいです

*1:LLなら軽率に配列を作ったりぶっ壊すのが楽という雑魚認識

【ruby】連番をキーとし配列を値にしてるハッシュ

irb(main):016:0> n = 4
=> 4
irb(main):017:0> table = (1..n).to_a.zip(Array.new(n, Array.new)).to_h
=> {1=>[], 2=>[], 3=>[], 4=>[]}
irb(main):018:0> table[1] << 1
=> [1]
irb(main):019:0> table
=> {1=>[1], 2=>[1], 3=>[1], 4=>[1]}

👿

irb(main):030:0> table = (1..n).to_a.map{|i| [i, Array.new]}.to_h
=> {1=>[], 2=>[], 3=>[], 4=>[]}
irb(main):031:0> table[1] << "あばばばばば"
=> ["あばばばばば"]
irb(main):032:0> table
=> {1=>["あばばばばば"], 2=>[], 3=>[], 4=>[]}

👼

Ruby オブジェクト -> hash -> json

アロー演算子じゃないです。

インスタンス変数: 値 みたいな感じで

require 'json'
require 'time'

class Hito
  attr_accessor :age, :name, :id
  def initialize(age, name)
    @age = age
    @name = name
    @id = Time.new.iso8601(6)
  end
end

human = Hash.new

a = Hito.new(392, "dare")
b = Hito.new(49, "kimi")
c = Hito.new(113, "watashi")

# ただハッシュにぶちこんだだけのオブジェクトをjsonizeする
def jsonize(dict)
  ret = Hash.new{|h,k| h[k] = {}}
  dict.map{|k, v|
    ret[k] = v.instance_variables.map{|var| [var, v.instance_variable_get(var)]}.to_h
  }.to_json
  open("hoge.json", 'w') do |io|
    JSON.dump(ret, io)
  end
end

human[a.id] = a
human[b.id] = b
human[c.id] = c

jsonize(human)

ハッシュ内ハッシュ Hash.new{|h,k| h[k] = {}}はこれ

qiita.com

インスタンス変数: 値はこれ

qiita.com

特異メソッドにしたいだけ、ならばmodule_functionは要らない

module_functionを使うと確かに特異メソッドが作れる。が、それで作られるメソッドは、同時にプライベートメソッドでもある。( 参考:伊藤淳一「プロを目指すためのRuby入門 言語仕様からテスト駆動開発デバッグ技法まで」kindle版 , 技術評論社, 位置 7425

hoge.rb

module Hoge
  module_function
  def hoge(n)
     "ほげ"*n
  end
end

main.rb

require_relative 'hoge'

def yobu
  puts Hoge.hoge(3)
end
    
yobu

結果

ほげほげほげ

Hogeには外部から呼び出すためのメソッドだけおいておけばいい、という場合に、プライベートメソッドは不要である。むしろ上記の場合だとmain.rbから呼ぶ際にレシーバを指定する方法がそもそも無いのでプライベートメソッドは必要ない。(ここんとこ理解および日本語があやしいかもしれないので間違ってたらそっと教えてください

解決法

特異クラスにしてやればよい。

hoge.rb

module Hoge
  # module_function <- プライベートメソッドは必要ない
  class << self
    def hoge(n)
       "ほげ"*n
    end
  end
end

参考

qiita.com