$shibayu36->blog;

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

冪集合を作るメソッドをジェネリクス対応する

以前 Javaで冪集合を生成する - アルゴリズム学習(その3) - $shibayu36->blog; で冪集合を作るメソッドを実装していた。しかし、以前の実装だとListでしか冪集合を作ることができなかった。最近パーフェクトJavaを読んで、ジェネリクスについて学んだので、試しに冪集合を作るメソッドをジェネリクス対応してみた。コードは ここ においてある。

package org.shibayu36.algorithms;
import java.util.ArrayList;
import java.util.List;

public class PowerSetGenerics {
  public static <T> List<List<T>> make(List<T> data) {
    if (data.size() == 0) {
      List<List<T>> empty = new ArrayList<>();
      empty.add(new ArrayList<>());
      return empty;
    }

    List<List<T>> result = new ArrayList<>();
    for (int i = 0; i < Math.pow(2, data.size()); i++) {

      List<T> set = new ArrayList<>();
      int flags = i;
      for (int j = 0; j < data.size(); j++) {
        int flag = flags % 2;
        if (flag == 1) {
          set.add(data.get(j));
        }
        flags = flags / 2;
      }
      result.add(set);
    }

    return result;
  }
}

前とのdiffはこれだけ。メソッドローカルの型変数を宣言して、Integerの変わりにこれを利用すれば良いだけ。

@@ -3,18 +3,18 @@
 import java.util.ArrayList;
 import java.util.List;
 
