$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

Union Findアルゴリズムの様々な実装とパフォーマンス計測

CourseraにAlgorithms Part1という授業があり、これが非常に評判が良いので、会社で勉強会をしている。Week1にUnion Findというアルゴリズムが出てきて、その実装パターンがいくつかあった。それぞれ計算量が違うらしいのだけど、速度がどのように変化するか試したかったので、実装してパフォーマンス計測をしてみた。それぞれの実装の詳しい説明が知りたかったら、https://www.coursera.org/learn/algorithms-part1 を見ると良い。

Union Findとは何か

二つのノードを繋いでいき(Union)、あるノードとあるノードがつながっているか(Find or Connected)を判定するアルゴリズム

例えば、union(1,6)、union(5,6)、union(2,7)、union(3,8)、union(4,9)、union(8,9)とすると以下のようになる。

この時、1と5はつながっている、0と7はつながっていないなどといったことを判定する。


以下の記事に説明が載っているので、こちらも参考になる。

Union Findを使えると何ができるか

例えば以下のことができたりする。

  • SNSで友達があった時に、ある人とある人が友達を辿っていくとつながっているかどうか調べる
  • 電気回路があった時に、正しく電気が流れるかシミュレーション
  • 水が上から下に流れる通路があるか調べる

実装パターン

  • 集合を使った実装
  • ツリー構造を使った実装
  • ツリー構造をできる限りバランスするようにした実装
  • ツリー構造を辿る時に平坦化もしていく実装

これらの説明は http://www.slideshare.net/chokudai/union-find-49066733 にいい感じにまとまっているので省く。

集合を使った実装

配列を使い、同じ集合には同じ数字を与えていくパターン。実装は https://github.com/shibayu36/algorithms/blob/b678d6a5121e91202dd4eda64f564b06434a4b82/src/main/java/org/shibayu36/algorithms/QuickFindUF.java

  • union(p,q) は配列を全てなめて、pの数字だったらqの数字に置き換えるので、O(n)という計算量になる
  • connected(p, q)は、数字が同じならつながっているとするだけなので、O(1)という計算量

connectedは速いけど、unionは毎回配列の全要素をなめるので、感覚的にも遅そう。

ツリー構造を使った実装

今度はツリー構造にするパターン。実装は https://github.com/shibayu36/algorithms/blob/b678d6a5121e91202dd4eda64f564b06434a4b82/src/main/java/org/shibayu36/algorithms/QuickUnionUF.java

  • union(p,q)は両方のrootを探して、qのrootの数字をpに代入する。最悪の場合一直線のツリーになり、その時rootを探すのがO(n)なので、計算量もO(n)。
  • connected(p,q)は両方のrootを探して、rootの数字が一致していたら繋がっている。こちらもO(n)

配列全てをなめる必要はなくなったので、速くなっていそう。

ツリー構造をできる限りバランスするようにした実装

先程のツリー構造を考えると、ツリーの高さが少なければ高速になる。そこでツリー構造を使った実装を改良し、ツリー同士をmergeするときはサイズが小さい方のツリーを下につるすようにし、高さが増えにくくなるようにする。この実装は https://github.com/shibayu36/algorithms/blob/b678d6a5121e91202dd4eda64f564b06434a4b82/src/main/java/org/shibayu36/algorithms/WeightedQuickUnionUF.java

これをすると、高さがlog2(n)に比例するようになるらしい。そのためunionもconnectedも計算量はO(log(n))となる。

ツリー構造を辿る時に平坦化もしていく実装

上記をさらに改良し、connectedでツリー構造をたどるときに、ツリーをどんどん平坦化させていく実装。https://github.com/shibayu36/algorithms/blob/b678d6a5121e91202dd4eda64f564b06434a4b82/src/main/java/org/shibayu36/algorithms/WeightedQuickUnionWithPathCompressionUF.java

これをすると、計算量がlog*(n)になるらしい。log*(2^65536) = 5らしいので、大分はやい。

それぞれのパフォーマンスを取る

後述するベンチマークスクリプトを使ってベンチマークを取る。以下の処理をそれぞれの仕組みでベンチマークする。

  • 10000ノードのUnion Findを行う
  • ランダムに10000回union、その後ランダムに10000回connectedをする
package org.shibayu36.algorithms;

import java.util.ArrayList;
import java.util.List;
import me.geso.nanobench.Benchmark;
import org.apache.commons.lang3.RandomUtils;

public class UnionFindBenchmark {

  public static void main(String[] args) throws Exception {
    new Benchmark(new UnionFindBenchmarkInner()).warmup(1).runByTime(1).timethese();
  }

  public static class UnionFindBenchmarkInner {
    private int size = 10000;
    private List<List<Integer>> unions = new ArrayList<>();
    private List<List<Integer>> connecteds = new ArrayList<>();

    // unionをするリストを作っておく
    {
      for (int i = 0; i < size; i++) {
        List<Integer> union = new ArrayList<>();
        union.add(RandomUtils.nextInt(0, size - 1));
        union.add(RandomUtils.nextInt(0, size - 1));
        unions.add(union);
      }
    }

