#はじめに
Coq/SSReflectで二分木やn分木のような子ノードの数が一定の木ではなく、不定であるような一般の根付き木を定義する方法を記述します。
n分木に関しては別記事「Coq/SSReflectで自然数nを引数に取ってn分木を返す型をInductiveに定義する」をご覧ください。
#根付き木を定義する
子ノード数が不定であるような根付き木の型tree
は以下のように定義できます。
Variable (T:Type).
Inductive tree : Type := Node of T & seq tree.
(* Node : T -> seq tree -> tree *)
tree
型のコンストラクタはNode
の1つのみであり、各ノードはその要素の型T
と、その子の列seq tree
から構成されています。
ここで二分木btree
と比較してみると、
Inductive btree : Type :=
| BLeaf of T (* BLeaf : T -> btree *)
| BNode of T & btree & btree. (* BNode : T -> btree -> btree -> btree *)
tree
型には二分木における葉を作るコンストラクタBLeaf
にあたるものがないように思えます。しかしtree
型では、葉は子の列が[::]
の場合として定義します。
Notation Leaf x := (Node x [::]).
つまり、一般の根付き木の場合、枝を作るコンストラクタのみで構成することが可能であるということです。
しかしながら、この定義では枝の構成に関する帰納原理がうまく自動生成されず、木に関する性質を証明するのが大変になります。そこで、木に関する帰納原理を定義します。
#木に関する帰納原理
先ほどの定義でtree
に関する帰納原理tree_ind
が自動生成されますが、コンストラクタNode
にtree
型を直接とる引数がないため、帰納法の仮定がうまく生成されません。
tree_ind
: forall P : tree -> Prop,
(forall (x:T), (s:seq tree), P (Node x s)) ->
forall t:tree, P t.
本当は、以下のような帰納法の仮定として各子ノードに全てに対して示したい命題が成り立つような命題mytree_ind
が欲しいです。
mytree_ind
: forall P : tree -> Prop,
(forall (x:T) (s:seq tree),
foldr (fun t:tree => and (P t)) True s -> P (Node x s)) ->
forall t:tree, P t.
ここで、foldr (fun t => and (P t)) True s
が、子ノードのリストs
に対し、その全ての子ノードt
に対してP t
が成り立っていることを表しています。
##帰納原理を示す
この帰納原理mytree_ind
を示すにはどうすればいいでしょうか?
帰納法の仮定がないtree_ind
は使えそうにないので、fix
コマンドを使います。
mytree_ind
と同じ型を持つ式を直接記述する方法と、Proof modeで示す方法があるので、それぞれ紹介します。
###定義式を直接記述する方法
実は、mathcompライブラリのchoice.v内に、葉が任意の型T
であって、枝がnat
であるような一般の根付き木GenTree.tree
が定義されています。この帰納原理GenTree.tree_ind
はfix
を使って直接定義されています。
(* in mathcomp/choice.v *)
Module GenTree.
Variable T : Type.
Inductive tree := Leaf of T | Node of nat & seq tree.
Definition tree_ind K IH_leaf IH_node :=
fix loop t : K t : Prop := match t with
| Leaf x ⇒ IH_leaf x
| Node n f0 ⇒
let fix iter_conj f : foldr (fun t ⇒ and (K t)) True f :=
if f is t :: f' then conj (loop t) (iter_conj f') else Logic.I
in IH_node n f0 (iter_conj f0)
end.
End GenTree.
GenTree.tree
は葉GenTree.Leaf
と枝GenTree.Node
の2つのコンストラクタがあり、帰納原理GenTree.tree_ind
では、葉と枝、それぞれに対する帰納法の仮定IH_leaf
とIH_node
を渡しています。
詳しい解説は一旦先にtree
の方の帰納原理mytree_ind
を定義してから、そちらを使って行います。これはGenTree.tree_ind
では型が省略されているため、分かりづらいためです。
まずは帰納原理mytree_ind
をGenTree.tree_ind
を参考にしながら定義します。
Definition mytree_ind (P:tree -> Prop)
(IH_node : forall x s,
foldr (fun t => and (P t)) True s -> P (Node x s)) :
forall t, P t :=
fix loop t : P t :=
match t with
| Node x s =>
let fix iter_conj s : foldr (fun t => and (P t)) True s :=
if s is u :: s' then conj (loop u) (iter_conj s') else Logic.I
in IH_node x s (iter_conj s)
end.
ここで、fix
式で定義されたloop t : P t
は、P t
において帰納法を適用していることを表しており、帰納法の仮定はloop u : P u
として内部に現れています。
実際にはloop
は再帰関数で、t:tree
の構造に関して再帰呼び出しを行なっています。
Coqでは停止する関数のみしか定義できないため、再起呼び出しを行うtree
型の引数に対して減少性が成り立たないといけません。今回の場合、再起呼び出しを行う引数u
はt
をコンストラクタNode
とcons
で分解して出てくるものであり、構造的に小さくなっていると言えるので、定義が通るというわけです。
###Proof modeで示す方法
Proof modeでもfix
を使って同様に示すことができます。
Lemma mytree_ind (P:tree -> Prop) :
(forall x s, foldr (fun t => and (P t)) True s -> P (Node x s)) ->
forall t, P t.
Proof.
move => Hnode.
fix HP 1.
elim =>[x s]. apply : Hnode. by elim : s =>[|u s IHs].
Qed.
証明中のコマンドfix HP 1
で、P t
に関する帰納法を使います。このコマンドの引数のうち、HP
は仮定の名前、1
は第1引数に対する帰納法を表しており、 実行すると、HP : forall t, P t
という仮定から、forall t, P t
を示すことになります。
ゴールと同じ仮定があるように見えるので、すぐapply
したくなりますが、残念ながらゴールは消えるものの、第1引数t
の減少性が言えないため、Qed
できない仕様になっています。
ここでは、子ノードに関する帰納法を使えばいいので、t
を分解して子ノードのリストs
を取り出し、これに関する帰納法を使えば子ノードu
が現れるので、このu
に対して帰納法の仮定P u
を適用してやればいいです。
この証明によって、直接定義を記述するのと同じような定義の式が生成されます。
#木の構造に関する再帰関数
木の構造に関する再帰関数をいくつか定義します。
##fold関数を定義する
リストにおいて主な再帰関数がfoldr
で記述できるように、木に関しても、全ての子ノードに対して再帰するような関数treefold
を記述することができます。
Fixpoint treefold (S:Type)
(f:T -> S -> S) (op:S -> S -> S) (id:S) (t:tree) : S :=
match t with
| Node x s => f x (foldr (fun u => op (treefold f op id u)) id s)
end.
木t
の各子ノードu
での再帰関数の返り値treefoldr f op id u
の列に対し、foldr op id
を施します。その結果と親t
の要素x
に対し、f
を掛けたものを返す関数です。
書き換えると以下のようになります。
Lemma treefold_equal S f op (id:S) t :
treefold f op id t =
match t with
| Node x s => f x (foldr op id (map (treefold f op id) s))
end.
Proof. case : t =>[x s]/=. by rewrite foldr_map. Qed.
例えば、木のノード数を返す関数treesize
はこのtreefold
を用いて以下のように書けます。
Definition treesize : tree -> nat := treefold (fun _ => S) addn 0.
##高さを調べる
木といえば高さを調べたくなりますが、高さを調べる関数height
はtreefold
で定義するより、直接定義したほうがいいです。
Fixpoint height (t:tree) : nat :=
match t with
| Node _ s => foldr (fun u => maxn (height u).+1) 0 s
end.
一応、treefold
を使っても高さを求める関数height'
が定義できますが、tree
型では葉も枝も同じコンストラクタNode
で定義されているため区別ができず、葉も枝と同じように高さとしてカウントしてしまうため、treefold
のみで定義しようとすると1大きい結果が返ってきます。つまり結果から.-1
する必要があります。
Definition height' (t:tree) : nat := (treefold (fun _ => S) maxn 0 t).-1.
このように定義すると帰納法を使うときに.-1.+1
が出てきますが、これを消去するためには、treefold
の返り値が0より大きいことを示す必要があります。実際には、以下の証明のrewrite prednK //; case : u.
のような記述をする必要があり、証明するのが面倒になります。
Lemma height_equal: height =1 height'.
Proof.
elim /mytree_ind =>[x s]/=.
elim : s =>[|u s IHs]//=[->/IHs->]. rewrite /height'.
by rewrite prednK //; case : u.
Qed.
#木に関する証明
上で木に関する帰納原理mytree_ind
と関数を定義したので、実際に使ってみます。
例として、木のノード数は高さより真に大きいことlt_height_treesize
を示してみます。
Lemma treesize_cons x u s:
treesize (Node x (u :: s)) = treesize u + treesize (Node x s).
Proof. by rewrite /treesize /= addnS. Qed.
Lemma lt_height_treesize t: height t < treesize t.
Proof.
elim /mytree_ind : t =>[x s]/=.
elim : s =>[|t s IHs]//=[Ht /IHs Hle]. rewrite treesize_cons maxnE -addnS.
apply : leq_add =>//. exact : leq_ltn_trans (leq_subr _ _) Hle.
Qed.
基本的には、先ほどのheight
とheight'
が外延的等価であったことを示した時と同様に、木に関する帰納法elim /mytree_ind : t =>[x s]
を使った後、子ノードを取り出すために子ノードのリストに関する帰納法elim : s
を使います。
#まとめ
Coq/SSReflectで子ノード数が不定である一般の根付き木を定義し、それに関する帰納原理や定義の仕方をまとめました。