-public class PowerSet1 {
-  public static List<List<Integer>> make(List<Integer> data) {
+public class PowerSetGenerics {
+  public static <T> List<List<T>> make(List<T> data) {
     if (data.size() == 0) {
-      List<List<Integer>> empty = new ArrayList<>();
+      List<List<T>> empty = new ArrayList<>();
       empty.add(new ArrayList<>());
       return empty;
     }
 
-    List<List<Integer>> result = new ArrayList<>();
+    List<List<T>> result = new ArrayList<>();
     for (int i = 0; i < Math.pow(2, data.size()); i++) {
 
-      List<Integer> set = new ArrayList<>();
+      List<T> set = new ArrayList<>();
       int flags = i;
       for (int j = 0; j < data.size(); j++) {
         int flag = flags % 2;

これで次のように、Integer以外の型でも冪集合を作れるようになった。便利。

List<List<Integer>> integerPowerSet = PowerSetGenerics.make(Arrays.asList(1, 2, 3));
System.out.println(integerPowerSet.toString());

List<List<String>> stringPowerSet = PowerSetGenerics.make(Arrays.asList("hoge", "fuga", "bar"));
System.out.println(stringPowerSet.toString());

// [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]
// [[], [hoge], [fuga], [hoge, fuga], [bar], [hoge, bar], [fuga, bar], [hoge, fuga, bar]]


ジェネリクスについては以下の記事が参考になる。

ハッカソン旅行 in 和倉温泉

和倉温泉に旅行に行きました。観光 & プログラミングをしていました。

f:id:shiba_yu36:20170109194244j:plain:h500

AKB丼(甘エビ・カニ・ブリ丼)を食す。
f:id:shiba_yu36:20170109194411j:plain:h500

生卵を買うと、自分で温泉卵に出来る。塩分が強い温泉なので、自然と味がついていた。
f:id:shiba_yu36:20170109194506j:plain:h500

こいつ自分も卵のキャラなくせに、絶対に子供を温泉卵にしようとしていると思う。
f:id:shiba_yu36:20170109194622j:plain:h500

のと鉄道永井豪のラッピング電車にも乗った。
f:id:shiba_yu36:20170109194755j:plain:h500
f:id:shiba_yu36:20170109194800j:plain:h500

途中で降りた西岸駅というところは花咲くいろはの聖地らしい。
f:id:shiba_yu36:20170109194936j:plain:h500
f:id:shiba_yu36:20170109194941j:plain:h500

西岸駅で特にやることはなくてウロウロしていたら、美味しい牡蠣を出してくれる店を偶然発見。営業前だけど入れてもらった。昼前には満席で並ぶ人もいたのでラッキーだった。
f:id:shiba_yu36:20170109195059j:plain:h500f:id:shiba_yu36:20170109195101j:plain:h500f:id:shiba_yu36:20170109195104j:plain:h500

帰りは花咲くいろはのラッピング電車に。
f:id:shiba_yu36:20170109195146j:plain:h500f:id:shiba_yu36:20170109195148j:plain:h500f:id:shiba_yu36:20170109195151j:plain:h500

帰る直前の電車の待ち時間は、「はいだるい」という店で暇を潰していた。「はいだるい」ってなんだよ、営業がだるいのかよって思ってたけど(看板もだるそう)、入って聞いてみたら「はいだるい」というのは方言ということが分かった。新聞記事に「観光客に意味を聞かれれば、そこから会話が生まれる」と書かれていて、実際に聞いて会話が生まれたから、非常に良い名前だなと思った。

f:id:shiba_yu36:20170109195538j:plain:h500f:id:shiba_yu36:20170109195542j:plain:h500f:id:shiba_yu36:20170109195547j:plain:h500

帰りの電車は花嫁のれんという電車に。特に何も知らず適当に予約していたのだけど、観光列車だったようで、普通の電車とは違い、良い体験だった。

f:id:shiba_yu36:20170109195658j:plain:h500f:id:shiba_yu36:20170109195702j:plain:h500f:id:shiba_yu36:20170109195708j:plain:h500f:id:shiba_yu36:20170109195711j:plain:h500f:id:shiba_yu36:20170109195715j:plain:h500f:id:shiba_yu36:20170109195718j:plain:h500

観光計画は全く立てずに行ったのだけど、偶然的にもいろいろ発見があったり、その土地の人と会話できたりして良かった!


ちなみにハッカソンでは、勉強しているアルゴリズムの実装をしたり、ブログを書いたりしていました。成果は以下。

blog.shibayu36.org
blog.shibayu36.org
blog.shibayu36.org
blog.shibayu36.org

Suffix Trieを使って文字列マッチングする

文字列マッチングを行うためのアルゴリズムとして、Suffix Trieを使った探索というものがある。これはテキストからSuffix Trieという構造を作り、パターンをつかってそれを辿ることで、パターンの長さmに対して、O(m)の計算量で探索できるものである。

今回はJavaでSuffix Trieを使った探索をしてみた。

トライ木とパトリシア

先にトライ木とパトリシアについて紹介。

今回Suffix Trieという構造を調べていると、同じような構造としてSuffix Treeというものが出てきて混乱した。よく調べてみると、Suffixの集合をトライ木という構造で実装したものがSuffix Trieで、パトリシアという構造で実装したものがSuffix Treeらしい。

トライ木はnodeを連結していって、その枝に1文字を割り当てて辿れるようにした構造。トライ木は枝に1文字しか割り当てない構造上非常にメモリを食うので、複数文字格納するようにしてメモリ効率をよくしたものをパトリシアと言うらしい。詳しくは以下を参照。

枝に複数文字入れるということは、普通に実装が大変そうな予感がしたので、今回はトライ木構造を使うSuffix Trieの方を実装することにした。

Suffix Trieを使ったマッチングとは

Coursera の Algorithms on Strings 受けました - たにしきんぐダム のSuffix Trieの説明が分かりやすい。

まず、テキストの全てのSuffix集合を使ってトライ木を作る。これで例えば「nana」というテキストなら、上記の記事のような構造が出来る。


f:id:tanishiking24:20161218022331p:plain
「Coursera の Algorithms on Strings 受けました」から引用

あとは「na」というパターンでマッチしようとすると、このトライ木を辿ったら以下のようになり、辿ったノードの下の全ての葉ノードの数字がマッチした位置となる。

f:id:shiba_yu36:20170108220909j:plain

他にも次の記事が参考になる。

実装

次のフローで文字列マッチングが出来ることが分かった。

  • 与えられたテキストからトライ木を構築する
  • 与えられたパターンでトライ木を辿り、辿った全ての子ノードを返す

というわけで実装は以下のとおりとなる。new SuffixTrie(text)とするとトライ木が構築され、そのインスタンスでsearchPatternするとpositionのリストが返ってくるようになっている。 https://github.com/shibayu36/algorithm-study/tree/master/algorithms-on-string/src/main/java あたりにコードは置いてある。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

// new SuffixTrie(text).searchPattern(pattern)とすれば
// text中からpatternがどこに出現するかpositionのリストを返してくれる
public class SuffixTrie {
  public String text;
  public SuffixTrieNode root;

  public SuffixTrie(String text) {
    // 番兵を入れておけば、葉ノードからさらに枝が生えることがなくなるので
    // 構築や探索が楽になる。
    this.text = text + "\0"; // マッチングしやすいように番兵を入れておく
    this.root = new SuffixTrieNode();

    // 全てのSuffixを一つずつTrie木に追加していく
    for (int i = 0; i < this.text.length(); i++) {
      this.insertSuffix(new Suffix(this.text, i));
    }
  }

  // Suffix一つをTrie木に追加する
  private void insertSuffix(Suffix suffix) {
    SuffixTrieNode node = this.root;

    // 1文字ずつrootから辿りながらnodeを作成していく
    for (int i = 0; i < suffix.length(); i++) {
      Character c = suffix.charAt(i);
      if (node.children.containsKey(c)) {
        // 既に辿る枝があれば辿る
        node = node.children.get(c);
      }
      else {
        // なければ次のノードを作る
        SuffixTrieNode newNode = new SuffixTrieNode();
        node.children.put(c, newNode);
        node = newNode;
      }
    }

    // 辿った最後が葉なので、そこにSuffixのpositionを入れておく
    node.position = suffix.index;
  }

  // patternから出現するpositionのリストを返す
  public List<Integer> searchPattern(String pattern) {
    // まずpatternからマッチするnodeを取得する
    SuffixTrieNode matched = this.searchNode(pattern);
    if (matched != null) {
      // 取得したnodeの全ての葉ノードを取得すれば、positionのリストが得られる
      return matched.getAllLeafNodes().stream().map(
          node -> node.position
      ).collect(Collectors.toList());
    }
    else {
      return new ArrayList<>();
    }
  }

  // patternを辿ったnodeを返す
  // nodeが見つからなければnull
  private SuffixTrieNode searchNode(String pattern) {
    SuffixTrieNode node = this.root;
    for (int i = 0; i < pattern.length(); i++) {
      Character c = pattern.charAt(i);
      if (!node.children.containsKey(c)) {
        return null;
      }
      else {
        node = node.children.get(c);
      }
    }
    return node;
  }
}

class SuffixTrieNode {
  public int position; // どこからのsuffixの葉なのか
  public Map<Character, SuffixTrieNode> children = new HashMap<>();

  public SuffixTrieNode() {}

  public boolean isLeaf() {
    return this.children.isEmpty();
  }

  // あるノードの全ての葉ノードを返す
  public List<SuffixTrieNode> getAllLeafNodes() {
    if (this.isLeaf()) {
      List<SuffixTrieNode> nodes = new ArrayList<>();
      nodes.add(this);
      return nodes;
    }
    else {
      List<SuffixTrieNode> nodes = new ArrayList<>();
      for (SuffixTrieNode child : this.children.values()) {
        nodes.addAll(child.getAllLeafNodes());
      }
      return nodes;
    }
  }
}

class Suffix implements Comparable<Suffix> {
  public String text;
  public int index;

  public Suffix(String text, int index) {
    this.text = text;
    this.index = index;
  }
  public int length() {
    return text.length() - index;
  }
  public char charAt(int i) {
    return text.charAt(index + i);
  }

  public int compareTo(Suffix that) {
    if (this == that) return 0;  // optimization
    int n = Math.min(this.length(), that.length());
    for (int i = 0; i < n; i++) {
      if (this.charAt(i) < that.charAt(i)) return -1;
      if (this.charAt(i) > that.charAt(i)) return +1;
    }
    return this.length() - that.length();
  }
}

実際に動かしてみる

実装は出来たので実際に動かしてみる。

まず自分のブログ記事を https://github.com/shibayu36/algorithm-study/blob/master/algorithms-on-string/blog1.txt においた。これから検索をしてみる。試したコードは以下のとおり。

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.List;

public class SuffixTrieSearchSample {

  public static void main(String[] args) throws Exception {
    String text = "";

    try {
      byte[] fileContentBytes = Files.readAllBytes(Paths.get("./blog1.txt"));
      text = new String(fileContentBytes, StandardCharsets.UTF_8);
    } catch (Exception e) {}

    searchAndPrint(text, "基礎技術");
    System.out.println("");
    searchAndPrint(text, "モチベーション");
  }

  private static void searchAndPrint(String text, String pattern) {
    List<Integer> positions = new SuffixTrie(text).searchPattern(pattern);
    positions.sort(Comparator.naturalOrder());
    for (int position : positions) {
      int begin = Math.max(0, position - 5);
      int end = Math.min(text.length(), position + pattern.length() + 5);
      System.out.println(text.substring(begin, end));
    }
  }
}

結果は以下のようになり、正しく文字列マッチングできた。

基礎技術の学習のモ
寿命の長い基礎技術を、モチベ
の中でその基礎技術を実際のサ
開発の中で基礎技術との繋がり
寿命の長い基礎技術でも、モチ
ンを保って基礎技術を学習をす

術の学習のモチベーションをどう保つ
いないと、モチベーションをずっと保
が難しい
モチベーションが保てない
とりあえずモチベーションは保ちやす
きなければモチベーションを保つこと
礎技術を、モチベーションを保ちつづ
を得られ、モチベーションが増加する
で、さらにモチベーションを高められ
技術でも、モチベーションを保って学
どうやってモチベーションを保って基

このアルゴリズムの課題

このアルゴリズムは文字列マッチングの探索フェーズには早いスピードで探索できる一方で、トライ木の構築フェーズではn^2のメモリを使ったり、O(n^2)の計算量が必要になったりすることである。実際に1万文字程度の文章でマッチングしようとすると、メモリを使いすぎて固まってしまった。

このあたりはSuffix Treeや、Suffix Array、LCP Arrayを使うことで改善することが出来るようだ。

まとめ

今回はSuffix Trieによる文字列マッチングをJavaで実装してみた。とりあえずマッチングして動くようなものが出来たので満足。

以前、以下のような記事でSuffix ArrayやLCP Arrayの構築を試していた。これを使うと今回のメモリ効率や計算量の問題が改善されるので、これらの構造を使ったマッチングも実装してみたい。