「情報科学」(4) データと計算(2) と 有限オートマトン 目次 • 写像と計算 有限要素の写像・無限の要素間の関係,再帰, メソッド定義 • データ構造の操作 再帰的データ構造の操作 • 関数から状態遷移へ 繰り返し構文 • 有限オートマトン 復習: 計算とは写像である 入力空間 出力空間 入力が組 だと考える • • 例: m月の日数, 「椿姫」の作曲者, 電話帳, じゃんけんの手がx, yのときの勝者 有限個の要素間の関係は表で表せる => "21000", (cf. 連想メモリ) directory = {"総長" "学部長" => "46001", Ruby "守衛" => "46666"} 表では表せない写像もある 入力空間 出力空間 • 入力空間に無限の要素が ある関係も表したい 例: xの自乗,xとyの和,n角形の対角 無限個…コン 線の数,身長heightで体重weightの ピュータが扱う整 人のBMI 数のようなデータ 無限に大きな表が必要になる • 無限の要素を扱う演算が予め用意されて は厳密には有限 だが,全ての整 いるので,それを使えば一部は表せる 数についての関 xの自乗: x*x,xとyの和: x+y 係を書き並べるこ • 無限の要素を扱う演算を組み合わせれ とは難しいので事 ば,さらに多くの関係を表せる 実上無限 対角線: (n−3)*n/2, BMI: weight/height**2 復習: 計算は関数で表される • 関数(メソッド)によって,引数が変化する場 合の出力を「式」として書ける 条件分岐などを使うとより複雑な計算が書ける def diagonals(n) Ruby (n-3)*n/2 n角形の対角線の数 end def bmi(height, weight) weight/height**2 身長heightで体重weightの人のBMI end n, height, weight がとり得る値は 事実上無限 演算の組み合わせだけでは 表せない計算もある • • 式の形が固定されていないような計算は,演算の 組み合わせだけでは表せない 例: nの階乗 (1×2×…×n) 掛け算だけで計算できるが,掛け算を使う回数は一定で はない コラッツ予想:「ある数が偶数のときは半分に,奇数のと きは3倍して1を足す」を繰り返すといずれ1になる xが1になるまでの繰り返し回数 • 5→16→8→4→2→1 …5回 再帰: 無限の計算を有限で表す • • 再帰: ある関数を定義するときに,その関数自身 を使って定義すること 例: nの階乗 f(n) は f(n–1)*n 厳密にはn=0のときは別に定義するので f(0) = 1 f(n) = f(n – 1)*n (n > 0のとき) n 1 n=0の場合 n>0の場合 1 – * f = f の定義 再帰による定義の例(2) • コラッツ予想「ある数nが偶数のときは半分に,奇数のときは 3倍して1を足す」を繰り返すといずれ1になる nが1になるまでの繰り返し回数c(n) • 場合分け+再帰で定義できる c(1) = 0 c(n) = c(n/2) + 1 c(n) = c(3*n + 1) + 1 未解決問題: 1になること は証明されていない! (1は0回で1に到達する) (nが偶数のとき) (nが奇数のとき) 例:c(5)の定義は: 5は奇数なので16になる。16から1になる回 数をc(16)とするとc(5)=c(16)+1 再帰的な計算を メソッドにする Ruby • 場合分けされた関数を一つのメ ソッドにする • 場合分けはif then elseを使う • プログラミング言語によっては 再帰的な関数を定義するため の特別な方法がある(letrec) f(0) = 1 f(n) = f(n–1) * n (n > 0のとき) c(1) = 0 c(n) = c(n/2) + 1 (nが偶数のとき) c(n) = c(3*n+1)+1 (nが奇数のとき) def f(n) if n == 0 1 else f(n-1)*n end end def c(n) if n == 1 0 elsif is_even(n) c(n/2) + 1 else c(3*n+1) + 1 end end いろいろ考えてみよう • • • • n番目のフィボナッチ数 nCm (n個からm個選ぶ組み合わせ数) k = 1…n の最大のc(k) 「ある数が偶数のときは半分に,奇数のときは3倍 して1を足す」を繰り返すといずれ1になるとしたと き,nが1になるまでに現われた数の最大値 • 素数の判定(nが素数ならtrueとなる関数) • 2…nの中の素数の個数 などなど データ構造に対する計算の例 • 構造の上を渡り歩く 合計を求める,最大値を求める • データ構造を作る/作り換える 整数を素因数分解して素因数のリストを作る 小さい順に整列する 整列されたリストに値を追加する リストの逆順のリストを作る 木構造データを 作る式の例 / − 3 * y + 1 5 * x x ** 2 Expr.new( Expr.new( Expr.new(3, :*, Expr.new(:x, :**, 2)), :-, Expr.new(5, :*, :x)), :/, Expr.new(:y, :+, 1)) 再帰的データ構造に対する計算 • (再帰的)データ構造も整数と同様に値と考えることができる • 再帰的な関数によって素直に書けることが多い • 例: 整数の木tに現われる数の合計を求めるsum(t) sum( 演算子(*とか/)は無視し,変数は0とみなすことにしよう 木のsum = (左の子のsum)と(右の子のsum)の和になる 場合分けが必要: tが木の場合, 変数の場合, 数の場合… / − 3 * y + 1 5 * x x ** 2 ) sum(3 * − ) + sum( 5 * x x ** 2 sum( y + 1 ) sum(y) + sum(1) sum(y) 0 sum(1) 1 y + 1 ) Ruby 再帰的データ構造に 対する再帰的な計算 Expr = Struct.new(:left, :operator, :right) def sum(t) if t.kind_of?(Expr) sum(t.left) + sum(t.right) elsif t.kind_of?(Integer) 再帰 t tは整数なので else そのまま返す 0 end tがそれ以外, end つまり変数のときは0 第2週で 定義した「式」 tの値が構造体 Exprのときtrue 左右のsumの 合計(再帰) tの値が整数の ときtrue 再帰的なデータ構造を扱う関数 • 木の左右を逆転させる / mirror( − 3 * y + 1 5 * x x ** 2 )→ / − 1 + y * 3 x * 5 2 ** x def mirror(t) 再帰的に子を反転させる if t.kind_of?(Expr) Expr.new(mirror(t.right), t.operator, mirror(t.left)) else t 新しくノードを作 Ruby end るのがミソ end いろいろ考えてみよう • • 木の「大きさ」を求める関数 (ノード,変数,数を「1個」と数 える) 木の中の変数を数に置き換える関数 / subst(:x,8, − / y + 1 3 * 5 * x )→ 3 * x ** 2 • • − y + 1 5 * 8 8 ** 2 変数の無い木を「計算」する関数 木を文字列にする関数 string( / − 3 * y + 1 5 * x x ** 2 ) →"(((3*(x**2))-(5*x))/(y+1))" 関数の計算過程 • 計算 = 式の書き換え という考え方 方針: 式の中で計算できる演算があったら,そこを結果で置きかえる 関数が使われていたら,関数の定義をそこに展開し,変数を 引数で置きかえる 特徴:手で計算する方法に近い どこを書き換えるかが難しい 無駄な計算が生じる場合がある • 計算 = 状態の変更 という考え方 方針: 変数の値を書き換えてゆくことで計算が進行 関数呼出は,現在の計算状態(変数の値)を退避して関数の中 を実行 特徴:実際のプログラミング言語の実行方式 無駄が少ない 式の書き換えによる計算過程 • 計算できる部分式を結果 に書き換え • 関数は関数の定義に書き 換える その際,変数を引数で置き かえ • 書き換えできる場所がなく なったら終了 f(n) if n=0 then 1 else f(n−1)*n f(3) if 3=0 then 1 else f(3-1)*3 f(2)*3 (if 2=0 then 1 else f(2-1)*2)*3 (f(1)*2)*3 ((if 1=0 then 1 else f(1-1)*1)*2)*3 ((f(0)*1)*2)*3 (((if 0=0 then 1 else f(0-1)*0)*1)*2)*3 (((1)*1)*2)*3 ((1)*2)*3 (2)*3 6 計算 = 状態の変更 def f(n) if n == 0 1 else f(n-1)*n end end 現在のnを退避して新 →f(3)の計算: n=3とする たにn=2とする if 文を実行 f(n−1)*nを実行 → f(2)の計算: n=2と する どこを実行 → f(1)の計算: n=1 if 文を実行 中か覚えて とする f(n−1)*nを実行 おく if 文を実行 f(n−1)*nを実行 覚えておい た場所から → 再開 ← 1*1=1を返す ←2*3=6を返す ← 1*2=2を返す ← nの値を復活させるのでn=3 f(0)の計算: n=0と する if 文を実行 1を返す 関数から状態遷移へ: 関数の定義のバリエーション • 一つの値を求める関数にも色々な定義がある 例: nの階乗 f(n) f(n) = f '(n,n) f '(1,g)=g f '(k,g)=f '(k−1,(k−1)*g) 補助関数f '(k,g)は 1*2*…*(k−1)*gを求める関数 補助関数f ''(k,g,n)は g*(k+1)*(k+2)*…*nを求める関数 f(n) = f ''(0,1,n) f ''(n,g,n)=g f ''(k,g,n)=f ''(k+1,(k+1)*g,n) 関数から状態遷移へ: 末尾再帰 • 関数の定義が,別の関数に丸投げしている ようなものを末尾再帰という f(n) = f'(n,n) f '(1,g)=g f '(k,g)=f '(k−1,(k−1)*g) f 'に丸投げ→ 末尾再帰 f(0) = 1 f(n) = f(n – 1)*n (n > 0のとき) fの値に手を加えている→ 末尾再帰ではない 関数から状態遷移へ: 状態遷移による計算 • 状態遷移による計算: 状態を 刻一刻変化させてゆき,最終 的に求める値に到るやり方 状態: 変数と値の結びつき全 体 状態は時間で変化 メモリの内容を状態と思うと, コンピュータの基本的な動作 • ある種の関数(末尾再帰的な 関数)の計算過程は,状態を 変化させてゆくものとも見倣 せる f(n) = f '(n,n) f '(1,g)=g f '(k,g)=f'(k−1,(k−1)*g) f(4) → f'(4,4) → f'(4−1,(4−1)*4) → f'(3,12) → f'(3−1,(3−1)*12) → f'(2,24) → f'(2−1, (2−1)*24) → f'(1,24) → 24 k g 4 4 3 12 2 24 1 24 状態の時間変化 状態遷移による計算の定義方法 • 考えるべきこと: 状態: 初期値: 遷移: 終了条件: 値の組 計算を始めるときの状態 繰り返し1回ごとの状態の変化させ方 繰り返しを止める条件と,最終的な解 • 計算過程: 初期値から終了条件が成り立つまで状態を遷移させる 終了条件が成り立ったら終わり • 例: nの階乗 状態: 初期値: 遷移: 終了条件: (k,gk) の組 ただし gk=k*(k+1)*…*n のような値 (n,n) (k,gk)→(k−1, (k−1)*gk) k=1になったらそのときのgkが解 Ruby 状態遷移を使ったプログラム 状態のために変数を用意 • [状態](k,gk) の組 def f_state(n) 初期値をセット • [初期値] (n,n) k=n g=n 「k>1の間, • [遷移] (k,gk)→ while k>1 end (k−1, (k−1)*gk) g = g * (k-1) までを繰り • [終了条件] k=1 (そ k = k-1 返せ」とい end う構文 のときのgkが解) g end k>1でなく なった; i.e., k=1のとき 状態の変化 (順序に注 意) Ruby • 繰り返し構文 while文 ―― 条件が成り立っている間の繰り返し 式がtrueである間, while 式 「何か」を計算 何か まず条件をチェックする end バリエーション: まず「何か」を 計算するもの(repeat until) • for文(DO文) ―― 決められた回数の繰り返し iをx, x+1, x+2, …, yと 変えて「何か」を計算 whileでも実現できる for i in x..y 何か end 状態遷移による計算の定義例 コラッツ予想 c(1) = 0 c(n) = c(n/2) + 1 (nが偶数のとき) c(n) = c(3*n+1)+1 (nが奇数のとき) • [状態](k,ck) の組 kに至るまで • [初期値] (n,0) がc k 回 • [遷移] (k,ck)→ if kが偶数 then (k/2, ck+1) else (k*3+1, ck+1) • [終了条件] k=1 (そのときのckが解) def c_state(n) k=n c=0 while k>1 if is_even(k) k=k/2 else k=k*3+1 end c=c+1 end c end 不変量 • 繰り返しの間,変化しない式のこと • 繰り返しの正しさを考える際に必要 • 例: nの階乗の計算の場合 X(k,g)=(g*(k–1の階乗))=n!で不変 k,gの繰り返し1回の後の値をk',g'とする (プログラムからg'=g*(k–1), k'=k–1) X(k',g') = g' * (k'–1の階乗) = g*(k–1)*(k–2の階乗) = g*(k–1の階乗) = X(k,g) X(n,n)=nの階乗なのでk=1のときのgは nの階乗 def f_state(n) k=n g=n while k>1 g = g * (k-1) k = k-1 end g end 考えてみよう • • • 状態・初期値・遷移・終了条件は何か? 状態遷移を使って関数の定義を書き直してみよう 不変量は何か? n番目のフィボナッチ数 nCm (n個からm個選ぶ組み合わせ数) k = 1…n の最大のc(k) 「ある数が偶数のときは半分に,奇数のときは3倍して1を足す」を 繰り返すといずれ1になるとしたとき,nが1になるまでに現われた 数の最大値 素数の判定(nが素数ならtrueとなる関数) 1…nの中の素数の個数 などなど 繰り返しとデータ構造 • 大きさが一定のデータ構造(e.g.,配列)に対 する計算は,繰り返し構文で記述するのが 自然 例: • • • 行列・ベクトルに対する演算 文字列(=文字の配列)検索 表形式データの整列 (次週) Ruby • 繰り返しの例: ベクトル演算 n次ベクトルは大きさnの数値配列で表せる 結果をしまう ベクトルvのスカラー倍 def vector_scale(s, v) 配列を作り w=Array.new() $x = [ 1.0, 0.0, 0.0 ] for i in 0..(v.size-1) $y = [ 0.0, 1.0, 0.0 ] w[i] = s * v[i] end vの各要素に $z = [ 0.0, 0.0, 1.0 ] スカラー倍した w ついて 値をセット end vector_add( x 2( y z ) $x, の計算 ベクトルの和 vector_scale( def vector_add(v1, v2) 2.0, w=Array.new() vector_add($y, $z))) for i in 0..(v1.size-1) w[i] = v1[i] + v2[i] end w end Ruby • 繰り返しの例: 文字列の検索 例: 配列sの中で配列pと一致する部分は何番目? find("uraniwaniwaniwaniwatorigairu", "waniwaniwato") → 9 uraniwaniwaniwaniwatorigairu waniwaniwato 2重の繰り返し Ruby • 繰り返しの例: 文字列の検索 例: 配列sの中で配列pと一致する部分は何番目? sのi番目からとpが一致するか? def match_at(s, p, i) j = 0 while j < p.size && s[i+j] == p[j] j = j + 1 遷移: jを増やす end j == p.size end 状態: pの添字 条件: jがpの終わりまで 到達するかsの(i+j)番目と pのj番目が一致しない jがpの終わりまで 到達していたら一致 pはsの何文字目から出現するか? def find(s, p) i = 0 while !match_at(s, p, i) i = i + 1 end i 簡単のため,最初の出現だけを end find("uraniwaniwaniwani watorigairu","waniwaniw ato") 見つける。また必ず見つかると仮定 有限オートマトン • 状態遷移による計算は,計算が状態空間を 移動してゆくものと考えられる 状態空間 • 有限オートマトンに対応 → 数理的なモデル f'(4,4) →f'(3,12) → f'(2,24) →f'(1,24) → 24 有限オートマトン • 有限個の状態 現在の状態 初期状態 受理状態 • 入力文字列 有限個の文字:アルファベット • 状態遷移 入力文字と現在の状態に従って, 現在の状態を変える 状態遷移図 0 初期状態 二進数を,0と1から 成る文字列として入力し, 3の余りを計算する 有限オートマトン 1 1 0 0 1 011101 0 初期状態 0 1 1 1 0 0 2 1 011101 0 初期状態 0 1 1 1 0 0 2 1 011101 0 初期状態 0 1 1 1 0 0 2 1 011101 0 初期状態 0 1 1 1 0 0 2 1 011101 0 初期状態 0 1 1 1 0 0 2 1 011101 0 初期状態 0 1 1 1 0 0 2 1 011101 0 初期状態 0 1 1 1 0 0 2 1 011101 0 初期状態 0 1 1 1 0 0 2 1 011101 0 初期状態 0 1 1 1 0 0 2 1 011101 0 初期状態 0 1 1 1 0 0 2 1 011101 0 初期状態 0 1 1 1 0 0 2 1 011101 0 初期状態 0 1 1 1 0 0 2 1 011101 0 初期状態 0 1 1 1 0 0 2 1 状態の 番号付け 状態遷移図 0 初期状態 0 二進数を,0と1から 成る文字列として入力し, 3の余りを計算する 有限オートマトン 1 1 1 0 1 0 2 Ruby オートマトンの実装 $automaton = [[0,1], [2,0], [1,2]] def make_transitions(input) s = 0 for i in 0..(input.size - 1) c = input[i] - ?0 ?0で0の文字コード の数値が返る s = $automaton[s][c] ここでは0と1の入力 文字しかないのでc end は0か1 s make_transitions("011001") end make_transitions("011011") make_transitions("011101") $automaton = [[0,1], [2,0], [1,2]] 011101 s==0 0 初期状態 0 1 1 1 0 0 2 1 $automaton = [[0,1], [2,0], [1,2]] $automaton[0][0] 011101 s==0 0 初期状態 i==0 c==0 0 1 1 1 0 0 2 1 $automaton = [[0,1], [2,0], [1,2]] 011101 s==0 0 初期状態 0 1 1 1 0 0 2 1 $automaton = [[0,1], [2,0], [1,2]] $automaton[0][1] 011101 s==0 0 初期状態 i==1 c==1 0 1 1 1 0 0 2 1 $automaton = [[0,1], [2,0], [1,2]] 011101 s==1 0 初期状態 0 1 1 1 0 0 2 1 $automaton = [[0,1], [2,0], [1,2]] $automaton[1][1] 011101 s==1 0 初期状態 i==2 c==1 0 1 1 1 0 0 2 1 $automaton = [[0,1], [2,0], [1,2]] 011101 s==0 0 初期状態 0 1 1 1 0 0 2 1 $automaton = [[0,1], [2,0], [1,2]] $automaton[0][1] 011101 s==0 0 初期状態 i==3 c==1 0 1 1 1 0 0 2 1 $automaton = [[0,1], [2,0], [1,2]] 011101 s==1 0 初期状態 0 1 1 1 0 0 2 1 $automaton = [[0,1], [2,0], [1,2]] $automaton[1][0] 011101 s==1 0 初期状態 i==4 c==0 0 1 1 1 0 0 2 1 $automaton = [[0,1], [2,0], [1,2]] 011101 s==2 0 初期状態 0 1 1 1 0 0 2 1 $automaton = [[0,1], [2,0], [1,2]] $automaton[2][1] 011101 s==2 0 初期状態 i==5 c==1 0 1 1 1 0 0 2 1 $automaton = [[0,1], [2,0], [1,2]] 011101 s==2 0 初期状態 0 1 1 1 0 0 2 1 言語 • アルファベット 有限個の文字の集合 Σなどで表す 例:Σ= {0,1} • 文字列(語) • アルファベットに属する文字の有限列 例:0, 00, 1100, 0101 空列も文字列 εやλで表す 文字列の全体をΣ* で表す (Kleene閉包) 言語(形式言語) 文字列の集合,すなわちΣ* の部分集合 例:3で割り切れる二進数の全体 言語の認識装置としての 有限オートマトン • 状態のいくつかを受理状態とする 受理状態は複数あってもよい • 与えられた文字列に従って,初期状態から始めて 状態遷移を繰り返す 初期状態は一つ • • • 文字列を読み終わった直後の状態が受理状態な らば,その文字列を受理 有限オートマトンMが受理する文字列の全体を L(M)と書く。 L(M)をMの受理言語という。 有限オートマトン 0 初期状態 3で割り切れる二進数の 全体を受理する 有限オートマトン 受理状態でもある。 1 1 0 0 1 Ruby オートマトンの実装 $final = 0 def accept(input) s = make_transitions(input) s == $final accept("011001") end accept("011011") accept("011101") 受理状態は複数あってもよいので,上のコードは不十分 受理状態が複数ある場合に対処せよ(演習) 有限オートマトン 初期状態 0 0 1 1 1 0 0 1 0と1を 奇数個ずつ 含む文字列を 受理する オートマトン 正則表現(または正規表現) • 言語を表す記法 • 文字列のパターンを表す 例:0(10)*1 • • 0で始まり10が繰り返されて1で終わる文字列 そういう文字列からなる言語 例:0(11+00)1 • 0の次に11または00が来て,1で終わる文字列 例:0(11+00)*1 • この意味を考えなさい 正則表現から有限オートマトンへ • 正則表現⇒有限オートマトン 任意の正則表現に対して,その言語を受理する非決定 的な有限オートマトンが存在する • 決定化 非決定的な有限オートマトンは,受理言語が同じ決定 的なオートマトンに変換できる。 • 最小化 決定的なオートマトンは,受理言語をかえずに状態数 が最小化できる。 • • 実は,有限オートマトン⇔正則表現 正則言語:正則表現で記述できる言語 = 有限オートマトンで受理できる言語 非決定的な有限オートマトン ε遷移: 文字を入力せずに 遷移できる ε 初期状態 0 0 0 1 1 ε 1 0(11+00)*1を受理する 非決定的な有限オートマトン 受理状態に至る遷移のしかたが 一つでもあればよい。 0 初期状態 0 0 0 1 1 1 1 1 決定的な有限オートマトン 初期状態 0 0 0 0 1 1 1 決定的な有限オートマトン 初期状態 0 0 0 0 1 1 1 1 0 0 1 1 状態数最小の有限オートマトン 0 初期状態 0 0 1 1 1 1 0 0 1 状態数最小の有限オートマトン 0 初期状態 1 0 2 0 0 1 1 1 3 1 0 0 1 4 Ruby オートマトンの実装 $automaton = [[1,4], [2,3], [1,4], [4,1], [4,4]] $final = 3 accept("01100111") 正則表現は /^ と $/ で囲む accept("0110111") 構文は説明と少し異なる 実はRubyでは... 文字列が正則表現に属するか? /^0(11|00)*1$/ =~ "01100111" /^0(11|00)*1$/ =~ "0110111" 有限オートマトンの性質 • 正則言語の合併,交わり,補集合,連接,Kleene閉 包などに関して閉じている たとえば,合併に関しては,任意のM1とM2に対して, L(M1)∪L(M2)=L(M)となるMが存在することが証明できる • 二つのオートマトンが受理する言語が同じかどうか を判定することができる しかし,数を数えられない:有限オートマトンの限界 例えば,0と1を同じ数だけ含む文字列のみを受理する有 限オートマトンは存在しない
© Copyright 2024 ExpyDoc