競技プログラマ向け 形式言語理論
入門 稲葉
JOI
春合宿一浩
2012
文字列やツリーやグラフの 集合
について考える分野
「形式言語理論」とは
文字列やツリーやグラフの 集合
を、どうやって表現するか について考える分野
「形式言語理論」とは
文字列やツリーやグラフの 集合
を、どうやって表現するか について考える分野
「形式言語理論」とは
今日は
文字列の集合だけ 扱います
文字列やツリーやグラフの 集合
を、表現するデータ構造 について考える分野
「形式言語理論」とは
#include <set>
#include <string>
std::set<std::string>
?
文字列やツリーやグラフの 無限かもしれない集合
の、有限のメモリでの表現 について考える分野
「形式言語理論」とは
{“”, “a”, “aa”, “aaa”, “aaaa”, ...}
「長さが偶数の文字列すべて」
「回文じゃない文字列」
「円周率の10
進表記の部分列」
・・・文字列の無限集合の例
いくつかの表現方法の紹介
どんな操作が可能か
(コンテストっぽい)応用
(夢の広がる)応用ここからの話題
有向グラフで表現
文字列集合を
{“a”, “ba”, “bba”, ...}
S G
a
a
b b
「aが奇数個」
{“”, “aa”, “ab”, “b”, “bab”, ...}
SG a
b a
b b G
a
a
G a
b b
「aで始まって長さ2以上、
またはbで始まってbで終わる」
辺に文字が書いてある
各頂点には S
という印 G
という印 S
とG
両方が付いているかも
グラフで表す文字列集合
SG a
b a
b b G
a
a
G a
b b
こういうグラフを、
「
S
からG
までの経路になってる文字列すべて」という集合を表していると考えます。
有限集合は全部書ける root
が、
leaf
がの
tree
で表現する
「特定の文字列を部分に含む文字列ぜんぶ」 KMP
法、Aho-Corasick
法
「a
とb
が交互に繰り返し出てくる文字列」
ループっぽいものはグラフでサイクルを作ると書ける
書けないものもあります。
「回文」 「括弧の対応が取れてる文字列」こんな集合が表せる
S G
2
つの流儀: DFA
とNFA
Deterministic
Finite Automaton
S
が1
つ
一つの頂点から出る、同じ文字の辺は1本以下
Nondeterministic Finite Automaton
なんでもあり
文字無し辺もOK
とする事もS G
S
a a
a b
b b
“”
S G
a
a b
b
bool contains(Automaton a, String w);
与えられた文字列を含むかの判定 Automaton complement(Automaton a);
補集合の計算 Automaton intersect(Automaton a, Automaton b);
共通部分の計算 Automaton equals(Automaton a);
集合として等しい?できる操作の、ごく一部
(だいたい思いつく物はなんでもできます)
S
が1
つ
一つの頂点から出る、同じ文字の辺は1本以下
bool contains(DFA a, String w);
DFA
の場合Node v = a.S; //S
は1
つなので始点は1
つに決まるforeach(char c : w)
v = a.next(v, c); //
文字が決まれば辺も1
つreturn v.isG();
S G
a
a b
b
少しだけ難しい
問題: S
からG
まで、文字列w
に 合わせて動いてたどり着く 道はあるか?bool contains(NFA a, String w);
NFA
の場合S G
S
a a
a b
b b
“”
解法1 :DP
bool[
頂点数][
文字列長+1]
bool contains(NFA a, String w);
NFA
の場合S1 G
S2
a a
a b
b b
“”
○
○
“a b b a”
S1 S2
G
解法1 :DP
bool[
頂点数][
文字列長+1]
bool contains(NFA a, String w);
NFA
の場合S1 G
S2
a a
a b
b b
“”
○
○
×
○
“a b b a”
S1 S2
G
解法1 :DP
bool[
頂点数][
文字列長+1]
bool contains(NFA a, String w);
NFA
の場合S1 G
S2
a a
a b
b b
“”
○ ×
○ ×
× ○
○ ○
“a b b a”
S1 S2
G
解法1 :DP
O( |edge|
・|w| )
bool contains(NFA a, String w);
NFA
の場合S1 G
S2
a a
a b
b b
“”
○ × ○ ○ ×
○ × ○ × ×
× ○ × × ○
○ ○ ○ ×
!!
○!!
“a b b a”
S1 S2
G
解法2 : ビットDP
○○×○ 2
進法で“1101”
とビットにエンコードできる int [
文字列長さ+1]
bool contains(NFA a, String w);
NFA
の場合○ × ○ ○ ×
○ × ○ × ×
× ○ × × ○
○ ○ ○ ×
!!
○!!
“a b b a”
S1 S2
G
1101 0011 1101 1000 0011
解法3
: ビットDP
& 前処理 O( 2 |node| + |w| )
bool contains(NFA a, String w);
NFA
の場合○ × ○ ○ ×
○ × ○ × ×
× ○ × × ○
○ ○ ○ ×
!!
○!!
“a b b a”
S1 S2
G
SG
1101
1101 0011 1101 1000 0011
G
0011
a
1000
b
b
G
0011
a a
b
・・・
・・・
NFA
をDFA
に変換してから処理している NFA
の表現力 =DFA
の表現力
サイズは指数でふくらみますが…
解法3のポイント
bool contains(Automaton a, String w);
与えられた文字列を含むかの判定 Automaton complement(Automaton a);
補集合の計算 Automaton intersect(Automaton a, Automaton b);
共通部分の計算 Automaton equals(Automaton a);
集合として等しい?できる操作その他
Automaton
で表現した 集合a
と集合b
の共通部分
を表す
Automaton
の求め方集合と集合の共通部分
頂点と頂点のペアを作る
1 2
a a
b
b
3 4
a a b
b
2,3 1,3
2,4
1,4
頂点と頂点のペアを作る
1 2
a a
b
b
3 4
a a b
b
2,3 1,3
2,4
a 1,4
1 --- a ---> 2 3 --- a ---> 4
なのでb
頂点と頂点のペアを作る
1 2
a a
b
b
3 4
a a b
b
2,3 1,3
2,4
a 1,4
b
b
a
両方
S
ならS
に 両方G
ならG
に1 2
a a
b
b
3 4
a a b
b
2,3 1,3
2,4
a 1,4
b
b a
「aが奇数個」 「長さが奇数」
「aが奇数個かつ 長さが奇数」
和集合
どちらかが
G
ならG
に1 2
a a
b
b
3 4
a a b
b
2,3 1,3
2,4
a 1,4
b
b a
「aが奇数個」 「長さが奇数」
「aが奇数個または 長さが奇数」
集合の補集合 DFA
ならG
とG
じゃない頂点を反転するだけ NFA
はDFA
に変換してください
空集合かどうか判定 S
からG
に到達可能か判定するだけ
集合の包含関係(
A ⊆ B; A
が完全にB
に含まれているか?)
「((B
の補集合)
とA
の共通部分)
が空集合か?」と同じ
集合が等しいかどうか A ⊆ B && B ⊆ A
(もっと効率よく判定もできます)できる操作その他
1
~N
の自然数を、それぞれ長さM
以下のビット列に変換して自然数のリストを表現することにした。例えば右下の変換表を使うと
[3,1,2]
がになる。
ところがこの変換表は困りもので、
[3,3,3]
もになるし
[1,2,3]
もになってしまう。
変換表を受け取って、こういう困ったこと(違うリストが
同じビット列になってしまう)が起こるかどうか判定せよ。
(N,M ≦ 50)
練習問題
: “Double Meaning”
1 → 1001 2 → 00
3 → 100
100100100
100100100
100100100
“100”
が“1001”
のprefix
なのが問題。これじゃ前から読んでってどちらか区別できない
想定誤答
for(int i=0; i<code.size(); ++i) for(int j=0; j<code.size(); ++j)
if(i!=j && code[i].is_prefix(code[j])) return BAD;
return GOOD;
1 → 1001 2 → 00
3 → 100
この変換表は一意に復元可能
撃墜例
1 → 1
2 → 10
3 → 001
解答例
(もっと効率いい解法はありそう
…
)for(int i=0; i<code.size(); ++i) if(
「i
から始まるリストの変換結果の集合」と 「
i
以外から始まるリストの変換結果の集合」の共通部分が空集合ではない
) return BAD;
return GOOD;
集合を使います
1 → 1001 2 → 00
3 → 100 S 1 0 0 1 G
1 0 0 1
0 0
1 0 0
S
1 0 0 G
1 0 0 1
0 0
1 0 0 S 0 0
「[1,...]で始まるリストの変換結果」
「[2,...]か[3,...]で始まるリストの変換結果」
集合の補集合 DFA
ならG
とG
じゃない頂点を反転するだけ NFA
はDFA
に変換してください
空集合かどうか判定 S
からG
に到達可能か判定するだけ
集合の包含関係(
A ⊆ B; A
が完全にB
に含まれているか?)
「((B
の補集合)
とA
の共通部分)
が空集合か?」と同じ
集合が等しいかどうか A ⊆ B && B ⊆ A
(もっと効率よく判定もできます)できる操作その他
その他にできること:
DFA
の最小化S G
a
a
b b
a
S G
a
a b
G
a b
b
b
実は
表してる集合は 同じ!
「aが奇数個」
O(|edge| log |node|)
でできる。
最小化するとグラフの形が一つに定まる。
集合の = の判定が簡単
共通部分や和集合やNFA → DFA
変換は 無駄に大きいDFA
を作ることが多いので、小さくするのが実用上は必須。
DFA
最小化の嬉しいところ最小化のアルゴリズム
S a G
b G
a
a b b
a
b
方針:
どんな文字列
w
についても、「頂点
v
からw
に沿って進んだ先がG
」if and only if
「頂点
u
からw
に沿って進んだ先がG
」 なら、v
とu
はマージする最小化のアルゴリズム
S a G
b G
a
a b b
a
b
「
G
な頂点」と
「
G
じゃない頂点」はマージできないので 別グループ
最小化のアルゴリズム
S a G
b G
a
a b b
a
b
注目!最小化のアルゴリズム
S a G
b G
a
a b b
a
b
文字
‘a’
で進んだらに入る頂点たちと そうじゃない頂点は マージ不可能
最小化のアルゴリズム
S a G
b G
a
a b b
a
b
よって分離。
最小化のアルゴリズム
S a G
b G
a
a b b
a
b
文字
‘b’
でも同じことを繰り返す。
分割が起きた場合は、
分かれてできたグループ
を両方(元 を使用済 みの場合は小さい方)を 使い同様に分割を繰返す
最小化のアルゴリズム
S a G
b G
a
a b b
a
b
収束したら終了 マージする。
S a G
b G
a
a
b
b
最初の分割 0
文字でG
とG
じゃないところに分かれる頂点を分離
次の分割 1
文字でG
とG
じゃないところに分かれる頂点を分離 ...
というループなので、収束するまでやれば正しい
を分割に使っていれば は小さい方 だけ 使う、というところだけ工夫最小化のアルゴリズム
問題 DFA a
(頂点数≦20
)
文字列w
(長さ≦10
万) 0 ≦ i
<k ≦ |w|
な自然数の組が1
万個 w[i, k)
がDFA
の表す集合に入っているかそれぞれ判定せよ
その他にできること:
セグメント木に載せる
a b b a a b a b
3 1
4
a 2
b
b a
14 23 32 4 1
1 4 23 32 41
1 4 23 32 41
1 4 23 32 41 12
21 34 4 3
1 2 21 34 43
1 2 21 34 43
1 2
21
34
43
a b b a a b a b
3 1
4
a 2
b
b a
14 23 32 4 1
1 4 23 32 41
1 4 23 32 41
1 4 23 32 41 12
21 34 4 3
1 2 21 34 43
1 2 21 34 43
1 2 21 34 43 1 3
2 4 3 1 4 2
1 3 2 4 3 1 4 2
1 3 2 4 3 1 4 2 1 3
2 4
3 1
4 2
a b b a a b a b
3 1
4
a 2
b
b a
14 23 32 4 1
1 4 23 32 41
1 4 23 32 41
1 4 23 32 41 12
21 34 4 3
1 2 21 34 43
1 2 21 34 43
1 2 21 34 43 1 3
2 4 3 1 4 2
1 3 2 4 3 1 4 2
1 3 2 4 3 1 4 2 1 3
2 4 3 1 4 2 1 1
2 2 3 3 4 4
1 1 2 2 3 3 4 4
1 1
2 2
3 3
4 4
a b b a a b a b
3 1
4
a 2
b
b a
14 23 32 4 1
1 4 23 32 41
1 4 23 32 41
1 4 23 32 41 12
21 34 4 3
1 2 21 34 43
1 2 21 34 43
1 2 21 34 43 1 3
2 4 3 1 4 2
1 3 2 4 3 1 4 2
1 3 2 4 3 1 4 2 1 3
2 4 3 1 4 2 1 1
2 2 3 3 4 4
1 1 2 2 3 3 4 4
1 1
2 2
3 3
4 4
DFA
・NFA
の場合、Segment Tree
の替わりにFactorization Forest
というテクニックで
同じ機能のものが定数高さの木でできます。
※詳しくは Web
で!練習問題
AOJ 2017
Topcoder SRM378 Div1 Hard AOJ 1169
Topcoder TCO’09 Semifinal Medium
おまけ: 形式言語理論の未解決問題
Cerný
予想DFA
をb
考えるa b
a
a
b a
a b
どの頂点に いても
baaabaaab
と歩くと
…
Cerný
予想この
DFA
の同期語
といいます。
b
a b
a
a
b a
a b baaabaaab
と歩くと
…
必ず同じ点に着きます
Cerný
予想b b
a a
問題:
N
頂点のDFA
が与えられる。最短の同期語の長さを求めよ。
存在しない場合は
-1
を返せNP 困難
予想: 同期語が存在するなら、
長さは必ず
(N-1) 2
以下である。Jan Cerný, 1964
≪ Cerný
予想≫論理式で表現
文字列集合を
b_after_a(string s) :=
forall i in indices(s) if s[i]==‘a’ then
exists j in indices(s) i ≦ j and s[j]==‘b’
「aが出たら後でbも出る」
even_A(string s) :=
exists A ⊆ indices(s) forall i in indices(s)
if i in A then s[i]==‘a’
else s[i]!=‘a’
& |A| mod 2 = 0
「aが偶数個」
and, or, not , if
~then
(
添え字に関する) forall
(
添え字に関する) exists
(
添え字の集合に関する) forall
(
添え字の集合に関する) exists
mod
定数 で数をカウント(詳細略) (紹介だけ)
すべて
Automaton
に変換できることが知られています
パターンで表現
文字列集合を
Perl, Ruby, Java
等での普通のプログラミングでよく使います。
正規表現
b*ab*(ab*ab*)*
「aが奇数個」a(a|b)(a|b)* | b((a|b)*b)?
「aで始まって長さ2以上、
またはbで始まってbで終わる」
正規表現の使用例
hoge | fuga
hoge
の表す集合とfuga
の表す集合の和集合hoge*
hoge
の表す集合の要素を0個以上何個でも並べた文字列の集合
hoge?
hoge
または空文字列正規表現
NFA
で 表せますhoge*
の例SG a
b a
b b G
a
a
G a
b b
NFA
で 表せますhoge*
の例SG a
b a
b b G
a
a
G a
b b
S “” G
“”
“”
“”
“”
foreach(i : nodes) d[i][i] = 0;
foreach(i--c-->j : edges) d[i][j] = c;
foreach(k : nodes) foreach(i : nodes) foreach(j : nodes) d[i][j] = min(
d[i][j], d[i][k]+d[k][j]);
方針:
Warshall-Floyd
逆に、
Automaton
はすべて正規表現で書けます
1 2
a
a
b
b
foreach(i : nodes) d[i][i] = “”;
foreach(i--c-->j : edges) d[i][j] = d[i][j] | c;
foreach(k : nodes) foreach(i : nodes) foreach(j : nodes) d[i][j] =
d[i][j] | d[i][k] d[k][k]* d[k][j];
方針:
Warshall-Floyd
逆に、
Automaton
はすべて正規表現で書けます
1 2
a
a
b
b
さっきの「
Automaton →正規表現」変換は
*
を使いすぎる。もっと減らせないか? | (
和集合), * (
繰り返し)
の他に& (
積集合),
¬(
補集合), Φ (
空集合)
も使う正規表現を 一般正規表現というおまけ: 形式言語理論の未解決問題
Star-Height
問題b*ab*(ab*ab*)*
¬
((
¬(
¬Φa
¬Φ)a
¬(
¬Φa
¬Φ)a
¬(
¬Φa
¬Φ))*)
「aが奇数個」 「aが偶数個、
じゃない」
*
のネストなしで、Automaton
を 全て一般正規表現で表せるか?Janusz Brzozowski, 1980
≪ Star-Height
問題≫文法で表現
文字列集合を
{“1+2*3/(4-5)”, “0*0*0+0”, ...}
EXPR ::= TERM
| TERM “+” TERM | TERM “-” TERM
TERM ::= FACTOR “*” FACTOR | FACTOR “/” FACTOR FACTOR ::= “(“ EXPR “)”
| “0” | “1” | ... | ”9”
「一桁の数の四則演算式」
{“”, “a”, “b”, “aa”, “aba”, ...}
PALINDROME ::= “a”
| “b”
| “”
| “a” PALINDROME “a”
| “b” PALINDROME “b”
「回文」
例)
文脈自由文法
C ontext F ree G rammar
PALINDROME ::= “a”
| “b”
| “”
| “a” PALINDROME “a”
| “b” PALINDROME “b”
PALIN
→ “a” PALIN “a”
→ “a” “b” PALIN “b” “a”
→ “a” “b” “a” PALIN “a” “b” “a”
→ “a” “b” “a” “” “a” “b” “a”
→ “abaaba”
左辺::=
右辺|
右辺| ... |
右辺という規則の集まりを、
「左辺の記号を右辺のどれかに書き換え」を
繰り返したら作れる文字列の集合、と見なします。
bool contains(CFG g, string w);
P ::= “”
| “a”
| “b”
| “a” P “a”
| “b” P “b”
P ::= “”
| “a”
| “b”
| “a” Q | “b” R Q ::= P “a”
R ::= P “b”
右辺の長さが
2
以下になるように 変形してから・・・bool contains(CFG g, string w);
P ::= “”
| “a”
| “b”
| “a” Q | “b” R Q ::= P “a”
R ::= P “b”
右辺の長さが
2
以下になるように 変形してから・・・動的計画法。
O ( |g| |w| 3 )
。bool
dp[
左辺記号][i][k] =
“
左辺記号”
からw[i .. k)
が作れるか?(オーダーの意味で)世界最速は
2012
年3
月現在O ( |g| ・ |w| 2.3723 )
だそうです。
行列の掛け算と同じオーダです。
bool contains(CFG g, string w);
空集合かどうかの判定
できる(簡単)
和集合の計算
できる(簡単)
共通部分 CFG
とCFG
の共通部分はCFG
では書けない!
補集合 CFG
の補集合はCFG
では書けない!
等しさ = の判定、 包含関係⊆
の判定
決定不能!ほかの集合演算は?
http://hos.ac/slides/
から引用http://d.hatena.ne.jp/ku-ma-me/20100724/p1
Post
の対応問題ゲーム「
2
つのCFG
の共通部分が空か?」は 決定不能A a 1 A “1”
| a 2 A “2”
...
| a r A “r”
| “$”
B b 1 B “1”
| b 2 B “2”
...
| b r B “r”
| “$”
CFG
とCFG
の共通部分の空判定は決定不能 CFG
とCFG
の共通部分はCFG
では書けない
空判定はできるので、Post
の対応問題が解けちゃう CFG
が文字列全部を表してるかの判定は 決定不能 (証明略) CFG
の補集合はCFG
で書けない CFG =? CFG
やCFG ⊆ ? CFG
は決定不能 CFG
とDFA
の共通部分は計算可能 CFG ⊆ ? DFA
は判定可能演算はあまりサポートしない
【応用事例】
文字列検索/解析以外への
和集合や文字列結合を使って、演算結果の型を計算
集合の包含判定を使って、型キャストのチェック文字列の「型」に使う
string<(a|b)(a|b)*> s;
s = “”; //
コンパイルエラー!s = “c” ; //
コンパイルエラー!string<(a|b)(a|b)(a|b)*> t = s; //
エラー!string<(a|b)(a|b)(a|b)*> u = s+s; //OK
!プログラムが正しい順で 関数を呼ぶことの検証
File file = new File(“input.txt”);
solve(file);
void solve(File file) { int T = file.ReadInt();
solveCases(T, file);
}
void solveCases(int N, File file) { if(N == 0) file.Close();
else {String s = file.ReadLine();
// solve here...
solveCases(N-1, file); }
}
プログラムが正しい順で 関数を呼ぶことの検証
File file = new File(“input.txt”);
solve(file);
void solve(File file) { int T = file.ReadInt();
solveCases(T, file);
}
void solveCases(int N, File file) { if(N == 0) file.Close();
else {String s = file.ReadLine();
// solve here...
solveCases(N-1, file); } }
MAIN ::= “new” SOLVE
SOLVE ::= “readint” CASES
CASES ::= “close” | “readline” CASES
プログラムの挙動
プログラムが正しい順で 関数を呼ぶことの検証
File file = new File(“input.txt”);
solve(file);
void solve(File file) { int T = file.ReadInt();
solveCases(T, file);
}
void solveCases(int N, File file) { if(N == 0) file.Close();
else {String s = file.ReadLine();
// solve here...
solveCases(N-1, file); } }
MAIN ::= “new” SOLVE
SOLVE ::= “readint” CASES
CASES ::= “close” | “readline” CASES
“new” (“readint” | “readline”)* “close”
プログラムの挙動 ファイルは
この順で 操作しないとダメ!
プログラムが正しい順で 関数を呼ぶことの検証
File file = new File(“input.txt”);
solve(file);
void solve(File file) { int T = file.ReadInt();
solveCases(T, file);
}
void solveCases(int N, File file) { if(N == 0) file.Close();
else {String s = file.ReadLine();
// solve here...
solveCases(N-1, file); } }
MAIN ::= “new” SOLVE
SOLVE ::= “readint” CASES
CASES ::= “close” | “readline” CASES
“new” (“readint” | “readline”)* “close”
CFG ⊆ ? DFA
は決定可能
⊆ ?
自然数の集合を表す
(2
進数表記を下位ビットから並べた文字列として)
SG G
0 1
0
SG
1
1 G 0
0
1 0
「2の倍数」
「3の倍数」
自然数のペアや
n
個組の集合(n
ビットを一文字として)
S
なんでも
0 1
G 0 1
1 1
「偶数と、3 mod 4 の数のペア」
(
16, 7
)→ (10000, 00111)
→ “00001
11100”
Automaton
で自然数の
大小関係
足し算
固定幅ビットシフト
定数でのmod
などが定義できる
いろいろな演算
もちろん
和集合
共通部分なども・・・
こんな集合が表現できます。幾何!
まとめ
まとめ
文字列の(
無限)
集合の幾つかの表現方法を紹介しました
有向グラフで表現する
論理式で表現する
パターンで表現する
文法で表現する
色々な集合演算の
実装方法
応用を紹介しました。
まとめ