    // connectedをするリストを作っておく
    {
      for (int i = 0; i < size; i++) {
        List<Integer> connected = new ArrayList<>();
        connected.add(RandomUtils.nextInt(0, size - 1));
        connected.add(RandomUtils.nextInt(0, size - 1));
        connecteds.add(connected);
      }
    }

    @Benchmark.Bench
    public void QuickFindUF() {
      QuickFindUF uf = new QuickFindUF(size);

      for (List<Integer> union : unions) {
        uf.union(union.get(0), union.get(1));
      }

      for (List<Integer> connected : connecteds) {
        uf.connected(connected.get(0), connected.get(1));
      }
    }

    @Benchmark.Bench
    public void QuickUnionUF() {
      QuickUnionUF uf = new QuickUnionUF(size);

      for (List<Integer> union : unions) {
        uf.union(union.get(0), union.get(1));
      }

      for (List<Integer> connected : connecteds) {
        uf.connected(connected.get(0), connected.get(1));
      }
    }

    @Benchmark.Bench
    public void WeightedQuickUnionUF() {
      WeightedQuickUnionUF uf = new WeightedQuickUnionUF(size);

      for (List<Integer> union : unions) {
        uf.union(union.get(0), union.get(1));
      }

      for (List<Integer> connected : connecteds) {
        uf.connected(connected.get(0), connected.get(1));
      }
    }

    @Benchmark.Bench
    public void WeightedQuickUnionWithPathCompressionUF() {
      WeightedQuickUnionWithPathCompressionUF uf = new WeightedQuickUnionWithPathCompressionUF(size);

      for (List<Integer> union : unions) {
        uf.union(union.get(0), union.get(1));
      }

      for (List<Integer> connected : connecteds) {
        uf.connected(connected.get(0), connected.get(1));
      }
    }
  }
}

結果は次のようになる。

Score:

QuickFindUF:  1 wallclock secs ( 1.07 usr +  0.01 sys =  1.08 CPU) @ 12.05/s (n=13)
QuickUnionUF:  1 wallclock secs ( 1.13 usr +  0.01 sys =  1.14 CPU) @ 31.50/s (n=36)
WeightedQuickUnionUF:  1 wallclock secs ( 1.03 usr +  0.03 sys =  1.06 CPU) @ 1452.27/s (n=1541)
WeightedQuickUnionWithPathCompressionUF:  1 wallclock secs ( 1.12 usr +  0.02 sys =  1.14 CPU) @ 1934.83/s (n=2199)
  • 集合を使った実装 = 12.05/s
  • ツリー構造を使った実装 = 31.50/s
  • ツリー構造をできる限りバランスするようにした実装 = 1452.27/s
  • ツリー構造をできる限りバランスするようにした実装 = 1934.83/s

O(n)とO(log(n))の速度差が実感できる。これがアルゴリズムの力か。

まとめ

今回はCourseraのAlgorithms Part1のWeek1の授業を見て、実装ごとにUnion Findの速度がどのように変化するか試したかったので、簡単に実装してパフォーマンス検証をしてみた。改めてアルゴリズムの重要性を感じる。

「ウェブ小説の衝撃」読んだ

ウェブ小説のドメイン知識を知りたかったので読んだ。

印象に残ったのは以下の二点。小説家になろうで小節ごとのアクセス数が公開されていることで、版元が判断しやすいという効果もあるのだなあと思った。

  • ウェブ小説であれば、書籍を出す前から人気度合いが分かるため、版元が判断しやすい
  • ウェブ小説であれば、書きながらすぐに反応をもらえるため、書き手が高速に改善を進めやすい

読書メモ

  • 今は紙の雑誌には小説をプロモーションする力がない 16
  • ウェブなら書籍化する前から数字が見えているので、部数などに参考になる数字が既にある 23,24
  • ジャンル小説の楽しみとはあるフォーマットを前提に差分を楽しむもの 48
  • アクセス数が可視化されているおかげで、版元が判断しやすくなる 59
  • ウェブ小説であれば作者が仮説検証を回すことができる 60
  • なろうの書籍化は初速がよく、回転率が良く、利幅が大きかった 115
  • 認知される成功例はターゲットxコンテンツxチャネルの三つの点を抑えている 126

様々な価値観を持った人の生の声を聞ける「現役東大生が1日を50円で売ってみたら」読んだ

読んだ。非常に面白かった。自分はエッセイやノンフィクションの本は読んだことはあまりなかったのだけど、今後はいろいろ探してみようかなと思うくらいに面白かった。

著者が1日を50円で売ったことで出会った人たちと会話している内容や、それについて著者がどう思ったかなどが、この本に書かれている。発達障害の子供、ニューハーフ、キャバクラの人、地方議員など、本当にいろんな価値観を持った人たちと出会っていて、しかもそれぞれでネットやテレビの情報などではない生の声が聞けているのが良かった。これを読んでいると、自分もまだレッテルを貼っていることがあるなあと思う。

個人的に好きだったエピソードは、「ニューハーフと、デート」という話。LGBTの割合が13人に1人もいるとは思ってもいなくて、左利きと同じくらいの人数と同じだし、マイノリティとはなんなのかという気持ちになった。

とにかくおすすめ。興味があったら是非読んで欲しい。