Skip to content

Instantly share code, notes, and snippets.

@MonoidMusician
Last active November 9, 2022 00:48
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save MonoidMusician/1c8cc2ec787ebaf91b95d67cbc5c3498 to your computer and use it in GitHub Desktop.
Save MonoidMusician/1c8cc2ec787ebaf91b95d67cbc5c3498 to your computer and use it in GitHub Desktop.
Graph Theory: watershed
-- 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
@NicolasRouquette
Copy link

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

@marcusrossel
Copy link

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?

@MonoidMusician
Copy link
Author

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 :)

@marcusrossel
Copy link

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 👍🏼

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment