-
-
Save MonoidMusician/1c8cc2ec787ebaf91b95d67cbc5c3498 to your computer and use it in GitHub Desktop.
-- This work by Nick Scheel is licensed under CC0 1.0 | |
-- https://creativecommons.org/publicdomain/zero/1.0/ | |
-- A basic framework for graph theory on multidigraphs in Lean | |
-- and a proof that no_watershed_condition is sufficient to | |
-- establish that a graph has a unique sink for each vertex | |
-- | |
-- I hope to give some introduction to the syntax of how Lean works here, | |
-- but I assume some familiarity with functions, pattern matching, | |
-- type theory, and proofs. | |
-- | |
-- The most important thing to note is that `begin` and `end` delineate | |
-- sections of code in "tactic/interactive proof mode" (as opposed to | |
-- "term mode"). The Lean compiler shows the proof goal and the available | |
-- assumptions and context, and then shows how it changes after tactics | |
-- are executed. The best way to see this happen is to download Lean from | |
-- the following links and open it in VSCode (or emacs) and walk through | |
-- the steps of a proof to see the changes. | |
-- | |
-- VSCode: "Typing ctrl-shift-enter opens up a message window which shows you | |
-- error messages, warnings, output, and goal information when in tactic mode." | |
-- Emacs: "C-c C-g show goal in tactic proof (lean-show-goal-at-pos)" | |
-- (https://leanprover.github.io/reference/using_lean.html) | |
-- | |
-- Make sure to create a project folder and install mathlib as explained here: | |
-- https://leanprover.github.io/reference/using_lean.html#using-the-package-manager | |
-- | |
-- http://leanprover.github.io/ | |
-- https://github.com/leanprover/lean | |
-- Quick primer on Lean: | |
-- | |
-- Propositions are types. This means that the mathematical language of Lean is | |
-- the language of types, which can encode any sensical sentence about math. | |
-- Types are, of course, also used in the design of computable programming | |
-- languages, which Lean also is. But "mere" propositions don't contain data | |
-- to compute on: only the type of a proof matters, really, not its exact details. | |
-- | |
-- The term language and the type language are the same, which means that values | |
-- can be mentioned in types, and vice-versa. This is a result of dependent type | |
-- theory, and it allows encoding interesting propositions, such as about the value | |
-- of functions: given some (f : ℝ → ℝ), (∀ x : ℝ, x > 0 → f x = 0) is the | |
-- proposition that, for positive values of `x`, `f x` evaluates to zero. Note that | |
-- `f` is a fixed value, and `x` is a variable value, both mentioned at the type level. | |
-- | |
-- ∀ is actually the same thing as Π – they are notation for dependently-typed | |
-- functions. (f : Π (a : α), β a) is a function that takes an element `a` of type `α` | |
-- and returns a result that `β a`, with a type that can depend on the value of `a`. | |
-- Putting this together, if we have `f` as above, and some `v : α`, then `f v : β v` | |
-- (meaning, the value of `f` applied to `v` has type `β v`). | |
-- If `β a` does _not_ depend on `a` (that is, if `β` is a constant function, say, | |
-- β = λ _, γ), then the type of `f` can be written as `α → γ`, the type of a | |
-- non-dependent function, also familiar as implication from logic. | |
-- The cool bit about dependent functions, though, is that they are also used to | |
-- introduce polymorphism, if `α` is chosen to be `Type` (or `Prop`, or `Sort u` | |
-- most generally). So `(λ _ a, a) : ∀ {α : Type}, α → α` is the polymorphic | |
-- identity function. Note that function terms are introduced with lambdas: | |
-- def f : Π (a : α), β a := | |
-- λ (a : α), <some value of type `β a` using `a : α` in scope> | |
-- | |
-- But more typically, top-level definitions will have this alternate syntax: | |
-- def f (a : α) : β a := <some value of type `β a` using `a : α` in scope> | |
-- There are subtle differences, but they are basically the same, just note | |
-- that in this latter form, (a : α) is already in scope without the lambda binder! | |
-- | |
-- I mention this below, but for completeness, there is existential quantification: | |
-- (∃ (a : α), p a) : Prop, and dependent products: (Σ' (a : α), p a) : Type. | |
-- Dependent products give access to the value and the proof, whereas existentials | |
-- only assert that there is such a value, without giving it explicitly. | |
-- This is the difference between (non-computable) classical logic and (computable) | |
-- intuitionistic logic. See https://homotopytypetheory.org/book/ for more. | |
-- | |
-- Definitional versus propositional equality: there are two varieties of equality: | |
-- `:=` is the syntax used to declare equalities, most of the time, and | |
-- `=` is the proposition that two things are equal. The Lean compiler will solve | |
-- definitional equalities, and this can be turned into a propositional equality | |
-- using `refl a : a = a` (standing for the reflexivity of equality). | |
-- For example, `refl 1 : 1 = 1` is the proof that 1 equals itself. Duh! | |
-- `eq.trans (e1 : a = b) (e2 : b = c) : a = c` is an example where, even though | |
-- `a`, `b`, and `c` are not known in advance (and thus are not definitionally | |
-- equal), their propositional equalities can still be combined. | |
-- Import libraries that this file depends on | |
import data.fintype data.equiv | |
-- Just a universe variable (used to avoid Girard's/Russel's paradox). | |
-- Not so important; see the following for more: | |
-- https://leanprover.github.io/reference/other_commands.html | |
-- https://en.wikipedia.org/wiki/Intuitionistic_type_theory#Universe_Types | |
universe u | |
-- A few helpful lemmata, explanations skipped | |
theorem psigma_elim_exists {α : Type} {β : α → Prop} {r : Prop} : | |
Exists β → (psigma β → r) → r := | |
λ e f, exists.elim e (λ a b, f (psigma.mk a b)) | |
theorem subtype_elim_exists {α : Type} {β : α → Prop} {r : Prop} : | |
Exists β → (subtype β → r) → r := | |
λ e f, exists.elim e (λ a b, f (subtype.mk a b)) | |
lemma list.nodup_tail_of_nodup {α : Type} {l : list α} : | |
list.nodup l → list.nodup (list.tail l) := | |
begin | |
intro nd, | |
cases l, | |
exact list.nodup_nil, | |
rw list.tail_cons, | |
apply list.nodup_of_nodup_cons, | |
exact nd, | |
end | |
-- end lemmata | |
-- First we will need to define the types that characterize a (multi)digraph. | |
-- An arc connects a starting and ending vertex; this defines the Type for that. | |
-- in particular, this states that, given a Type (call it V), arcT V is a Type | |
-- defined as `V × V`, which is an ordered pair consisting of two elements of type V. | |
def arcT (V : Type) : Type := V × V | |
def arcT.mk {V : Type} (v1 v2 : V) : arcT V := | |
prod.mk v1 v2 | |
-- Each digraph has a particular type of vertices, type of arcs, and consists of | |
-- the mapping φ from specific arcs to their starting and ending vertices. | |
structure digraph | |
(V E : Type) : Type := | |
(φ : E → arcT V) | |
-- `arc G v1 v2` is an arc that goes from v1 to v2 in graph G. | |
-- V and E are implicit arguments (since they use {}): they can be filled in | |
-- once we know what the type of G is, since (G : digraph V E) mentions V and E. | |
def arc | |
{V E : Type} (G : digraph V E) | |
(v1 v2 : V) := | |
-- this is an edge paired with a proof that the edge connects | |
-- the vertices mentioned in the type of the arc | |
Σ'(e : E), G.φ e = arcT.mk v1 v2 | |
-- The proposition that an arc exists, without specifying the arc itself. | |
def has_arc | |
{V E : Type} (G : digraph V E) | |
(v1 v2 : V) := | |
∃ (e : E), G.φ e = arcT.mk v1 v2 | |
-- Eliminate the existential has_arc with a non-dependent proof function taking an arc. | |
def has_arc.elim | |
{V E : Type} {G : digraph V E} | |
{v1 v2 : V} {r : Prop} : | |
has_arc G v1 v2 → (arc G v1 v2 → r) → r := | |
psigma_elim_exists | |
namespace digraph | |
-- Get the source of an edge E in a graph G | |
def source | |
{V E : Type} (G : digraph V E) | |
(e : E) : V := (G.φ e).1 | |
-- Target of an arc | |
def target | |
{V E : Type} (G : digraph V E) | |
(e : E) : V := (G.φ e).2 | |
-- An initial vertex has no edges leading to it in G | |
def is_initial | |
{V E : Type} (G : digraph V E) | |
(v : V) : Prop := | |
¬∃ (e : E), target G e = v | |
-- A final vertex has no edges leading from it in G | |
def is_final | |
{V E : Type} (G : digraph V E) | |
(v : V) : Prop := | |
¬∃ (e : E), source G e = v | |
-- Subtype generated by is_initial | |
-- (that is, a vertex with a proof that it is initial) | |
def initial | |
{V E : Type} (G : digraph V E) := | |
{ v : V // is_initial G v } | |
-- Subtype generated by is_final | |
def final | |
{V E : Type} (G : digraph V E) := | |
{ v : V // is_final G v } | |
-- A walk, as an inductive type, consists of arcs where each next arc leads | |
-- from the previous arc. This is encoded by matching the vertices at the type | |
-- level: only an arc from v1 to v2 and a walk from v2 to v3 can produce a walk | |
-- from v1 to v3. | |
-- Again, V and E can be figured out from an explicit G. | |
inductive walk | |
{V E : Type} (G : digraph V E) : | |
V → V → Type | |
| empty {v : V} : walk v v | |
| step {v1 v2 v3 : V} : arc G v1 v2 → walk v2 v3 → walk v1 v3 | |
-- This begins a namespace; everything declared in this namespace will be accessed | |
-- with the prefix `walk.` outside of the namespace, like `walk.length` | |
namespace walk | |
-- The edges underlying a walk. | |
-- This is a function defined by taking some arguments (most notably a walk) | |
-- and returning a list of elements of type E. | |
-- Note that G is implicit: it's inferrable from the type of the walk given to edges | |
def edges | |
{V E : Type} {G : digraph V E} | |
{v1 v2 : V} : | |
walk G v1 v2 → list E := | |
-- this is tactic mode | |
begin | |
-- `intro` introduces the argument `w : walk G v1 v2` | |
intro w, | |
-- `induction` takes apart the two cases of `w` (`empty` and `step`) | |
-- and adds an induction hypothesis for the nested walk in the `step` case | |
induction w, | |
-- for the first case, we want to produce an empty list | |
-- `exact` just means that we are finishing the goal with a suitable term | |
exact list.nil, | |
-- for the second case, we `cons`truct a new list by adding the edge | |
-- of the arc on the front to the front on the rest of the edges | |
-- (note that w_a and w_ih are names automatically generated by `induction w`) | |
exact list.cons w_a.1 w_ih, | |
end | |
-- The vertices visited in a walk, including the start and end vertices | |
-- (which are already mentioned in the type). | |
def vertices | |
{V E : Type} {G : digraph V E} | |
{v1 v2 : V} : | |
walk G v1 v2 → list V := | |
begin | |
-- similar story, but we start with v2 in the empty case | |
intro w, induction w, | |
exact list.cons v2 list.nil, | |
-- and add v1 at each step | |
exact list.cons w_v1 w_ih, | |
end | |
-- The length of a walk, i.e. the number of arcs used in it, as a natural number. | |
def length | |
{V E : Type} {G : digraph V E} | |
{v1 v2 : V} : | |
walk G v1 v2 → ℕ := | |
begin | |
intro w, | |
induction w, | |
exact 0, | |
-- Note: nat.succ n = n+1, get used to it ;) | |
exact nat.succ w_ih, | |
end | |
-- This is our first lemma; it's like a definition but usually it contains a proof, | |
-- (in this case: a proof that the length of an empty walk is 0). Conceptually, | |
-- in type theory, there is no real difference: this is just another | |
-- dependently-typed function, taking variables V E G and v and returning a result. | |
@[simp] -- this attribute lets it get picked up by the `simp` tactic | |
lemma length_empty | |
{V E : Type} {G : digraph V E} | |
{v : V} : | |
-- (empty G : walk G v v) is just a type annotation specifying v | |
-- This could be written (empty _ : walk G v v), since G is | |
-- inferrable by unification | |
length (empty G : walk G v v) = 0 := | |
by refl -- definitional equality holds here | |
@[simp] | |
lemma length_step | |
{V E : Type} {G : digraph V E} | |
{v1 v2 v3 : V} | |
(a : arc G v1 v2) | |
(w : walk G v2 v3) : | |
length (step a w) = length w + 1 := | |
by refl | |
-- A walk from v1 to v2 can be concatenated with a walk from v2 to v3 by first | |
-- taking the arcs from the first walk, then taking the arcs from the second. | |
-- This is a binary function with two arguments, nothing else is new. | |
def concat | |
{V E : Type} {G : digraph V E} | |
{v1 v2 v3 : V} : | |
walk G v1 v2 → walk G v2 v3 → walk G v1 v3 := | |
begin | |
intros w1 w2, | |
induction w1, | |
exact w2, | |
exact step w1_a (w1_ih w2), | |
end | |
-- A proof of a fact that's obvious enough for the equation compiler to solve | |
@[simp] | |
lemma concat_empty_left | |
{V E : Type} {G : digraph V E} | |
{v1 v2 : V} | |
(w : walk G v1 v2) : | |
concat (empty G) w = w := | |
by refl | |
-- Again, pretty obvious | |
lemma concat_step_left | |
{V E : Type} {G : digraph V E} | |
{v1 v2 v3 v4 : V} | |
(a : arc G v1 v2) | |
(w1 : walk G v2 v3) | |
(w2 : walk G v3 v4) : | |
concat (step a w1) w2 = step a (concat w1 w2) := | |
by refl | |
-- But this one needs to be solved by induction, since there's no special case | |
-- for an empty walk on the right (one must walk through all of the first walk). | |
@[simp] | |
lemma concat_empty_right | |
{V E : Type} {G : digraph V E} | |
{v1 v2 : V} | |
(w : walk G v1 v2) : | |
concat w (empty G) = w := | |
begin | |
-- luckily it is simple using induction | |
induction w, | |
refl, | |
-- `rw` (or `rewrite`) is a tactic that changes the type of the goal | |
-- given particular equalities; in this case, it finishes the goal too. | |
rw [concat_step_left, w_ih], | |
end | |
-- Concatenating walks adds their length. | |
lemma concat_length | |
{V E : Type} {G : digraph V E} | |
{v1 v2 v3 : V} | |
(w1 : walk G v1 v2) | |
(w2 : walk G v2 v3) : | |
length (concat w1 w2) = length w1 + length w2 := | |
begin | |
induction w1, | |
-- this is easy enough for the tactic to solve by simplification, | |
-- by using these facts (available with the [simp] attribute): | |
-- 1. concat (empty G) w2 = w2 | |
-- 2. length (empty G) = 0 | |
simp, | |
-- provide two additional theorems to help `simp` close the goal: | |
simp [concat_step_left, w1_ih], | |
end | |
-- Useful lemmata regarding walk.vertices | |
-- (still inside the walk namespace!) | |
namespace vertices | |
-- Takes an assumption that the walk is empty, making it easier to use | |
-- when the proof is not obviously `refl`. When it is obvious, use: | |
-- rw [walk.vertices.empty _ rfl], | |
@[simp] | |
def empty | |
{V E : Type} {G : digraph V E} | |
{v : V} | |
(w : walk G v v) : | |
(w = walk.empty G) → walk.vertices w = [v] := | |
λ h, eq.substr h rfl | |
-- Takes an assumption that the walk is nonempty, making it easier to use | |
-- when the proof is not obviously `refl`. When it is obvious, use: | |
-- rw [walk.vertices.step _ _ _ rfl], | |
@[simp] | |
def step | |
{V E : Type} {G : digraph V E} | |
{v1 v2 v3 : V} | |
(a : arc G v1 v2) | |
(w : walk G v2 v3) | |
(w' : walk G v1 v3) : | |
(w' = walk.step a w) → walk.vertices w' = v1 :: walk.vertices w := | |
λ h, eq.substr h rfl | |
-- The list of vertices of a walk starts with the starting vertex of the walk. | |
def starts_with | |
{V E : Type} {G : digraph V E} | |
{v1 v2 : V} | |
(w : walk G v1 v2) : | |
walk.vertices w = v1 :: list.tail (walk.vertices w) := | |
begin | |
cases w, refl, refl, | |
end | |
-- The list of vertices follow concatenation ... except for the first vertex of | |
-- the second walk, which would be duplicated by just joining the verticces, | |
-- since it is the last entry in `walk.vertices w1` already. | |
def concat | |
{V E : Type} {G : digraph V E} | |
{v1 v2 v3 : V} | |
(w1 : walk G v1 v2) | |
(w2 : walk G v2 v3) | |
(w3 : walk G v1 v3) : | |
(w3 = walk.concat w1 w2) → | |
walk.vertices w3 = walk.vertices w1 ++ list.tail (walk.vertices w2) := | |
begin | |
intro h, rw h, | |
induction w1, | |
{ simp, exact starts_with w2 }, | |
rw [concat_step_left w1_a w1_a_1 w2], | |
-- simplify right side | |
rw [step w1_a w1_a_1 _ rfl], | |
-- simplify left side | |
rw [step w1_a (concat w1_a_1 w2) _ rfl], | |
-- pass through the cons, and apply induction hypothesis | |
congr, apply w1_ih _ _ rfl, | |
end | |
end vertices | |
end walk | |
-- close out the namespaces; back to the default namespace of this file | |
-- Back to types, a similar story regarding paths as walks: | |
-- A path is a walk with a proof that vertices are not repeated. | |
def path | |
{V E : Type} (G : digraph V E) : | |
V → V → Type := | |
λ v1 v2, { w : walk G v1 v2 // list.nodup (walk.vertices w) } | |
-- The proposition that a path between two vertices merely exists. | |
def has_path | |
{V E : Type} (G : digraph V E) : | |
V → V → Prop := | |
λ v1 v2, ∃ (w : walk G v1 v2), list.nodup (walk.vertices w) | |
-- Eliminate the existential has_path via a real path | |
def has_path.elim | |
{V E : Type} {G : digraph V E} | |
{v1 v2 : V} {r : Prop} : | |
has_path G v1 v2 → (path G v1 v2 → r) → r := | |
subtype_elim_exists | |
namespace path | |
-- An empty path is simple to generate; its list of 1 vertex obviously has no | |
-- duplicates. | |
def empty | |
{V E : Type} (G : digraph V E) | |
{v : V} : path G v v := | |
begin | |
-- `apply` is sort of like `exact`, but it expects a function that requires more | |
-- arguments to reach the proof goal. | |
-- In particular, this starts to create a subtype with the value `empty` | |
apply subtype.mk (walk.empty G), | |
-- but it requires a proof that there are no duplicates in its vertex list: | |
simp [walk.vertices.empty, list.nodup_singleton], | |
end | |
-- An arc between distinct vertices gives a path | |
def from_arc | |
{V E : Type} (G : digraph V E) | |
{v1 v2 : V} : (v1 ≠ v2) → arc G v1 v2 → path G v1 v2 := | |
begin | |
intro h, intro a, | |
-- `let` binds a local constant in a way that it can be referred to | |
-- in other proof terms | |
let w := walk.step a (walk.empty G), | |
apply subtype.mk w, | |
-- `have` just provides a usuable constant whose definition can't be referred to | |
have : walk.vertices w = [v1, v2], refl, | |
rw this, | |
-- `simp` helps make progress on the goal | |
simp [list.nodup_singleton], | |
-- the only thing missing is a proof that the vertices are distinct, | |
-- which is true by assumption | |
exact h -- or we could even use the `assumption` tactic | |
end | |
@[reducible] -- [reducible] makes this definition more transparent | |
def length | |
{V E : Type} {G : digraph V E} | |
{v1 v2 : V} : | |
path G v1 v2 → ℕ := | |
λ p, walk.length p.1 | |
-- This is a big topic: creating a path from a walk | |
-- Note that my proofs in this section are ugly, so feel free to skip them | |
-- (the high-level idea is easy enough to grasp; just know that it's | |
-- machine-verified ;D) | |
namespace from_walk | |
-- A helpful invariant: a walk with just a vertex removed preserves the property | |
-- of having no duplicates. | |
def preserves_nodup | |
{V E : Type} {G : digraph V E} | |
{v v1 v2 : V} | |
(w : walk G v1 v2) | |
(w' : walk G v v2) := | |
list.nodup (list.tail (walk.vertices w)) → | |
list.nodup (list.tail (walk.vertices w')) | |
-- But it turns out that the desired walk being a subwalk is sufficient | |
def split_at | |
{V E : Type} {G : digraph V E} | |
{v1 v2 : V} (w : walk G v1 v2) (v : V) := | |
Σ' (w1 : walk G v1 v) (w2 : walk G v v2), | |
w = walk.concat w1 w2 | |
-- This includes the other invariant we want to guarantee (that v does not | |
-- appear except as the first vertex of w2). | |
def split_at_without | |
{V E : Type} {G : digraph V E} | |
{v1 v2 : V} (w : walk G v1 v2) (v : V) := | |
Σ' (w1 : walk G v1 v) (w2 : walk G v v2), | |
(v ∉ list.tail (walk.vertices w2)) ∧ | |
w = walk.concat w1 w2 | |
-- Weaken the latter to the former ... | |
-- Note: `a_of_b` is convention to indicate a function/lemma with a type like `b → a` | |
def split_at_of_split_at_without | |
{V E : Type} {G : digraph V E} | |
{v1 v2 : V} {w : walk G v1 v2} {v : V} : | |
split_at_without w v → split_at w v := | |
begin | |
intro s, constructor, constructor, exact s.2.2.2 | |
end | |
-- And recover the invariant from it | |
lemma split_at.preserves_nodup | |
{V E : Type} {G : digraph V E} | |
{v1 v2 : V} {w : walk G v1 v2} {v : V} | |
(s : split_at w v) : preserves_nodup w s.2.1 := | |
begin | |
cases s with w1 s, cases s with w2 p, | |
intro nd, simp, | |
rw [walk.vertices.concat _ _ _ p] at nd, | |
rw [walk.vertices.starts_with _] at nd, | |
simp at nd, | |
rw list.nodup_append at nd, | |
exact nd.2.1, | |
end | |
def discard_vertex.split_at | |
{V E : Type} [decidable_eq V] {G : digraph V E} | |
(v : V) {v1 v2 : V} (w : walk G v1 v2) : | |
psum (split_at_without w v) (v ∉ list.tail (walk.vertices w)) := | |
begin | |
induction w with v0 v1 v2 v3 a w ih, | |
right, | |
simp [walk.vertices.empty _ rfl], | |
simp [walk.vertices.step _ _ _ rfl], | |
cases ih, left, | |
apply psigma.mk (walk.step a ih.1), | |
apply psigma.mk ih.2.1, | |
apply and.intro ih.2.2.1, | |
rw [walk.concat_step_left _ _ _], | |
congr, | |
exact ih.2.2.2, | |
by_cases h : v = v2, | |
left, rw h, rw h at ih, | |
have : walk.concat (walk.step a (walk.empty G)) w = walk.step a w, | |
{ | |
simp [walk.concat_step_left _ _ _], | |
}, | |
exact psigma.mk (walk.step a (walk.empty G)) (psigma.mk w (and.intro ih this)), | |
right, | |
have : walk.vertices w = v2 :: list.tail (walk.vertices w), | |
cases w, refl, refl, | |
rw this, intro vin, cases vin, | |
exact h vin, exact ih vin, | |
end | |
-- Remove any occurrence of a vertex from a walk, by either returning a new walk | |
-- formed as a subset of the previous walk but starting from the vertex to be | |
-- removed (along with proofs that the vertex was removed, and that the new walk | |
-- contains no dupicates if the original did), OR a proof that the vertex | |
-- does not occur in the original walk. | |
-- | |
-- This is obviously ugly, but encodes enough information to make from_walk work. | |
def discard_vertex | |
{V E : Type} [decidable_eq V] {G : digraph V E} | |
(v : V) {v1 v2 : V} (w : walk G v1 v2) : | |
psum | |
-- either a new walk | |
(Σ' (w' : walk G v v2), -- from v to v2 | |
-- where v does not occur | |
(v ∉ list.tail (walk.vertices w')) ∧ | |
-- and preserving the no duplicates property | |
preserves_nodup w w') | |
-- or a proof that the vertex does not occur in the original walk | |
(v ∉ list.tail (walk.vertices w)) := | |
begin | |
induction w with v0 v1 v2 v3 a w ih, | |
right, | |
simp [walk.vertices.empty _ rfl], | |
simp [walk.vertices.step _ _ _ rfl], | |
cases ih, left, apply psigma.mk ih.1, apply and.intro ih.2.1, | |
intro ndaw, simp [walk.vertices.step _ _ _ rfl] at ndaw, apply ih.2.2, | |
apply list.nodup_tail_of_nodup, exact ndaw, | |
by_cases h : v = v2, | |
left, rw h, rw h at ih, | |
have : preserves_nodup (walk.step a w) w, | |
{ | |
intro ndaw, | |
simp [walk.vertices.step _ _ _ rfl] at ndaw, | |
apply list.nodup_tail_of_nodup, exact ndaw, | |
}, | |
exact psigma.mk w (and.intro ih this), | |
right, | |
have : walk.vertices w = v2 :: list.tail (walk.vertices w), | |
cases w, refl, refl, | |
rw this, intro vin, cases vin, | |
exact h vin, exact ih vin, | |
end | |
-- This can obviously be used to discard repeats of the first vertex of a walk. | |
-- But this is actually unused ... | |
def discard_vertex.beginning | |
{V E : Type} [decidable_eq V] {G : digraph V E} | |
{v1 v2 : V} (w : walk G v1 v2) : | |
Σ' (w' : walk G v1 v2), | |
(v1 ∉ list.tail (walk.vertices w')) ∧ | |
preserves_nodup w w' := | |
begin | |
have := discard_vertex v1 w, | |
cases this, exact this, | |
exact psigma.mk w (and.intro this id), | |
end | |
-- Using induction on the walk, discard all the vertex repeats to generate a | |
-- path. This has the effect of removing all cycles in the walk. | |
def discard_vertex.all.explicit | |
{V E : Type} [decidable_eq V] {G : digraph V E} | |
{v1 v2 : V} (w : walk G v1 v2) : path G v1 v2 := | |
begin | |
-- Induction on a walk considers the base case (empty) and the step case | |
induction w with v v1 v2 v3 a w ih, | |
-- If the walk is empty, we already know how to generate an empty path; done. | |
exact path.empty _, | |
-- But if v1 = v2, we discard the arc, and just return the induction | |
-- hypothesis (that is, the path generated from the tail), once we have | |
-- rewritten v1 as v2. | |
by_cases v1_v2 : (v1 = v2), rw v1_v2, exact ih, | |
-- Just splits up the induction hypothesis into the walk and the proof. | |
cases ih with ih_w ih_nodup, | |
-- Now we discard the starting vertex from the induction hypothesis' walk | |
have ih_w' := discard_vertex v1 ih_w, | |
-- Consider the cases: a new walk, or the same walk | |
cases ih_w', cases ih_w' with w' p, | |
-- If we obtained a new walk, it is the walk we want to return | |
apply subtype.mk w', | |
-- Construct a proof that the walk has no duplicates | |
rw [walk.vertices.starts_with, list.nodup_cons], constructor, | |
-- from the fact that v does not occur in the rest of the vertices | |
exact p.1, | |
-- and the fact that the ih had no duplicates, so the tail of this has none | |
exact (p.2 (list.nodup_tail_of_nodup ih_nodup)), | |
-- Otherwise, we should return the walk consisting of this arc and the ih walk | |
apply subtype.mk (walk.step a ih_w), | |
-- Do some rewrites of the goal ... | |
rw [walk.vertices.step _ _ _ rfl, list.nodup_cons], constructor, | |
rw [walk.vertices.starts_with, list.mem_cons_iff], | |
-- We know that v1 is not the second vertex v2, nor is it in ih_w | |
apply not_or v1_v2 ih_w', | |
-- And the rest of the ih already had no duplicates, by induction | |
exact ih_nodup, | |
end | |
-- This contains the above algorithm but also produces the proof that either | |
-- the result is the same, or the walk/graph contained a cycle. | |
-- | |
-- This can be used to prove the equivalence of walks and paths in acyclic | |
-- digraphs. | |
def discard_vertex.all.acyc_prf | |
{V E : Type} [decidable_eq V] {G : digraph V E} | |
{v1 v2 : V} (w : walk G v1 v2) : | |
Σ' (p : path G v1 v2), | |
p.1 = w ∨ ∃(v : V) (c : walk G v v), c ≠ walk.empty G := | |
begin | |
induction w with v v1 v2 v3 a w ih, | |
exact psigma.mk (path.empty _) (or.inl rfl), | |
by_cases v1_v2 : (v1 = v2), | |
apply psigma.mk, right, tactic.swap, | |
rw v1_v2, exact ih.1, | |
apply exists.intro v2, | |
rw v1_v2 at a, | |
apply exists.intro (walk.step a (walk.empty _)), | |
exact λ h, walk.no_confusion h, | |
cases ih with ih prf, cases ih with ih_w ih_nodup, | |
have ih_w' := discard_vertex.split_at v1 ih_w, | |
cases ih_w', | |
have := split_at.preserves_nodup (split_at_of_split_at_without ih_w'), | |
cases ih_w' with w1 ih_w', cases ih_w' with w2 p, | |
apply psigma.mk (subtype.mk w2 | |
begin | |
rw [walk.vertices.starts_with, list.nodup_cons], constructor, | |
exact p.1, | |
exact (this (list.nodup_tail_of_nodup ih_nodup)), | |
end : path G v1 v3), | |
right, apply exists.intro v1, | |
apply exists.intro (walk.step a w1), | |
exact λ h, walk.no_confusion h, | |
apply psigma.mk (subtype.mk (walk.step a ih_w) | |
begin | |
rw [walk.vertices.step _ _ _ rfl], | |
rw [list.nodup_cons], constructor, | |
rw [walk.vertices.starts_with], | |
rw list.mem_cons_iff, | |
apply not_or v1_v2 ih_w', | |
exact ih_nodup, | |
end : path G v1 v3), | |
cases prf, | |
left, | |
simp at prf, | |
simp [prf], | |
right, | |
exact prf, | |
end | |
-- This defeq will be important | |
@[reducible] | |
def discard_vertex.all | |
{V E : Type} [decidable_eq V] {G : digraph V E} | |
{v1 v2 : V} (w : walk G v1 v2) : path G v1 v2 := | |
(discard_vertex.all.acyc_prf w).1 | |
end from_walk | |
-- This just references the version inside the from_walk namespace | |
@[reducible] | |
def from_walk | |
{V E : Type} [decidable_eq V] {G : digraph V E} | |
{v1 v2 : V} : | |
walk G v1 v2 → path G v1 v2 := from_walk.discard_vertex.all | |
-- This concatenates the underlying paths then removes cycles to generate | |
-- the new path. | |
def concat | |
{V E : Type} [decidable_eq V] {G : digraph V E} | |
{v1 v2 v3 : V} | |
(p1 : path G v1 v2) | |
(p2 : path G v2 v3) : | |
path G v1 v3 := from_walk (walk.concat p1.1 p2.1) | |
-- A maximal path is a path going to a final vertex. | |
def maximal | |
{V E : Type} (G : digraph V E) | |
(v1 v2 : V) := pprod (path G v1 v2) (is_final G v2) | |
-- (pprod stores a piece of data and a separate proof) | |
-- Proposition that one exists. | |
def has_maximal | |
{V E : Type} (G : digraph V E) | |
(v1 v2 : V) := (has_path G v1 v2) ∧ (is_final G v2) -- regular conjunction | |
-- Eliminate it following the usual pattern | |
def has_maximal.elim | |
{V E : Type} {G : digraph V E} | |
{v1 v2 : V} {r : Prop} : | |
has_maximal G v1 v2 → (maximal G v1 v2 → r) → r := | |
λ h f, has_path.elim h.1 (λ p, f (pprod.mk p h.2)) | |
-- Hopefully this makes sense now by just looking at the types! | |
def maximal.concat | |
{V E : Type} [decidable_eq V] {G : digraph V E} | |
{v1 v2 v3 : V} | |
(p1 : path G v1 v2) | |
(p2 : path.maximal G v2 v3) : | |
path.maximal G v1 v3 := | |
begin | |
constructor, | |
exact path.concat p1 p2.1, | |
exact p2.2, | |
end | |
def has_maximal.concat | |
{V E : Type} [decidable_eq V] {G : digraph V E} | |
{v1 v2 v3 : V} | |
(p1 : path G v1 v2) | |
(p2 : path.has_maximal G v2 v3) : | |
path.has_maximal G v1 v3 := | |
flip and.intro p2.2 | |
begin | |
apply exists.elim p2.1, intro w, intro nodup, | |
exact subtype.exists_of_subtype (path.concat p1 (subtype.mk w nodup)), | |
end | |
-- If the walk underlying a path is nonempty, produce the path corresponding | |
-- to its tail. | |
def vertices.step_back | |
{V E : Type} [decidable_eq V] {G : digraph V E} | |
{v1 v2 v3 : V} | |
(a : arc G v1 v2) | |
(w : walk G v2 v3) | |
(p : path G v1 v3) : | |
(p.1 = walk.step a w) → path G v2 v3 := | |
begin | |
intro h, cases p, simp at h, | |
apply subtype.mk w, | |
rw [walk.vertices.step _ _ _ h] at p_property, | |
exact list.nodup_of_nodup_cons p_property, | |
end | |
end path | |
-- One encoding (fairly direct and easy to use) of the property of an acyclic | |
-- graph: all walks from a vertex to itself are empty. | |
def is_acyclic | |
{V E : Type} (G : digraph V E) := | |
∀ (v : V) (w : walk G v v), w = walk.empty G | |
-- Subtype generated from that (unused). | |
def acyclic | |
{V E : Type} := | |
{ G : digraph V E // is_acyclic G } | |
-- It turns out that in acyclic graphs, from_walk just elaborates the same walk | |
-- with a proof term. | |
lemma is_acyclic.from_walk | |
{V E : Type} [decidable_eq V] {G : digraph V E} | |
{v1 v2 : V} | |
(acyc : is_acyclic G) | |
(w : walk G v1 v2) : | |
(path.from_walk w).val = w := | |
begin | |
let fw := path.from_walk.discard_vertex.all.acyc_prf w, | |
have : path.from_walk w = fw.1, refl, rw this, | |
cases h : fw with p prf, | |
simp, | |
cases prf, | |
exact prf, | |
exfalso, | |
apply exists.elim prf, intro v, intro prf', | |
apply exists.elim prf', intro c, intro prf'', | |
exact prf'' (acyc v c) | |
end | |
-- Thus paths and walks are equivalent in acyclic digraphs. | |
def path.equiv_walk | |
{V E : Type} [decidable_eq V] {G : digraph V E} | |
{v1 v2 : V} : | |
is_acyclic G → | |
walk G v1 v2 ≃ path G v1 v2 := λ acyc, | |
-- this is an equivalence a ≃ b: a pair of functions a → b and b → a, | |
-- with proofs that they are inverses of each other | |
equiv.mk path.from_walk subtype.val | |
(is_acyclic.from_walk acyc) | |
(λ p, subtype.eq (is_acyclic.from_walk acyc p.val)) | |
-- Since the cycle elimination algorithm does nothing in an acyclic graph, | |
-- lengths are preserved through concatenation | |
lemma path.acyclic_concat_length | |
{V E : Type} [decidable_eq V] {G : digraph V E} | |
{v1 v2 v3 : V} | |
(p1 : path G v1 v2) | |
(p2 : path G v2 v3) : | |
is_acyclic G → | |
path.length (path.concat p1 p2) = path.length p1 + path.length p2 := | |
begin | |
intro acyc, | |
show walk.length (path.concat p1 p2).1 = walk.length p1.1 + walk.length p2.1, | |
show walk.length (path.from_walk (walk.concat p1.1 p2.1)).1 | |
= walk.length p1.1 + walk.length p2.1, | |
rw [is_acyclic.from_walk acyc _], | |
exact walk.concat_length _ _, | |
end | |
-- Okay, done with types! On to the main course: | |
-- The so-called “no-watershed condition”: for any two vertices with arcs | |
-- from(!) a common vertex, there is another vertex that has paths from | |
-- both of those. (Obviously true if {u,v,w} are not distinct.) | |
-- See section 0.2 in http://www-users.math.umn.edu/~dgrinber/comb2/mt3.pdf | |
def no_watershed_condition | |
{V E : Type} (G : digraph V E) : Prop := | |
Π (u v w : V), | |
arc G u v → arc G u w → | |
∃ (t : V), has_path G v t ∧ has_path G w t | |
-- Having the no-watershed condition in an acyclic graph produces the result | |
-- that each vertex has a unique sink; that is, a vertex with a maximal path | |
-- from the other vertex. | |
namespace unique_sink | |
-- Inside a namespace/section, we can use the following commands to declare variables | |
-- used in the definitions/lemmata for this section: | |
-- implicit and explicit arguments | |
variables {V E : Type} (G : digraph V E) | |
variables acyc : is_acyclic G | |
-- and typeclass arguments! these are variables of a special type that have a | |
-- canonical value | |
-- In particular: | |
-- decidable_eq V gives a function Π(v1 v2 : V), (v1 = v2) ∨ (v1 ≠ v2) | |
-- that decides whether two vertices are equal, giving a proof either way | |
-- fintype V asserts that V has finitely many inhabitants, by giving a list | |
-- of them all (actually a finite set `finset`), and a proof that every | |
-- element occurs in it: ∀ v : V, v ∈ univ V | |
variables [decidable_eq V] [decidable_eq E] [fintype V] [fintype E] | |
-- Now that we have G as a fixed local variable, we can use it in notation! | |
-- who doesn't love syntactic sugar?!?!? :D | |
local infix `⟶`:50 := arc G | |
local infix `~⟶`:50 := walk G -- unused | |
local infix `*⟶`:50 := path G | |
local infix `×⟶`:50 := path.maximal G | |
-- and their propositional equivalents | |
local infix `⟼`:50 := has_arc G | |
local infix `*⟼`:50 := has_path G | |
local infix `×⟼`:50 := path.has_maximal G | |
-- In finite acyclic graphs, each vertex will have a longest path; | |
-- if `bounded_by p n` is provided, then the longest path from p is at most n steps. | |
def bounded_by (p : V) (n : ℕ) := ∀ (q : V) (w : p *⟶ q), path.length w ≤ n | |
-- This is the proposition used for induction. It limits the results to | |
-- only pertain to vertices p whose paths away from them have length at most n. | |
def P (n : ℕ) := | |
∀ (p : V), | |
bounded_by G p n → | |
∃! (q : V), p ×⟼ q | |
-- Helper: These are logically equivalent via two existing lemmate... | |
lemma eq_zero_iff_le_zero {n : nat} : n ≤ 0 ↔ n = 0 := | |
iff.intro nat.eq_zero_of_le_zero nat.le_of_eq | |
-- A walk of length 0 obviously connects the same two vertices. | |
lemma length0 {v1 v2 : V} (w : v1 ~⟶ v2) : | |
walk.length w = 0 → v1 = v2 := | |
begin | |
intro h, | |
-- obviously the `empty` case is trivial: | |
cases w, refl, | |
-- but the `step` case is absurd! | |
rw [walk.length_step] at h, | |
exact absurd h (nat.succ_ne_zero (walk.length w_a_1)), | |
end | |
-- Two maximal paths from the same vertex cannot have one be empty and the other | |
-- be nonempty. | |
lemma not_diff_emptiness | |
{p q1 q2 : V} | |
(w1 : p ×⟶ q1) | |
(w2 : p ×⟶ q2) : | |
¬(path.length w1.1 = 0 ∧ path.length w2.1 > 0) := | |
begin | |
intro h, | |
have pq1 := length0 G w1.1.1 h.1, | |
have q1_final := w1.2, | |
rw [← pq1] at q1_final, | |
apply q1_final, | |
let w := w2.1.1, | |
cases hw : w, | |
apply flip absurd (ne_of_gt h.2), | |
show walk.length w = 0, | |
rw hw, exact walk.length_empty, | |
apply exists.intro a.1, | |
have x : (arcT.mk p v2).1 = p, refl, | |
rw [← a.2] at x, | |
exact x, | |
end | |
-- ... basically the same as above, but more explicit. | |
lemma same_emptiness | |
{p q1 q2 : V} | |
(w1 : p ×⟶ q1) | |
(w2 : p ×⟶ q2) : | |
(path.length w1.1 = 0 ∧ path.length w2.1 = 0) ∨ | |
(path.length w1.1 > 0 ∧ path.length w2.1 > 0) := | |
begin | |
cases h1 : path.length w1.1, | |
left, constructor, refl, | |
cases h2 : path.length w2.1, refl, | |
apply flip absurd (not_diff_emptiness G w1 w2), | |
constructor, exact h1, | |
have := nat.zero_lt_succ n, | |
rw [← h2] at this, | |
exact this, | |
right, constructor, exact nat.zero_lt_succ n, | |
cases h2 : path.length w2.1, | |
apply flip absurd (not_diff_emptiness G w2 w1), | |
constructor, exact h2, | |
have := nat.zero_lt_succ n, | |
rw [← h1] at this, | |
exact this, | |
exact nat.zero_lt_succ n_1, | |
end | |
-- A proof that each path has finite length, bounded above by the number of vertices | |
lemma finite {p q : V} (w : p *⟶ q) : path.length w + 1 ≤ fintype.card V := | |
begin | |
cases w with w nodup, | |
-- this should really be a separate lemma, but it's easy enough to prove inline... | |
have : walk.length w + 1 = list.length (walk.vertices w), | |
{ | |
induction w, | |
refl, simp [walk.vertices.step _ _ _ rfl], | |
rw add_comm, apply w_ih, | |
rw [walk.vertices.step _ _ _ rfl] at nodup, | |
exact list.nodup_of_nodup_cons nodup, | |
}, | |
rw this, | |
-- now we don't need the fact that it is a list of vertices anymore | |
-- so replace it with a generic `l : list V` in the hypothesis and goal: | |
generalize_hyp h : walk.vertices w = l at nodup, rw h, | |
-- make a finite set from the list | |
let fs : finset V := finset.mk (quotient.mk l) nodup, | |
-- with the same size | |
have : list.length l = finset.card fs := | |
by symmetry; apply quot.lift_beta list.length multiset.card._proof_1, | |
rw this, | |
-- it must be smaller or the same size, since it is a subset | |
exact finset.card_le_of_subset (finset.subset_univ _), | |
end | |
-- A vertex is either final or it has an arc leading from it to some other vertex. | |
-- Relies on [fintype E] | |
def any_arc_from (p : V) : | |
(∃ (v : V), p ⟼ v) ∨ is_final G p := | |
begin | |
let elems : finset E := finset.univ, | |
-- Prove that these are equivalent | |
-- instead of proving each direction separately, we use bijections at each step | |
have : (p ∈ multiset.map (source G) elems.val) ↔ (∃(e : E), source G e = p), | |
symmetry, | |
rw multiset.mem_map, | |
apply exists_congr, | |
intro e, | |
symmetry, | |
apply and_iff_right, | |
exact finset.mem_univ e, | |
-- this is a decidable proposition, so we can determine whether it is true or false | |
by_cases h : p ∈ multiset.map (source G) elems.val, | |
{ | |
rw this at h, | |
left, | |
apply exists.elim h, intro e, intro he, | |
apply exists.intro (target G e), | |
apply exists.intro e, | |
apply prod.eq_iff_fst_eq_snd_eq.2, constructor, | |
show (G.φ e).1 = p, exact he, | |
show (G.φ e).2 = target G e, refl, | |
}, | |
{ | |
rw this at h, | |
right, exact h, | |
}, | |
end | |
-- tactic mode doesn't know to include this variable | |
-- we will use it in the remaining proofs | |
include acyc | |
-- If all paths leading out of a vertex have length 0, it must be a sink | |
def is_final_of_bounded_by_zero (v : V) : | |
bounded_by G v 0 → is_final G v := | |
begin | |
intros len, | |
have := any_arc_from G v, | |
cases this, | |
{ | |
exfalso, | |
apply exists.elim this, intros v1 ha, | |
apply has_arc.elim ha, intro a, | |
let p : v *⟶ v1 := path.from_walk (walk.step a (walk.empty _)), | |
have len0 := len _ p, | |
rw [eq_zero_iff_le_zero] at len0, | |
suffices : path.length p = 1, cc, | |
have : p.1 = walk.step a (walk.empty _), | |
from is_acyclic.from_walk acyc _, | |
unfold path.length, rw this, refl | |
}, | |
exact this, | |
end | |
-- Helper for induction: giving a path from v1 to v2 with length no less | |
-- than m and the fact that all paths from v1 are at most length n+m, means | |
-- that a path from v2 to v3 must have length at most n. | |
lemma reduce_length | |
{v1 v2 v3 : V} | |
{n m : ℕ} | |
(w : v1 *⟶ v2) | |
(p : v2 *⟶ v3) | |
(lw : path.length w ≥ m) | |
(mh : bounded_by G v1 (n+m)) : | |
path.length p ≤ n := | |
begin | |
let wp : v1 *⟶ v3 := path.concat w p, | |
have lwp : path.length wp = path.length w + path.length p, | |
from path.acyclic_concat_length w p acyc, | |
-- rewrite lw at lwp, | |
apply @nat.le_of_add_le_add_left (path.length w) _ _, | |
rw [← lwp, add_comm], | |
transitivity, | |
apply mh v3, | |
exact nat.add_le_add_left lw _, | |
end | |
-- All vertices have a sink. | |
-- This is a smaller proof following the style of the larger proof. | |
-- We prove it by induction for all vertices that are bounded by n, | |
-- then we pick a sufficiently large n to cover all vertices. | |
lemma find_sink.bounded (n : ℕ) (v1 : V) : | |
bounded_by G v1 n → | |
∃ (s : V), v1 ×⟼ s := | |
begin | |
intros len, | |
induction n with n ih generalizing v1, | |
{ | |
apply exists.intro v1, | |
apply and.intro (subtype.exists_of_subtype (path.empty G)), | |
apply is_final_of_bounded_by_zero, assumption, assumption, | |
}, | |
have := any_arc_from G v1, | |
cases this, | |
{ | |
apply exists.elim this, intros v2 ha, | |
apply has_arc.elim ha, intro a, | |
let p : v1 *⟶ v2 := path.from_walk (walk.step a (walk.empty _)), | |
have := ih v2 | |
begin | |
have : path.length p = 1, | |
have : p.1 = walk.step a (walk.empty _), | |
from is_acyclic.from_walk acyc _, | |
unfold path.length, rw this, refl, | |
intros v' p', | |
apply reduce_length G acyc p, | |
exact ge_of_eq this, | |
exact len, | |
end, | |
apply exists.elim this, intros s hm, | |
apply exists.intro s, | |
exact path.has_maximal.concat p hm | |
}, | |
{ | |
apply exists.intro v1, | |
apply and.intro (subtype.exists_of_subtype (path.empty G)), | |
assumption, | |
}, | |
end | |
-- Using `finite`, we pick a sufficient n to bound all vertices | |
lemma find_sink | |
(v1 : V) : | |
∃ (s : V), v1 ×⟼ s := | |
find_sink.bounded G acyc _ v1 | |
begin | |
unfold bounded_by, intros, | |
rw ← nat.pred_succ (path.length w), | |
apply nat.pred_le_pred, | |
apply unique_sink.finite G | |
end | |
-- Final vertices are their own sink | |
lemma P_final (p : V) : is_final G p → ∃!(q : V), p ×⟼ q := | |
begin | |
intro final, apply exists_unique.intro p, | |
constructor, exact subtype.exists_of_subtype (path.empty G), exact final, | |
intro sink, intro has_max, apply subtype_elim_exists has_max.1, | |
intro p, cases p, cases p_val, refl, exfalso, | |
apply final, | |
apply exists.intro p_val_a.1, | |
show (G.φ p_val_a.1).1 = p, | |
rw p_val_a.2, refl | |
end | |
-- The base case, basically applies only to sinks, which are their own | |
-- sink, obviously. | |
lemma P0 : P G 0 := | |
λ p len, P_final _ acyc p (is_final_of_bounded_by_zero _ acyc p len) | |
-- The whole proof together. | |
-- | |
-- Big picture idea: | |
-- For any vertex p with bounded_by G p n, try to find an arc leading out of it, call | |
-- its target v (≠ p). The unique sink can be obtained by applying induction to v. | |
-- But to establish that it is also unique for p, we take any otherSink | |
-- with a maximal path from p, take the first new vertex visited by that path v2 | |
-- and we can apply the watershed condition to an arc p v and an arc p v2 to get t with | |
-- a path v t and a path v2 t. But by using this, the sinks from v, t, and v2 must | |
-- all be the same since we can extend the maximal path from t to be from v or v2. | |
-- This is essentially the statement that connected vertices share the same sink... | |
-- (Obviously the computer requires more details to formalize this and accept | |
-- it as rigorous, but they are explained below.) | |
theorem Pn : no_watershed_condition G → ∀ (n : ℕ), P G n := | |
begin | |
-- introduce the watershed condition as a variable in scope | |
intro watershed, | |
-- this is the variable for induction, named n' since n will be in scope | |
intro n, | |
-- regular induction on natural numbers (just a special case of induction in DTT) | |
induction n with n ih, | |
-- base case, already proved as P0 above | |
exact P0 G acyc, | |
-- this is the vertex of interest | |
intro p, | |
-- and the proof that maximal paths from p have length at most n+1 | |
intro len, | |
-- try to obtain an arc from p | |
have aa := any_arc_from G p, | |
-- this splits it up into the two cases, replacing aa with the result of each case | |
-- (what follows is mostly the proof of the first case, the second takes one line) | |
cases aa, | |
apply exists.elim aa, | |
-- obtain the vertex it goes to, and the proof that there is an arc | |
intro v, assume ha : p ⟼ v, | |
-- obtain some actual arc from p to v | |
apply has_arc.elim ha, assume a : p ⟶ v, | |
-- a funny proof by contradiction that p ≠ v since it is in an acyclic graph | |
-- TODO: factor this out | |
by_cases (p = v), | |
{ | |
let a' : p ⟶ v := a, | |
rw h at a', | |
have := acyc _ (walk.step a' (walk.empty G)), | |
exfalso, | |
exact walk.no_confusion this, | |
}, | |
-- using that we can make a path from the arc ... | |
let start : p *⟶ v := path.from_arc G h a, | |
-- ... of length 1 (duh!) | |
have len1 : path.length start = 1, refl, | |
-- Now we can apply the induction hypothesis onto v, since `start` | |
-- guarantees that the length will go down by 1. | |
have exu := ih v | |
begin | |
intro v', intro p', | |
apply reduce_length G acyc start p' (ge_of_eq len1) len, | |
end, | |
-- Now we unpack the existentials ... | |
apply exists_unique.elim exu, simp, | |
-- ... to obtain the sink, the existence of a maximal path from v to that | |
-- sink, and the proof that that sink is unique | |
intro sink, intro has_maxPathToSink, intro uniq, | |
-- so we know that this is the sink we want, just need to prove it has a | |
-- maximal path ... | |
apply exists_unique.intro sink, | |
apply subtype_elim_exists has_maxPathToSink.1, | |
assume pathToSink : v *⟶ sink, | |
constructor, | |
apply subtype.exists_of_subtype, | |
apply path.concat start pathToSink, | |
exact has_maxPathToSink.2, | |
-- ... and that it is unique. Oh boy. | |
-- Given otherSink and a max path from p to otherSink, prove that they are | |
-- the same sink. | |
intro otherSink, intro has_maxPathToOtherSink, | |
show otherSink = sink, | |
apply path.has_maximal.elim has_maxPathToOtherSink, | |
assume maxPathToOtherSink : p ×⟶ otherSink, | |
-- Argue that the maximal path is nonempty. | |
cases hp : maxPathToOtherSink.1.1, | |
{ | |
exfalso, apply maxPathToOtherSink.2, apply exists.intro a.1, | |
show (G.φ a.1).1 = p, | |
exact (prod.eq_iff_fst_eq_snd_eq.1 (a.2)).1, | |
}, | |
-- Now we have hp : (maxPathToOtherSink.fst).val = walk.step a_1 a_2 | |
-- so a_1 is an arc from p to some v2 | |
-- Apply the watershed condition with a : walk p v, a_1 : walk p v2 | |
have water1 := watershed p v v2 a a_1, | |
-- so there is some t with paths from v to t and from v2 to t | |
apply exists.elim water1, intro t, simp, | |
intro has_vt, intro has_v2t, | |
apply subtype_elim_exists has_vt, intro vt, | |
apply subtype_elim_exists has_v2t, intro v2t, | |
-- now we have another path like starting, but from p to t, via v. | |
let startt : p *⟶ t := path.concat start vt, | |
-- Induction again! | |
have exu2 := ih t | |
begin | |
intro v', intro p', | |
-- startt has length at least 1, since start has length 1, and | |
-- vt may have length greater than 1, and since it is acyclic. | |
have len_ge1 : path.length startt ≥ 1, | |
have : path.length startt = path.length start + path.length vt, | |
from path.acyclic_concat_length start vt acyc, | |
rw len1 at this, | |
transitivity, | |
exact le_of_eq (eq.symm this), | |
rw [add_comm], | |
exact nat.succ_le_succ (nat.zero_le (path.length vt)), | |
apply reduce_length G acyc startt p' len_ge1 len, | |
end, | |
apply exists_unique.elim exu2, simp, | |
-- Oh here is anotherSink! the unique sink from t | |
intro anotherSink, intro has_maxPathToAnotherSink, intro uniq2, | |
-- but anotherSink = sink, since any maxpath can be extended along vt | |
have := uniq _ (path.has_maximal.concat vt has_maxPathToAnotherSink), | |
-- again argue that an arc connects distinct vertices | |
by_cases (p = v2), | |
{ | |
let a' : p ⟶ v2 := a_1, | |
rw h at a', | |
have := acyc _ (walk.step a' (walk.empty G)), | |
exfalso, exact walk.no_confusion this, | |
}, | |
-- So there's a path from p to v2 just using the arc a_1. | |
let startv2 : p *⟶ v2 := path.from_arc G h a_1, | |
-- Induction yet again! last time I promise | |
have exuv2 := ih v2 | |
begin | |
intro v', intro p', | |
apply reduce_length G acyc startv2 p', | |
exact ge_of_eq (rfl : path.length startv2 = 1), | |
exact len, | |
end, | |
apply exists_unique.elim exuv2, simp, | |
-- finalSink is the unique sink from v2 | |
intro finalSink, intro has_maxPathToFinalSink, intro uniqv2, | |
-- but finalSink = otherSink, due to the maxpath a_2 from v2 to otherSink | |
have := uniqv2 otherSink | |
begin | |
-- construct the maxpath | |
constructor, | |
-- we can demonstrate the exact path, but we just need its existence, | |
apply subtype.exists_of_subtype, | |
-- hp : (maxPathToOtherSink.fst).val = walk.step a_1 a_2 | |
-- means that just the walk a_2 creates a path too | |
exact path.vertices.step_back _ _ maxPathToOtherSink.1 hp, | |
-- and is maximal since otherSink is still final | |
exact maxPathToOtherSink.2, | |
end, | |
-- and finalSink = anotherSink, by extending maxpaths along v2t | |
have := uniqv2 anotherSink | |
(path.has_maximal.concat v2t has_maxPathToAnotherSink), | |
-- so the congruence closure calculates that otherSink = sink via | |
-- the equalities in scope | |
cc, | |
-- Oh yeah, and if aa had no arcs, then its a final vertex and we're done. | |
exact P_final G acyc p aa, | |
end | |
end unique_sink | |
-- It turns out that we can pick a sufficient n such that Pn holds for all p | |
theorem unique_sink | |
{V E : Type} {G : digraph V E} | |
[decidable_eq V] [decidable_eq E] [fintype V] [fintype E] : | |
is_acyclic G → no_watershed_condition G → | |
∀ (p : V), ∃! (q : V), path.has_maximal G p q := | |
λ acyc watershed p, unique_sink.Pn G acyc watershed _ p | |
begin | |
unfold unique_sink.bounded_by, intros, | |
rw ← nat.pred_succ (path.length w), | |
apply nat.pred_le_pred, | |
apply unique_sink.finite G | |
end | |
end digraph |
Hi @MonoidMusician, thanks for publishing this! It's too bad Lean doesn't have native digraphs yet, so this is a great "workaround".
Would it be ok if I used parts of this in a project I'm working on?
Hi @marcusrossel! Feel free to copy and reuse anything here, I just added a CC0 license header to it. Just be warned that it’s my first and only substantial project in Lean, so I don’t know if I made sensible choices in the context of the Lean ecosystem. But I’m pretty sure it’s mathematically/logically correct :)
Hi @marcusrossel! Feel free to copy and reuse anything here, I just added a CC0 license header to it. Just be warned that it’s my first and only substantial project in Lean, so I don’t know if I made sensible choices in the context of the Lean ecosystem. But I’m pretty sure it’s mathematically/logically correct :)
Thanks :) I really only needed the definition for acyclic digraphs, and that seems to be working out well 👍🏼
Thank you so much for writing explanations; it's very helpful.
I had problems using this file with lean 3.23.0 but I managed to repair it in my fork:
https://gist.github.com/NicolasRouquette/7cdbce09e105e0469fe48221a9e7f812