情報科学」内容案 データと計算(2)

「情報科学」(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を同じ数だけ含む文字列のみを受理する有
限オートマトンは存在しない