文字列リストをTRIE Optimizeされた正規表現に
Posted feedbacks - Nested
Flatten Hidden参考までに日本語版
トライ木 - Wikipedia: http://ja.wikipedia.org/wiki/%E3%83%88%E3%83%A9%E3%82%A4%E6%9C%A8
お題は要するに「文字列のリストを与えられたときに、それにマッチする正規表現を返せ」ということですね。
で、(?:foo|bar|baz)なんていうトリビアルな出力を許すと何も面白くないので、単語の頭が一致する場合はくっつけて(?:foo|ba[rz])とかにするということですよね?
> お題は要するに「文字列のリストを与えられたときに、それにマッチする正規表現を返せ」ということですね。
出力的にはYesですが、実装的にはNoです。 TRIE Optimizeされた正規表現は、そうでない正規表現と比べてかなりの速度差が出ます。詳しくは
のベンチマークの項をご覧ください。
Dan the Regular Expressionist
素朴な方法で
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | import sys
import re
class Node(dict):
def __init__(self, c, acceptable, children = None):
if children is not None:
super(Node, self).__init__(children)
else:
super(Node, self).__init__()
self.c = c
self.acceptable = acceptable
def get_pattern(self):
s = re.escape(self.c)
trail = '|'.join(child.get_pattern() for child in self.itervalues())
if trail:
s += '(?:%s)' % trail
if self.acceptable:
s += '?'
return s
def optimize(self):
children = dict()
for child in self.itervalues():
optimized = child.optimize()
children[optimized.c] = optimized
if len(children) == 1 and not self.acceptable:
child = children.values()[0]
c = self.c + child.c
acceptable = child.acceptable
return Node(c, acceptable, child)
else:
return Node(self.c, self.acceptable, children)
class Trie(object):
def __init__(self, root = None):
if root is None:
self.root = Node('', False)
else:
self.root = root
def add(self, word):
node = self.root
for c in word:
if c not in node:
node[c] = Node(c, False)
node = node[c]
node.acceptable = True
def get_pattern(self):
return self.root.get_pattern()
def optimize(self):
return Trie(self.root.optimize())
def main(args):
import fileinput
trie = Trie()
for line in fileinput.input(args):
word = line.strip()
trie.add(word)
trie = trie.optimize()
print trie.get_pattern()
if __name__ == '__main__':
main(sys.argv[1:])
|
あぁ、よく考えれば TRIE そのものをいじらなくてもいいな。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | import sys
import re
class Node(dict):
def __init__(self, c, acceptable, children = None):
if children is not None:
super(Node, self).__init__(children)
else:
super(Node, self).__init__()
self.c = c
self.acceptable = acceptable
def get_pattern(self):
s = re.escape(self.c)
if len(self) > 0:
if len(self) == 1 and not self.acceptable:
s += self.values()[0].get_pattern()
else:
s += '(?:%s)' % '|'.join(c.get_pattern() for c in self.itervalues())
if self.acceptable:
s += '?'
return s
class Trie(object):
def __init__(self, root = None):
if root is None:
self.root = Node('', False)
else:
self.root = root
def add(self, word):
node = self.root
for c in word:
if c not in node:
node[c] = Node(c, False)
node = node[c]
node.acceptable = True
def get_pattern(self):
return self.root.get_pattern()
def main(args):
import fileinput
trie = Trie()
for line in fileinput.input(args):
word = line.strip()
trie.add(word)
print trie.get_pattern()
if __name__ == '__main__':
main(sys.argv[1:])
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | using System;
using System.Collections.Generic;
using System.IO;
static class Program {
public static void Main(String[] args) {
// 行ごとに読み込んで
List<string> words = new List<string>();
using(StreamReader sr = new StreamReader(args[0])) {
foreach(string line in sr.ReadToEnd().Split('\n', '\r')) {
if (line != string.Empty && !words.Contains(line)) {
words.Add(line);
}
}
words.Sort();
}
// 重複文字数をチェックして
List<int> indexes = new List<int>(); {
int[] itemp = new int[words.Count];
for(int i = words.Count - 1; 1 <= i; i--) {
int to = Math.Min(words[i - 1].Length, words[i].Length);
for(int j = 0; j < to && words[i - 1][j] == words[i][j]; j++) {
itemp[i]++;
}
}
indexes.AddRange(itemp);
}
// 重複文字数を多い順に並び替えて
List<int> times = new List<int>(); {
foreach(int cnt in indexes) {
if (!times.Contains(cnt))
times.Add(cnt);
}
times.Sort();
times.Reverse();
}
// 重複部分を削っていく
foreach(int steps in times) {
for(int i = words.Count - 1; 0 < i; i--) {
if (indexes[i] == steps) {
words[i - 1] = string.Format("{0}(?:{1}|{2})",
words[i - 1].Substring(0, steps),
words[i - 1].Substring(steps),
words[i].Substring(steps));
words.RemoveAt(i);
indexes.RemoveAt(i);
}
}
}
string result = "^" + words[0] + "$";
Console.WriteLine(result);
}
}
|
1 2 3 4 5 6 7 8 9 10 11 12 | --- trie.cs.old Tue Nov 20 01:20:52 2007
+++ trie.cs Tue Nov 20 01:21:24 2007
@@ -8,7 +8,7 @@
using(StreamReader sr = new StreamReader(args[0])) {
foreach(string line in sr.ReadToEnd().Split('\n', '\r')) {
if (line != string.Empty && !words.Contains(line)) {
- words.Add(line);
+ words.Add(System.Text.RegularExpressions.Regex.Escape(line)
);
}
}
words.Sort();
|
こういうやり方もあるよ、ということで。
- 文字列を辞書順にソート
- 隣り合う文字列同士の共通接頭辞長(lcps)を計算
で、後はある範囲で lcp の最小値が共通接頭辞の長さで、最小値の位置でリストを分割すれば分割統治法でパターンを構築できると。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | import sys
import re
SLIM_RE = False
class RangeError(Exception):
pass
def lcp(a, b):
"returns length of longest common prefix"
i = 0
l = min(len(a), len(b))
while i < l and a[i] == b[i]:
i += 1
return i
def min_index(l, first, last):
"returns index of minimum value in [first:last]"
if last - first <= 0:
raise RangeError()
i = first
for j in xrange(first + 1, last):
if l[j] < l[i]:
i = j
return i
def get_pattern(words, lcps, level, first, last):
"get optimized regular expression from list of words which is sorted"
if last - first == 1:
return re.escape(words[first][level:])
m = min_index(lcps, first, last - 1)
prefix = words[first][level:lcps[m]]
sub = (get_pattern(words, lcps, lcps[m], first, m + 1),
get_pattern(words, lcps, lcps[m], m + 1, last))
if SLIM_RE:
pattern = '%s(?:%s)' % (re.escape(prefix), '|'.join(filter(bool, sub)))
if len(words[m]) == lcps[m]:
pattern += '?'
else:
pattern = '%s(?:%s|%s)' % (re.escape(prefix), sub[0], sub[1])
return pattern
def main(args):
import fileinput
words = []
for line in fileinput.input(args):
words.append(line.strip())
words.sort()
lcps = [lcp(a, b) for a, b in zip(words[:-1], words[1:])]
pattern = get_pattern(words, lcps, 0, 0, len(words))
print pattern
pat = re.compile(pattern)
for word in words:
if not pat.match(word):
print >>sys.stderr, 'not match with %s' % word
if __name__ == '__main__':
main(sys.argv[1:])
|
Emacs Lisp だと regexp-opt が要求通り(+α)のことをやってくれるようです。 ただ、backslash だらけな elisp の正規表現が返ります。 実行結果は以下。 "program\\(?:ist\\(?:ic\\)?\\|m\\(?:a\\(?:r\\|ti\\(?:c\\(?:ally\\)?\\|st\\)\\)?\ \\|er\\)\\)?"
1 2 3 4 5 6 7 8 9 | (regexp-opt '("program"
"programist"
"programistic"
"programma"
"programmar"
"programmatic"
"programmatically"
"programmatist"
"programmer"))
|
ごちゃごちゃになってしまいました。 (空文字列の有無 (先頭 残り)...) で trie っぽいものを作ってますが、 データ構造が適当すぎたかも。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | (defun classify (strings)
(let ((a ()) (empty nil))
(dolist (s strings (cons empty a))
(if (string= s "")
(setf empty t)
(let* ((fst (elt s 0)) (rest (subseq s 1))
(x (assoc fst a)))
(if x (push rest (cdr x))
(push `(,fst . (,rest)) a)))))))
;; TRIE ::= (data (char . TRIE)*)
(defun make-trie (strings)
(let ((x (classify strings)))
(cons (car x)
(mapcar (lambda (a) (cons (car a) (make-trie (cdr a))))
(cdr x)))))
(defun make-regexp (trie)
(cond ((and (null (car trie)) (null (cddr trie)))
;; (nil (c . subtrie)) => common prefix がある
(format nil "~C~A" (caadr trie) (make-regexp (cdadr trie))))
((cdr trie)
;; (d (c . subtrie) ...) => common prefix なし
(format nil "(?:~{~{~C~A~}~^|~})~@[?~]"
(mapcar (lambda (a) (list (car a) (make-regexp (cdr a))))
(cdr trie))
(car trie)))
(t
;; 空文字列しかない
"")))
(defun trie-optimize (strings) (make-regexp (make-trie strings)))
|
だいたい同じことを OCaml で。 パターンマッチのおかげで多少は読みやすくなったかも。 リストの破壊的操作ができないので Map を使ってみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | open Printf
type trie = Node of bool * (char * trie) list
module M = Map.Make(struct type t = char let compare = Char.compare end)
let rec classify b map = function
| [] -> b, map
| ""::rest -> classify true map rest
| str::rest ->
let c, s = str.[0], String.sub str 1 (String.length str - 1) in
let newmap =
M.add c (s::try M.find c map with Not_found -> []) map in
classify b newmap rest
let rec make_trie strings =
let b, map = classify false M.empty strings in
Node(b, M.fold (fun c strs al -> (c, (make_trie strs))::al) map [])
let rec make_regexp = function
| Node(false, [c, trie]) -> sprintf "%c%s" c (make_regexp trie)
| Node(b, (_::_ as alist)) ->
let res =
List.map (fun (c, t) -> sprintf "%c%s" c (make_regexp t)) alist in
let re = String.concat "|" res in
sprintf "(?:%s)%s" re (if b then "?" else "")
| _ -> ""
let trie_optimize strings = make_regexp (make_trie strings)
|
内部ではTRIEを使っていませんが、出力はお題でのサンプルと、 少なくともこの入力に関しては、同じになっています。 お題の解釈が出力だけを要求しているようにも読めるので、 とりあえず投稿します。 ちなみにカッコの中に'-xism'がある正規表現は pythonのreモジュールでは使えないようです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import sys
import re
def f1(l):
if len(l) == 1:
return l[0]
r = '^(?:%s%s)' % ('(?:'.join(list(l[0])), ')?' * (len(l[0])-1))
b = []
c = []
for s in l:
s = s.rstrip()
m = re.findall(r, s)
if m:
b.append((m[0], s))
else:
c.append(s)
a = min([t[0] for t in b])
flg = bool([t[1] for t in b if a == t[1]])
b = [t[1][len(a):] for t in b if len(t[1]) != len(a)]
if b:
a = '%s(?:%s)%s' % (a, f1(b), '?' if flg else '')
if c:
c = f1(c)
a = '%s|%s' % ((a, c) if len(a) > len(c) else (c, a))
return a
def f(l):
return '(?-xism:%s)' % f1(l)
# return '(?:%s)' % f1(l)
if __name__ == '__main__':
print f(sys.stdin.readlines())
|
1文字ずつのツリーを作って、子を1つしかもたない親に子をくっつけて、参考ページにあるようなTrieツリーにしました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | import sys
def get_chr_tree(strs, before=""):
index = len(before)
s = set([str[index] for str in strs if len(str) > index and str[:index]==before])
l = [get_chr_tree(strs, before+c) for c in s]
return [before[-1]] + l if before else l[0]
def merge_chr(chr_tree):
try:
if len(chr_tree) == 2:
return merge_chr([chr_tree[0]+chr_tree[1][0]] + chr_tree[1][1:])
else: return [chr_tree[0]] + [merge_chr(tree) for tree in chr_tree[1:]]
except IndexError:
return chr_tree
def to_regexp(chr_tree):
def make_regexp(chr_tree):
if len(chr_tree) == 1: return chr_tree[0]
else:
hatena = "?" if [True for l in chr_tree[1:] if "\n" in l] else ""
try:
inner = "".join(["|" + make_regexp(tree) for tree in chr_tree[1:]])
except IndexError:
print "IndexError",chr_tree
inner = ""
return chr_tree[0] + "(?:" + inner + ")" + hatena
return "(?-xism:" + make_regexp(chr_tree).replace("|\n", "").replace("\n", "").replace(":|", ":") + ")"
words = []
for line in sys.stdin: words.append(line)
chr_tree = get_chr_tree(words)
merged_chr_tree = merge_chr(chr_tree)
print to_regexp(merged_chr_tree) # => (?-xism:program(?:m(?:a(?:ti(?:c(?:ally)?|st)|r)?|er)|ist(?:ic)?)?)
|
しまった。言語の指定忘れてた。Pythonです。
SiroKuro さんの #4105 を参考にさせて頂きました。最後、たたみ込みながら正規表現を生成するところはたいへん小気味よいですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | | assemble |
assemble := [:words |
| prefixSizes |
words := words sort.
prefixSizes := words overlappingPairsCollect: [:aa :bb |
| shorterSize found |
shorterSize := aa size min: bb size.
found := (1 to: shorterSize) findFirst: [:idx | (aa at: idx) ~= (bb at: idx)].
found isZero ifTrue: [shorterSize] ifFalse: [found - 1]].
prefixSizes asSet asArray sort reverseDo: [:idx |
| found |
[(found := prefixSizes lastIndexOf: idx) > 0] whileTrue: [
words at: found put: ('{1}(?:{2}|{3})' format: {
(words at: found) first: idx.
(words at: found) allButFirst: idx.
(words at: found + 1) allButFirst: idx}).
words := words copyWithoutIndex: found + 1.
prefixSizes := prefixSizes copyWithoutIndex: found]].
'^', words first, '$'].
assemble value: #(
program
programist
programistic
programma
programmar
programmatic
programmatically
programmatist
programmer)
"=> '^program(?:|(?:ist(?:|ic)|m(?:a(?:|(?:r|ti(?:c(?:|ally)|st)))|er)))$' "
|
なんだかメタキャラクタのエスケープを全くしていないコードが目立つような。 まずくないですかね?
気付いていながらエスケープしないコードを投稿してしまった人です。
単に、まあいいかなと思ってそのままにしました。 お題の趣旨はどうやって trie を作るかで、問題文が細かいところにはあまり拘らないという感じだったので。
ただ、改めて考えると一応正規表現を作れという問題ですから、よくなかったかもしれません。少なくとも気付いてたなら投稿時に言及しとくべきでしたね。
Text.RegexモジュールではデフォルトではPOSIXの正規表現をつかっている. ここではそれにしたがった正規表現文字列を生成する. メタ文字のエスケープもしたつもり. 実行結果 *Main> putStrLn $ oregex (unfoldTree phi sample) "" program(ist(ic)?|m(a(r|ti(c(ally)?|st))?|er))?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | import Data.List
import Data.Tree
type Trie = Tree (Bool,String)
eqapp f x y = f x == f y
lcp :: [String] -> (String, [String])
lcp [] = ("",[])
lcp xxs@(x:xs)
= if null x
then ("",xxs)
else if all (isPrefixOf [head x]) xs
then case lcp (map tail xxs) of
(ps,yys) -> (head x:ps,yys)
else ("",xxs)
phi :: [String] -> ((Bool,String), [[String]])
phi [x] = ((False,x), [])
phi xxs = case lcp xxs of
(ps,"":ys) -> ((True,ps), groupBy (eqapp head) ys)
(ps,yys) -> ((False,ps),groupBy (eqapp head) yys)
oregex :: Trie -> ShowS
oregex (Node (p,s) [])
| p = showString (escape s) . showString "?"
| otherwise = showString (escape s)
oregex (Node (p,s) cs)
| p = (showString (escape s) .) $ (. showString "?") $ paren $ foldr (.) id (intersperse (showString "|") (map oregex cs))
| otherwise = (showString (escape s) .) $ paren $ foldr (.) id (intersperse (showString "|") (map oregex cs))
paren :: ShowS -> ShowS
paren s = showString "(" . s . showString ")"
metaChars = "\\|[](){}.*+?^$"
escapeMeta c | elem c metaChars = '\\':[c]
| otherwise = [c]
escape = concatMap escapeMeta
sample = sort ["program"
,"programist"
,"programistic"
,"programma"
,"programmar"
,"programmatic"
,"programmatically"
,"programmatist"
,"programmer"
]
|
program
programist
programistic
programma
programmar
programmatic
programmatically
programmatist
programmer
を与えた場合は、
program(?:|ist(?:|ic)|m(?:a(?:|r|ti(?:c(?:|ally)|st))|er))
を生成します。
#Javaの総称で再帰的なデータ構造を表現する事はできないのでしょうか(この例ではキャストで逃げています)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | import java.util.*;
@SuppressWarnings("unckecked")
public class Trie {
public static final String MUST_ESCAPE = "()[]\\.+*?|";
private Map<Character, Map> trie = new TreeMap<Character, Map>();
public void add(String str) {
Map<Character, Map> t = trie;
for (char c : str.toCharArray()) {
if (t.containsKey(c)) {
t = (Map<Character, Map>)t.get(c);
} else {
Map<Character, Map> t2 = new TreeMap<Character, Map>();
t.put(c, t2);
t = t2;
}
}
t.put('\0', null);
}
public String toRegex() {
return toRegex(trie).toString();
}
private StringBuilder toRegex(Map<Character, Map> t) {
StringBuilder sb = new StringBuilder();
Set<Character> keys = t.keySet();
if (keys.size() > 1)
sb.append("(?:");
Iterator<Character> i = keys.iterator();
while (i.hasNext()) {
char c = i.next();
if (c != '\0') {
if (MUST_ESCAPE.indexOf(c) >= 0)
sb.append('\\');
sb.append(c);
sb.append(toRegex(t.get(c)));
}
if (i.hasNext())
sb.append('|');
}
if (keys.size() > 1)
sb.append(")");
return sb;
}
public static void main(String[] args) {
Trie t = new Trie();
for (String str : args) {
t.add(str);
}
System.out.println(t.toRegex());
}
}
|
標準ライブラリ(std.regexp)では後方参照なしのグルーピングがサポートされないので、普通の括弧にしました。
program(ist(ic)?|m(a(r|ti(c(ally)?|st))?|er))?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | import std.stdio, std.string, std.utf;
class Trie {
private struct Node {
Node*[dchar] nodes;
bool matchTail;
const string toString() {
if(!nodes.length) return "";
string[] patterns;
foreach(c, node; this.nodes) {
patterns ~= quotemeta(toUTF8([c])) ~ node.toString();
}
string pattern = patterns.join("|");
if(nodes.length == 1) {
return matchTail ? "(" ~ pattern ~ ")?" : pattern;
} else {
return "(" ~ pattern ~ ")" ~ (matchTail ? "?" : "");
}
}
}
private Node tree;
this() { }
this(const(string)[] words) {
foreach(word; words) {
this.addWord(word);
}
}
void addWord(string word) {
auto node = &this.tree;
foreach(dchar c; word) {
if(auto p = c in node.nodes) {
node = *p;
} else {
node = node.nodes[c] = new Node;
}
}
node.matchTail = true;
}
const string toString() {
return tree.toString();
}
}
private string quotemeta(string |




dankogai
#4038()
[
Perl
]
Rating0/2=0.00
これは、実例を見た方が簡単だと思います。 CPANにRegexp::Assembleというモジュールがあるのですが、要はこれの簡易版を作って欲しいということです。私自身、同様のことを行うモジュールを過去にいくつか作っています(e.g Regexp::Optimizer)。
ここでは、文字列のリストを受け取って、それをTRIE化した正規表現に出来ればOKです。Regexp::AssembleやRegexp::Optimizerは正規表現を受け取ってそれをTrie化することも可能ですし、Perl 5.10では内部的にTrie Optimizationを行ったりするのですが、そこまでの機能は求めません。
なお、ここで言う「正規表現」は、必ずしもPerl互換のものである必要はありません。それがTrieになっていることをきちんと示せればOKです。
とはいうものの、Perl5互換になっていた方が、サポートしている環境が多くて有用性は高そうです。可能であればそうして下さい。
Dan the Regexp Assembler
see: Trie (en.wikipedia)
Rating0/2=0.00-0+
[ reply ]