木の数え上げ
code:Tree.hs
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE NPlusKPatterns #-} module Tree where
import Data.Bool (bool)
import Data.List (genericIndex, genericTake, findIndex, scanl')
import Data.List.NonEmpty (NonEmpty(..))
import Numeric.Natural
import Control.Comonad.Cofree (Cofree(..))
import Data.Functor.Foldable
import Data.Functor.Base hiding (head, tail)
木の定義
ここで扱う木は以下のように定義した全二分木です.以降この記事では単に木と呼ぶことにします.
(コンテナではなく構造だけであることに注意してください)
code:Tree.hs
data Tree
= Leaf
| Tree :^: Tree
deriving (Eq, Show)
定義に再帰的な構造があるので,再帰スキームを入れておきましょう.
code:Tree.hs
data TreeF r
= LeafF
| r :^^: r
deriving (Eq, Show)
instance Functor TreeF where
fmap f = \ case
LeafF -> LeafF
x :^^: y -> f x :^^: f y
type instance Base Tree = TreeF
instance Recursive Tree where
project = \ case
Leaf -> LeafF
s :^: t -> s :^^: t
instance Corecursive Tree where
embed = \ case
LeafF -> Leaf
s :^^: t -> s :^: t
木の構成
含まれる分岐の数をパラメータとして構成することを考えましょう.0個の分岐を含む木は葉だけです.n + 1個の分岐を含む木は,根での分岐の左にl個の分岐をもつ木を,右にr個の分岐をもつ木を,それぞれ含むとすると,l + r = nが成り立ちます.そこで自然数nの2分割をsplitsで列挙し,それを使って構成します.
code:Tree.hs
{- -- 後で改良
trees = \ case
-- -}
splitsの定義は以下のとおり.
code:Tree.hs
splits = (zip <*> reverse) . enumFromTo 0
全ての木を列挙する
これで全ての木を含むリストallTreesは以下のように定義できます.
code:Tree.hs
toIndex :: Tree -> Natural
toIndex = head . flip genericFindIndices allTrees . (==)
allTrees = concatMap trees 0 .. 列挙ができたので,すべての木に自然数を付与できます.
code:Tree.hs
{- -- 後で改良
toIndex :: Tree -> Natural
toIndex = head . flip genericFindIndices allTrees . (==)
where
genericFindIndices :: (Integral n) => (a -> Bool) -> a -> n genericFindIndices p xs = [ i | (x, i) <- zip xs 0 .., p x ] fromIndex :: Natural -> Tree
fromIndex = genericIndex allTrees
-- -}
木を数える
すべての木を列挙しているので,その構成方法から考えることができます.数える方も木に含まれる分岐の数をパラメータとしましょう.
code:Tree.hs
{- -- 次で変更
countTrees :: Natural -> Natural
countTrees = \ case
0 -> 1
-- -}
所定数の分岐を含む木が数え上げられていることは明らかなコードです.しかし,重複を含むいわゆる樹状再帰になっているので,計算は恐しく非効率です.こういうときは,メモ化法を使うの1つの手段でしょう.メモ化を使いやすいようにcountTreesの定義を見直しましょう.
code:Tree.hs
{- -- 後で改良
countTrees :: Natural -> Natural
countTrees = count . downFrom
downFrom = \ case
n+1 -> enumFromThen (n+1) n
count = \ case
_:[] -> 1
_:xs@(_:_) -> sum $ zipWith (*) <*> reverse $ map countTrees xs
-- -}
これで downFrom をAnamorphismのインスタンス,countをHistomorphismのインスタンスとして定義できれば,countTreesはDynamorphismのインスタンスとなります.
code:Tree.hs
countTrees :: Natural -> Natural
countTrees = count . downFrom
downFrom :: Natural -> NonEmpty Natural
downFrom = ana psi
where
psi :: Natural -> NonEmptyF Natural Natural
psi = \ case
0 -> NonEmptyF 0 Nothing
n+1 -> NonEmptyF (n+1) (Just n)
count :: NonEmpty Natural -> Natural
count = histo phi
where
phi :: NonEmptyF Natural (Cofree (NonEmptyF Natural) Natural) -> Natural
phi = \ case
NonEmptyF 0 _ -> 1
NonEmptyF n (Just ns) -> sum $ zipWith (*) <*> reverse $ xs
where
xs = taking n ns
taking :: Natural -> Cofree (NonEmptyF a) b -> b taking = \ case
0 -> const []
n+1 -> \ case
x :< NonEmptyF _ Nothing -> x x :< NonEmptyF _ (Just xs) -> x : taking n xs
木の構成(改良)
countTreesとtreesは同じ構造なのでtreesも同じ手法で改良してしまいましょう.
code:Tree.hs
trees = mkTrees . downFrom
mkTrees :: NonEmpty Natural -> Tree mkTrees = histo phi
where
phi :: NonEmptyF Natural (Cofree (NonEmptyF Natural) Tree) -> Tree phi = \ case
NonEmptyF n (Just ns) -> concat $ zipWith (*^*) (reverse xs) xs
where
xs = taking n ns
ss *^* ts = (:^:) <$> ss <*> ts
Catalan数
分岐数$ nの木の数を$ C_nとすると,
$ C_0 = 1
$ C_{n+1} = \sum_{i=0}^{n}C_i C_{n-i}
でした.一般項は$ nの閉じた式であらわせることが知られています.
$ C_n = \frac{(2n)!}{(n+1)!n!}
$ C_nはCatalan数と呼ばれています.
code:Tree.hs
catalan :: Natural -> Natural
catalan = \ case
0 -> 1
n -> dwnprd 1 (2*n) (n+2) div dwnprd 1 n 2
where
dwnprd a m n | m < n = a
| otherwise = dwnprd (a*m) (pred m) n
また,
$ C_{n+1} = \frac{2(2n+1)}{n+2}C_n
なので,カタラン数列は以下のように構成できます.
code:Tree.hs
catalans = 1 : zipWith rec 0 .. catalans where
rec n cn = (4*n + 2) * cn div (n + 2)
木のインデックス付け
すべての木を列挙するときの方法から考えて,分岐数と,その分岐数の木のなかでの順番との対でインデックスを付けるのが自然です.まずこの方法で木にインデックスを付けてみましょう.
code:Tree.hs
toLocalIndex :: Tree -> (Natural, Natural)
toLocalIndex = cata phi
where
phi :: TreeF (Natural, Natural) -> (Natural, Natural)
phi = \ case
LeafF -> (0, 0)
(m,i) :^^: (n,j) -> (o, k)
where
o = succ (m + n)
k = acc 0 m 0 (m + n) + i * catalan n + j
acc a 0 _ _ = a
acc a p q r = acc (a + catalan q * catalan (r - q)) (pred p) (succ q) r
これを用いてtoIndexを再定義しましょう.
code:Tree.hs
toIndex :: Tree -> Natural
toIndex = l2g . toLocalIndex
where
l2g = \ case
(0,_) -> 0
(o,k) -> genericIndex accCatalans o + k
accCatalans = scanl' (+) 0 catalans
つぎにインデックスから木を再現する方法を改良しましょう.
まず,fromLocalIndex :: (Natural, Natural) -> Tree
code:Tree.hs
fromLocalIndex :: (Natural, Natural) -> Tree
fromLocalIndex = ana psi
where
psi = \ case
(0, _) -> LeafF
(o, k) -> (m, i) :^^: (n, j)
where
cs = accDeconCatalans o
m = spanCount (<= k) cs
n = pred o - m
cn = catalan n
k' = k - genericIndex cs (pred m)
i = bool (spanCount (<= k') (map (* cn) 1 ..)) 0 (m < 2) j = bool (bool (k' - cn * i) k (m == 0)) 0 (n < 2)
accDeconCatalans :: Natural -> Natural accDeconCatalans = scanl1 (+) . deconCatalan
deconCatalan = (zipWith (*) <*> reverse) . flip genericTake catalans
spanCount :: (a -> Bool) -> a -> Natural spanCount p = cata phi
where
phi = \ case
Nil -> 0
Cons x c -> bool 0 (succ c) (p x)
これを用いて,fromIndexを定義します.
code:Tree.hs
fromIndex :: Natural -> Tree
fromIndex = fromLocalIndex . g2l
where
g2l = \ case
0 -> (0, 0)
m@(n+1) -> (o, k)
where
o = pred (spanCount (<= m) accCatalans)
k = m - genericIndex accCatalans o