【随時追記予定】読書メモ:関数プログラミング 珠玉のアルゴリズムデザイン
「関数プログラミング 珠玉のアルゴリズムデザイン」を買いました。
「関数プログラミング 珠玉のアルゴリズムデザイン」買った。二章まで読んだけど読み応えあってよい感じ。 pic.twitter.com/tGSm0ceW0E
— xenophobia (@xenophobia__) 2014, 11月 14
読みながらツイートしてたら
珠玉のアルゴリズムデザイン、おれが買う頃には @xenophobia__ さんがわかりにくいと思ったところを記事にまとめてくれてて、それを見ればスムーズに読み進められるんだろうなー!
— とむらっきー (@masquerade0324) 2014, 11月 14
というフリを受けたので、理解を深めるためにもメモを書くことにします。
随時更新していきますが、とりあえず3章まで。
(11/19追記)サポートサイトがあるようです。なんとこの記事にリンクを貼っていただきました。ありがとうございます。
関数プログラミング 珠玉のアルゴリズムデザイン
【更新履歴】
2014/11/19: 6章まで更新。あと見た目を多少改善
2015/5/4: 7章を更新。
1章「最小自由数」:
最初の章だけあって平和だった。
強いていえばaccumArray関数の説明がわかりにくかったことぐらい。
なんかこんな感じに挙動の例があれば一瞬で理解できたと思う:
accumArray (+) 0 (1,5) [] = array (1,5) [(1,0),(2,0),(3,0),(4,0),(5,0)] accumArray (+) 0 (1,5) [(1, 3), (5, 6), (1, 2)] = array (1,5) [(1,0+3+2),(2,0),(3,0),(4,0),(5,0+6)] = array (1,5) [(1,5),(2,0),(3,0),(4,0),(5,6)]
2章「上位者問題」:
運算を追っていたらウッとなりました。一部の行間がデカいです。
まぁ手を動かせばいい話なんですが。
[(z, scount z zs) | z:zs <- map (++ys) (tails xs) ++ tails ys] = {- <- を++上で分配 -} [(z, scount z (zs ++ ys)) | z:zs <- tails xs] ++ [(z, scount z zs) | z:zs <- tails ys]
これはつまり
[(z, scount z zs) | z:zs <- map (++ys) (tails xs) ++ tails ys] = {- <- を++上で分配 -} [(z, scount z zs) | z:zs <- map (++ys) (tails xs)] ++ [(z, scount z zs) | z:zs <- tails ys] = [(z, scount z zs') | z:zs <- tails xs, let zs' = zs ++ ys] ++ [(z, scount z zs) | z:zs <- tails ys] = [(z, scount z (zs ++ ys)) | z:zs <- tails xs] ++ [(z, scount z zs) | z:zs <- tails ys]
ということ。
今後もこれぐらいは瞬時に脳内補完できるような運算力が求められてくるのだろうか。
3章「鞍型探索の改良」:
p12. Theoの発言
最悪の場合でをおよそ回計算し, 最良の場合では回になります.またはのどちらかはよりも十分小さくなりうるので, たとえば, であれば最悪の場合でステップしかかからないアルゴリズムになりますね
これは全く何を言っているのかわからなかったし、今でも理解できない。
理解できないというのは、「またはのどちらかはよりも十分小さい」からの話の流れとして意味がわからない、ということ。の場合はももに比べて十分小さいので最悪ケースでもステップという結論自体は正しいと思います。
p14. Anneによるinvert関数の計算量の下界の解析
ここでというのは、二引数狭義単調増加関数の値をにする(、つまりとなる)ようなのリストとしてありうる全ての組み合わせの数。
(つまりたとえばなら、問題「をみたすようなのリストを求めよ」の答えとしては
の6通りが考えられるから、)
を見積もるのはやさしいことです. かつの範囲でとなる対のそれぞれのリストが, の長方形を左上隅から右上隅へいく階段形状と一対一で対応しています. この階段形状内の角部分にが現れます.
ここは難しかった。
まず、この章のテーマでもある鞍型探索アルゴリズムが解を見つけていく挙動をみてもわかるように、をみたすは長方形上に左上から右下に
(をみたすを赤で表示している)
のように並ぶということがわかる。
すると、のリスト一つに対し、その各点が角になるような階段
を対応付けることができる。これは一対一の対応である。
4章「選択問題」:
以下、タイトルに「」が付いていた場合は引用であることを意味するものとします。
「順序付き型の要素の……」
型に順序が付いてる……?とか一瞬思ったけど要はOrd型クラスのインスタンスになるような型のことを言っているらしい。
abideな演算子
縦横不問(abide)というのは初めて聞いたが、要するに
xs U ys ++ ++ ys U vs
と並べたときに
縦、横の順で[xs ++ ys] ∪ [us ++ vs]と計算しても、
横、縦の順で[xs ∪ us] ++ [ys ∪ vs]と計算しても結果は同じ、というイメージで理解した。
「の場合は双対になる」
smallest k (xs ++ [a] ++ ys) (us ++ [b] ++ vs)の結果は(xs ++ [a] ++ ys)と(us ++ [b] ++ vs)とを取り替えても変わらないので、
適当に取り替えることでを仮定できるという意味。
の場合は?
今、unionする集合同士は互いに素であることを仮定している。
5章「組和の整列」:
「この行列は標準Young盤の一例である」
xs,ysが昇順ソート済みなら、[[x y | y <- ys] | x <- xs]の各行・各列は明らかに昇順だから標準Young盤(ヤング図形 - Wikipedia)になっている。ここでは、xs及びysは集合(なので要素に重複がない)という前提が使われている。(重複がもしあれば半標準(semi-standard)というべき)
比較回数の下界
はソートしたい組和が並ぶ行列の各要素にインデックスを割り振る方法の数なので、ソートの出力は通りありうる。
あとは一般的な比較ソートの下界の議論と同じ。
tableの意味
xs、ysを順序集合の要素のリストとする。
table xs ys :: [(Int, Int, Int)]の中の(1, i, j) < (2, k, l)という2つの要素を見ることで得られる情報は、
(xsのi番目の要素)(xsのj番目の要素) (ysのk番目の要素)(ysのl番目の要素)
つまり
(xsのi番目の要素)(ysのk番目の要素) (xsのj番目の要素)(ysのl番目の要素) ((5.2)より)
である。
したがって、2つの組差とを比較するにはtable xs ysの対応する要素の大小を参照すればよく、配列に変換してしまえばこれは定数時間でできる。
ここで重要なのは、目的は上の比較回数を減らすことであるということ。当然、tableを使ってもソートにはかかるが、その際の比較演算は自然数上の比較演算なので、
必要な上の要素の比較はtableを構築する際に必要な分だけということになる。
6章「小町算」:
正格関数
divergence: を適用した結果が発散する関数を正格関数という。
Haskellなら「undefinedを適用したときに*** Exception: Prelude.undefinedを吐かない」と言いかえてもいいはず。
filter f: [a] -> [b]はまずリストをパターンマッチして次の挙動を決めるのだから、自明に正格。
(6.3)は(iii)の具体化
(6.3)が
filter (ok . value) . extend x = filter (ok . value) . extend x . filter (ok . value)
f = filter (ok . value), g = extend, h = extend' = filter (ok . value) . extendを代入して
f . (g x) = h x . f
両辺にyを適用
f (g x y) = h x (f y)
これが条件(iii)に一致。
(6.10)の導出
なんでこれだけ導出載ってないんだろう。「自明」ってことだろうか。
filter (p . snd) . map (fork (f, g)) = {- filter p . map f = map f . filter (p . f) より -} map (fork (f, g)) . filter (p . snd . fork (f, g)) = {- (6.5) snd . fork (f, g) = g より -} map (fork (f, g)) . filter (p . g)
glue(一つめの定義)の意味
glue 1 "23×4+56" = "123×4+56" ++ "1×23×4+56" ++ "1+23×4+56"
のような「式の一番前に数字を一つ加える」挙動が、
glue 1 [[[2,3],[4]],[[5,6]]] = [[[1,2,3],[4]],[[5,6]]] ++ [[[1], [2,3],[4]],[[5,6]]] ++ [[[1]], [[2,3],[4]],[[5,6]]]
とリスト上の操作で実現されている。
7章「最小高さ木の構築」:
長らく更新がなかったのは普通にこの章が難しいからです。
誤りがあるかもしれませんのでこの章に関するツッコミは特に歓迎です。
この章を難しくしている最大の要因は、『木(Treeデータ型)』に対して定義されている関数と『森(Forestデータ型)*1』に対して定義されている関数が同名で、
かつそれらが文脈によってフレキシブルに入れかわるという点だと思います。後述するようにそれらは実質同じものなのでそうすることで議論の本質を外してしまうことはありませんが、
どちらがどちらなのかを注意して読まないとどっかで置いていかれることもあるかと思います。
prefixes関数(Tree版)
prefixes x tで「tの一番左の葉としてxを挿入した場合の木のリスト」を返すとあるが、
これは「周縁の最左要素がxとなるようにtにxを挿入した場合の木のリスト」という意味である。
つまりは木の周縁(fringe)を返す関数fringeを
fringe :: Tree -> [Int] fringe (Leaf x) = [x] fringe (Fork u v) = fringe u ++ fringe v
と定義したとき、以下の性質
t' ∈ prefixes x t ⇒ fringe t' === x ++ (fringe t)
を満たすということ。
left spine(木の左背骨)
二分木の定義
data Tree = Leaf Int | Fork Tree Tree
より、二分木は必ずFork (Fork (...(Fork (Leaf x) )... ) という形をしている。
このとき[Leaf x]を元の木の左背骨という。これは森である。
後に、左背骨を求める関数spineが登場する。
この左背骨をrollup = foldl1 Forkで一つの木にすると元の木に戻る。この操作を巻き上げという。
木の左背骨は一意に決まるので、これをもって左背骨(=最左要素が葉であるような森)と木とに一対一対応がつけられる。
prefixesの再定義
これまで木に対して各関数を考えてきたが、左背骨(:: Forest)と木の間に一対一対応がつけられることがわかった。
また、森の各木の周縁を同じ順で連結すると、その森を巻き上げた木の周縁になる、つまり
concatMap fringe forest === fringe $ rollup forest
成り立つ。
したがって、prefixes x tsを「(巻き上げた後にできる木の)周縁の最左要素がxとなるように森tsにxを挿入した場合の森のリスト」と再定義できる。
非決定的な(nondeterministic)関数
ここでいう非決定的とは、プログラム中に非決定性(nondeterminism)が含まれているという意味ではなく、
仕様だけでは出力が一意に決まらないという意味。
これとは別に、定義に乱数やインプットが含まれているようなプログラムをnondeterministicと呼ぶケースもある。
*2
この先の議論にはminBy costの定義がしっかりと与えられている必要はなく、
あくまで「costが最小の木を返す」という仕様が満たされていればよいので、定義を一部曖昧なままにした、と捉えたほうがいいかもしれない。
精緻化関係 上の運算
非決定的な関数にもプログラム運算が適用できるように精緻化関係上の運算が導入される。
つまり、精緻化関係の右辺が出力しうる値は左辺でも出力されうることが保証されている。
たとえば、anyOf :: [Int] -> Intを
anyOf [n1, n2, ..., nm] ∈ {n1, n2, ..., nm} {- n1, ..., nm のどれかをランダムに(非決定的に)出力 -}
と定義すると、anyOf (l1 ++ l2) anyOf l1 のような関係が成り立つ。
たまにを指してと書かれていることがあるっぽい?*3
融合変換の章
個人的にはここの話の流れが掴みづらい。
全体として「cost関数で順序付けをすると融合条件等を考えづらいので、cost'を使う」みたいな話になっている。
色々なことが省略され、行間に消えてしまっているので、補いつつ読むしかない。
cost', costsについて
costは木のコストで、cost'は木の左背骨に沿ってコスト情報を拾うことで得られるリストである。
cost' tの最左要素はcost tなので、cost'で最小の要素はcostで最小の要素でもある。
またcosts = map cost . reverseは、左背骨のコストを定義していると思ってよい。
cost' = costs . spineなので、costs = cost' . rollupである。つまり、costs ts < costs ts'ならばtsを左背骨にもつ木はts'を左背骨にもつ木よりコストが小さい。
insertについて
p40の最下部にある運算では、insertの規定が
minBy costs . prefixes x insert x
になっている。元々示されていたinsertの規定はminBy cost . prefixes x insert xなので、型がInt -> Tree -> TreeからInt -> Forest -> Forestに変わっている。
つまり、insert xは「森に対し、コストが最小になるようにxを挿入した森(のうちのどれか一つ)を返す」という関数。実際には一般の森ではなく左背骨に対して挿入が行われる。
以下、insertはInt -> Forest -> Forestの版として議論します。
以下、議論に必要なのでInt -> Tree -> Tree版のinsertをinsert'で表します。
insertとinsert'の間には次のような関係が成り立ちます。
(insert-rel): insert' x (rollup ts) = rollup (insert x ts)
p40最下部の運算で行われている「融合変換(fusion)」について
minBy costs . foldrn (concatMap . prefixes) (wrap . wrap . Leaf) foldrn insert (wrap . Leaf)
が単に「minBy costs . prefixes x insert xを用いて融合変換すると……」と書かれている。
これも要するに全てのxsについて
minBy costs (foldrn (concatMap . prefixes) (wrap . wrap . Leaf) xs) foldrn insert (wrap . Leaf) xs
ということだと思うのでそのつもりで解説する。
foldrnの融合変換を上の形で使うには、以下の2つが成り立たなければならない。
- 任意のxについて minBy costs ((wrap . wrap . Leaf) x) (wrap . Leaf) x
- (7.1'): 任意のx, fs, ts'について minBy costs fs ts' ならば minBy costs (concatMap (prefixes x) fs) insert x ts'
前者は要素が一つしかないので自明。後者(7.1')についてはp39に倣っていろいろ言う必要がある。
ここが完全に省略されているのは多分「前のページと同じようにできるので」的な意味合いなのだと思う。
まず、以下に示す(7.2')が成り立てば(7.1')が成り立つ。(★)
(7.2') minBy costs fs ts minBy costs (map (insert x) fs) insert x ts
(★)を示すには、p39の最下段にある運算と全く同じことをすればよいです。
{- concatMapを展開 -}
= minBy costs (concat (map (prefixes x) fs))
= (minBy costs . concat) (map (prefixes x) fs)
{- minBy costs . concat = minBy costs . map (minBy costs) -}
= (minBy costs . map (minBy costs)) (map (prefixes x) fs)
= minBy costs . (map (minBy costs) (map (prefixes x) fs))
{- map f (map g xs) = map (f.g) xs -}
= minBy costs . (map (minBy costs . prefixes x) fs)
{- f f' ならば map f map f' かつ g.f g.f' -}
minBy costs . (map (insert x) fs)
で、(7.2')が言えるためには単調性条件が言えて欲しいが、(7.2)とは違い、ここでは(7.4)が成り立つならば以下の単調性条件(7.3')が成り立つことが言える。
(7.3'): costs us costs vs costs (insert x us) costs (insert x vs)
なぜならば、costs us costs vs cost' (rollup us) cost' (rollup vs)であり、
また、これと(insert-rel)より
costs (insert x us) costs (insert x vs)
cost' (rollup (insert x us)) cost' (rollup (insert x vs))
cost' (insert' (rollup us)) cost' (insert' (rollup vs))
なので、(7.3')と(7.4)は同値であるからである。
すなわち、(7.4)が言えれば(7.2')、(7.1')が言えるから融合変換が可能になる。
結局、insert :: Int -> Forest -> Forestが実装でき、(7.4)が}成り立てば融合変換が可能ということになり、p41冒頭の主張に繋がる。
単調性条件について
なぜ(7.3)ならば(7.2)が成り立つのか、について。
(7.3)は関数 insert x が順序関係に関して単調であるという条件であり、
これが成り立つならばtsでに関して最小の要素はt_min = [insert x t | t<-ts]でに関して最小の要素はinsert x t_minとなるので、(7.2)が成り立つこととなる。
上述した(7.2')⇒(7.3')も同じ議論で導ける。
「(7.4)から……が導けることをこのあと示す」
ここが全くわからない。どう見てもこの後に導出があるようには見えないし、そもそもこの先の議論で必要な事実ではない。
訳出ミスでもなさそう。情報求む。
j+1 k nの範囲で
「観察によって確かめよ」とあるが、以下の事実に着目するとわかりやすい。
, の定義より、がより真に増加するのは、
がより真に増加し、かつその値がより大きいとき、つまり
のときである。
つまり、ならばは増加しないことになり、当然kがj+1より大きい範囲では増加しようがない。
最適なinsertの方針
p42冒頭では、「jが(7.5)を満たし、かつijなるiも(7.5)を満たすなら、(下から数えて)i番目に挿入したほうがj番目に挿入するよりコストが低くなる」
という事を述べている。これはつまり、コスト最小を目指すなら(7.5)を満たす範囲でなるべく低い位置に挿入すべきだということを示している。
これで、最適な挿入を実装する方針が固まったことになる。