-- Neutro_derecha.lean
-- Si G es un grupo y a ∈ G, entonces a * 1 = a
-- José A. Alonso Jiménez <https://jaalonso.github.io>
-- Sevilla, 15-septiembre-2022
-- ---------------------------------------------------------------------

-- ---------------------------------------------------------------------
-- Demostrar que si G es un grupo y a ∈ G, entonces
--    a * 1 = a
-- ----------------------------------------------------------------------

import algebra.group
variables {G : Type*} [group G]
variables a : G

-- 1ª demostración
-- ===============

example : a * 1 = a :=
calc
  a * 1 = a * (a⁻¹ * a) : congr_arg (λ x, a * x) (mul_left_inv a).symm
    ... = (a * a⁻¹) * a : (mul_assoc a a⁻¹ a).symm
    ... = 1 * a         : congr_arg (λ x, x* a) (mul_right_inv a)
    ... = a             : one_mul a

-- 2ª demostración
-- ===============

example : a * 1 = a :=
calc
  a * 1 = a * (a⁻¹ * a) : by rw mul_left_inv
    ... = (a * a⁻¹) * a : by rw mul_assoc
    ... = 1 * a         : by rw mul_right_inv
    ... = a             : by rw one_mul

-- 3ª demostración
-- ===============

example : a * 1 = a :=
calc
  a * 1 = a * (a⁻¹ * a) : by simp
    ... = (a * a⁻¹) * a : by simp
    ... = 1 * a         : by simp
    ... = a             : by simp

-- 3ª demostración
-- ===============

example : a * 1 = a :=
by simp

-- 4ª demostración
-- ===============

example : a * 1 = a :=
-- by library_search
mul_one a


/-
Copyright (c) 2021 Martin Zinkevich. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Johannes Hölzl, Martin Zinkevich, Rémy Degenne
-/
import logic.encodable.lattice
import measure_theory.measurable_space_def

/-!
# Induction principles for measurable sets, related to π-systems and λ-systems.

> THIS FILE IS SYNCHRONIZED WITH MATHLIB4.
> Any changes to this file require a corresponding PR to mathlib4.

## Main statements

* The main theorem of this file is Dynkin's π-λ theorem, which appears
  here as an induction principle `induction_on_inter`. Suppose `s` is a
  collection of subsets of `α` such that the intersection of two members
  of `s` belongs to `s` whenever it is nonempty. Let `m` be the σ-algebra
  generated by `s`. In order to check that a predicate `C` holds on every
  member of `m`, it suffices to check that `C` holds on the members of `s` and
  that `C` is preserved by complementation and *disjoint* countable
  unions.

* The proof of this theorem relies on the notion of `is_pi_system`, i.e., a collection of sets
  which is closed under binary non-empty intersections. Note that this is a small variation around
  the usual notion in the literature, which often requires that a π-system is non-empty, and closed
  also under disjoint intersections. This variation turns out to be convenient for the
  formalization.

* The proof of Dynkin's π-λ theorem also requires the notion of `dynkin_system`, i.e., a collection
  of sets which contains the empty set, is closed under complementation and under countable union
  of pairwise disjoint sets. The disjointness condition is the only difference with `σ`-algebras.

* `generate_pi_system g` gives the minimal π-system containing `g`.
  This can be considered a Galois insertion into both measurable spaces and sets.

* `generate_from_generate_pi_system_eq` proves that if you start from a collection of sets `g`,
  take the generated π-system, and then the generated σ-algebra, you get the same result as
  the σ-algebra generated from `g`. This is useful because there are connections between
  independent sets that are π-systems and the generated independent spaces.

* `mem_generate_pi_system_Union_elim` and `mem_generate_pi_system_Union_elim'` show that any
  element of the π-system generated from the union of a set of π-systems can be
  represented as the intersection of a finite number of elements from these sets.

* `pi_Union_Inter` defines a new π-system from a family of π-systems `π : ι → set (set α)` and a
  set of indices `S : set ι`. `pi_Union_Inter π S` is the set of sets that can be written
  as `⋂ x ∈ t, f x` for some finset `t ∈ S` and sets `f x ∈ π x`.

## Implementation details

* `is_pi_system` is a predicate, not a type. Thus, we don't explicitly define the galois
  insertion, nor do we define a complete lattice. In theory, we could define a complete
  lattice and galois insertion on the subtype corresponding to `is_pi_system`.
-/

open measurable_space set
open_locale classical measure_theory

/-- A π-system is a collection of subsets of `α` that is closed under binary intersection of
  non-disjoint sets. Usually it is also required that the collection is nonempty, but we don't do
  that here. -/
def is_pi_system {α} (C : set (set α)) : Prop :=
∀ s t ∈ C, (s ∩ t : set α).nonempty → s ∩ t ∈ C

namespace measurable_space

lemma is_pi_system_measurable_set {α:Type*} [measurable_space α] :
  is_pi_system {s : set α | measurable_set s} :=
λ s hs t ht _, hs.inter ht

end measurable_space

lemma is_pi_system.singleton {α} (S : set α) : is_pi_system ({S} : set (set α)) :=
begin
  intros s h_s t h_t h_ne,
  rw [set.mem_singleton_iff.1 h_s, set.mem_singleton_iff.1 h_t, set.inter_self,
      set.mem_singleton_iff],
end

lemma is_pi_system.insert_empty {α} {S : set (set α)} (h_pi : is_pi_system S) :
  is_pi_system (insert ∅ S) :=
begin
  intros s hs t ht hst,
  cases hs,
  { simp [hs], },
  { cases ht,
    { simp [ht], },
    { exact set.mem_insert_of_mem _ (h_pi s hs t ht hst), }, },
end

lemma is_pi_system.insert_univ {α} {S : set (set α)} (h_pi : is_pi_system S) :
  is_pi_system (insert set.univ S) :=
begin
  intros s hs t ht hst,
  cases hs,
  { cases ht; simp [hs, ht], },
  { cases ht,
    { simp [hs, ht], },
    { exact set.mem_insert_of_mem _ (h_pi s hs t ht hst), }, },
end

lemma is_pi_system.comap {α β} {S : set (set β)} (h_pi : is_pi_system S) (f : α → β) :
  is_pi_system {s : set α | ∃ t ∈ S, f ⁻¹' t = s} :=
begin
  rintros _ ⟨s, hs_mem, rfl⟩ _ ⟨t, ht_mem, rfl⟩ hst,
  rw ← set.preimage_inter at hst ⊢,
  refine ⟨s ∩ t, h_pi s hs_mem t ht_mem _, rfl⟩,
  by_contra,
  rw set.not_nonempty_iff_eq_empty at h,
  rw h at hst,
  simpa using hst,
end

lemma is_pi_system_Union_of_directed_le {α ι} (p : ι → set (set α))
  (hp_pi : ∀ n, is_pi_system (p n)) (hp_directed : directed (≤) p) :
  is_pi_system (⋃ n, p n) :=
begin
  intros t1 ht1 t2 ht2 h,
  rw set.mem_Union at ht1 ht2 ⊢,
  cases ht1 with n ht1,
  cases ht2 with m ht2,
  obtain ⟨k, hpnk, hpmk⟩ : ∃ k, p n ≤ p k ∧ p m ≤ p k := hp_directed n m,
  exact ⟨k, hp_pi k t1 (hpnk ht1) t2 (hpmk ht2) h⟩,
end

lemma is_pi_system_Union_of_monotone {α ι} [semilattice_sup ι] (p : ι → set (set α))
  (hp_pi : ∀ n, is_pi_system (p n)) (hp_mono : monotone p) :
  is_pi_system (⋃ n, p n) :=
is_pi_system_Union_of_directed_le p hp_pi (monotone.directed_le hp_mono)

section order

variables {α : Type*} {ι ι' : Sort*} [linear_order α]

lemma is_pi_system_image_Iio (s : set α) : is_pi_system (Iio '' s) :=
begin
  rintro _ ⟨a, ha, rfl⟩ _ ⟨b, hb, rfl⟩ -,
  exact ⟨a ⊓ b, inf_ind a b ha hb, Iio_inter_Iio.symm⟩
end

lemma is_pi_system_Iio : is_pi_system (range Iio : set (set α)) :=
@image_univ α _ Iio ▸ is_pi_system_image_Iio univ

lemma is_pi_system_image_Ioi (s : set α) : is_pi_system (Ioi '' s) :=
@is_pi_system_image_Iio αᵒᵈ _ s

lemma is_pi_system_Ioi : is_pi_system (range Ioi : set (set α)) :=
@image_univ α _ Ioi ▸ is_pi_system_image_Ioi univ

lemma is_pi_system_Ixx_mem {Ixx : α → α → set α} {p : α → α → Prop}
  (Hne : ∀ {a b}, (Ixx a b).nonempty → p a b)
  (Hi : ∀ {a₁ b₁ a₂ b₂}, Ixx a₁ b₁ ∩ Ixx a₂ b₂ = Ixx (max a₁ a₂) (min b₁ b₂)) (s t : set α) :
  is_pi_system {S | ∃ (l ∈ s) (u ∈ t) (hlu : p l u), Ixx l u = S} :=
begin
  rintro _ ⟨l₁, hls₁, u₁, hut₁, hlu₁, rfl⟩ _ ⟨l₂, hls₂, u₂, hut₂, hlu₂, rfl⟩,
  simp only [Hi, ← sup_eq_max, ← inf_eq_min],
  exact λ H, ⟨l₁ ⊔ l₂, sup_ind l₁ l₂ hls₁ hls₂, u₁ ⊓ u₂, inf_ind u₁ u₂ hut₁ hut₂, Hne H, rfl⟩
end

lemma is_pi_system_Ixx {Ixx : α → α → set α} {p : α → α → Prop}
  (Hne : ∀ {a b}, (Ixx a b).nonempty → p a b)
  (Hi : ∀ {a₁ b₁ a₂ b₂}, Ixx a₁ b₁ ∩ Ixx a₂ b₂ = Ixx (max a₁ a₂) (min b₁ b₂))
  (f : ι → α) (g : ι' → α) :
  @is_pi_system α ({S | ∃ i j (h : p (f i) (g j)), Ixx (f i) (g j) = S}) :=
by simpa only [exists_range_iff] using is_pi_system_Ixx_mem @Hne @Hi (range f) (range g)

lemma is_pi_system_Ioo_mem (s t : set α) :
  is_pi_system {S | ∃ (l ∈ s) (u ∈ t) (h : l < u), Ioo l u = S} :=
is_pi_system_Ixx_mem (λ a b ⟨x, hax, hxb⟩, hax.trans hxb) (λ _ _ _ _, Ioo_inter_Ioo) s t

lemma is_pi_system_Ioo (f : ι → α) (g : ι' → α) :
  @is_pi_system α {S | ∃ l u (h : f l < g u), Ioo (f l) (g u) = S} :=
is_pi_system_Ixx (λ a b ⟨x, hax, hxb⟩, hax.trans hxb) (λ _ _ _ _, Ioo_inter_Ioo) f g

lemma is_pi_system_Ioc_mem (s t : set α) :
  is_pi_system {S | ∃ (l ∈ s) (u ∈ t) (h : l < u), Ioc l u = S} :=
is_pi_system_Ixx_mem (λ a b ⟨x, hax, hxb⟩, hax.trans_le hxb) (λ _ _ _ _, Ioc_inter_Ioc) s t

lemma is_pi_system_Ioc (f : ι → α) (g : ι' → α) :
  @is_pi_system α {S | ∃ i j (h : f i < g j), Ioc (f i) (g j) = S} :=
is_pi_system_Ixx (λ a b ⟨x, hax, hxb⟩, hax.trans_le hxb) (λ _ _ _ _, Ioc_inter_Ioc) f g

lemma is_pi_system_Ico_mem (s t : set α) :
  is_pi_system {S | ∃ (l ∈ s) (u ∈ t) (h : l < u), Ico l u = S} :=
is_pi_system_Ixx_mem (λ a b ⟨x, hax, hxb⟩, hax.trans_lt hxb) (λ _ _ _ _, Ico_inter_Ico) s t

lemma is_pi_system_Ico (f : ι → α) (g : ι' → α) :
  @is_pi_system α {S | ∃ i j (h : f i < g j), Ico (f i) (g j) = S} :=
is_pi_system_Ixx (λ a b ⟨x, hax, hxb⟩, hax.trans_lt hxb) (λ _ _ _ _, Ico_inter_Ico) f g

lemma is_pi_system_Icc_mem (s t : set α) :
  is_pi_system {S | ∃ (l ∈ s) (u ∈ t) (h : l ≤ u), Icc l u = S} :=
is_pi_system_Ixx_mem (λ a b, nonempty_Icc.1) (λ _ _ _ _, Icc_inter_Icc) s t

lemma is_pi_system_Icc (f : ι → α) (g : ι' → α) :
  @is_pi_system α {S | ∃ i j (h : f i ≤ g j), Icc (f i) (g j) = S} :=
is_pi_system_Ixx (λ a b, nonempty_Icc.1) (λ _ _ _ _, Icc_inter_Icc) f g

end order

/-- Given a collection `S` of subsets of `α`, then `generate_pi_system S` is the smallest
π-system containing `S`. -/
inductive generate_pi_system {α} (S : set (set α)) : set (set α)
| base {s : set α} (h_s : s ∈ S) : generate_pi_system s
| inter {s t : set α} (h_s : generate_pi_system s) (h_t : generate_pi_system t)
  (h_nonempty : (s ∩ t).nonempty) : generate_pi_system (s ∩ t)

lemma is_pi_system_generate_pi_system {α} (S : set (set α)) :
  is_pi_system (generate_pi_system S) :=
λ s h_s t h_t h_nonempty, generate_pi_system.inter h_s h_t h_nonempty

lemma subset_generate_pi_system_self {α} (S : set (set α)) : S ⊆ generate_pi_system S :=
λ s, generate_pi_system.base

lemma generate_pi_system_subset_self {α} {S : set (set α)} (h_S : is_pi_system S) :
  generate_pi_system S ⊆ S :=
begin
  intros x h,
  induction h with s h_s s u h_gen_s h_gen_u h_nonempty h_s h_u,
  { exact h_s, },
  { exact h_S _ h_s _ h_u h_nonempty, },
end

lemma generate_pi_system_eq {α} {S : set (set α)} (h_pi : is_pi_system S) :
  generate_pi_system S = S :=
set.subset.antisymm (generate_pi_system_subset_self h_pi) (subset_generate_pi_system_self S)

lemma generate_pi_system_mono {α} {S T : set (set α)} (hST : S ⊆ T) :
  generate_pi_system S ⊆ generate_pi_system T :=
begin
  intros t ht,
  induction ht with s h_s s u h_gen_s h_gen_u h_nonempty h_s h_u,
  { exact generate_pi_system.base (set.mem_of_subset_of_mem hST h_s),},
  { exact is_pi_system_generate_pi_system T _ h_s _ h_u h_nonempty, },
end

lemma generate_pi_system_measurable_set {α} [M : measurable_space α] {S : set (set α)}
  (h_meas_S : ∀ s ∈ S, measurable_set s) (t : set α)
  (h_in_pi : t ∈ generate_pi_system S) : measurable_set t :=
begin
  induction h_in_pi with s h_s s u h_gen_s h_gen_u h_nonempty h_s h_u,
  { apply h_meas_S _ h_s, },
  { apply measurable_set.inter h_s h_u, },
end

lemma generate_from_measurable_set_of_generate_pi_system {α} {g : set (set α)} (t : set α)
  (ht : t ∈ generate_pi_system g) :
  measurable_set[generate_from g] t :=
@generate_pi_system_measurable_set α (generate_from g) g
  (λ s h_s_in_g, measurable_set_generate_from h_s_in_g) t ht

lemma generate_from_generate_pi_system_eq {α} {g : set (set α)} :
  generate_from (generate_pi_system g) = generate_from g :=
begin
  apply le_antisymm; apply generate_from_le,
  { exact λ t h_t, generate_from_measurable_set_of_generate_pi_system t h_t, },
  { exact λ t h_t, measurable_set_generate_from (generate_pi_system.base h_t), },
end

/- Every element of the π-system generated by the union of a family of π-systems
is a finite intersection of elements from the π-systems.
For an indexed union version, see `mem_generate_pi_system_Union_elim'`. -/
lemma mem_generate_pi_system_Union_elim {α β} {g : β → set (set α)}
  (h_pi : ∀ b, is_pi_system (g b)) (t : set α) (h_t : t ∈ generate_pi_system (⋃ b, g b)) :
  ∃ (T : finset β) (f : β → set α), (t = ⋂ b ∈ T, f b) ∧ (∀ b ∈ T, f b ∈ g b) :=
begin
  induction h_t with s h_s s t' h_gen_s h_gen_t' h_nonempty h_s h_t',
  { rcases h_s with ⟨t', ⟨⟨b, rfl⟩, h_s_in_t'⟩⟩,
    refine ⟨{b}, (λ _, s), _⟩,
    simpa using h_s_in_t', },
  { rcases h_t' with ⟨T_t', ⟨f_t', ⟨rfl, h_t'⟩⟩⟩,
    rcases h_s with ⟨T_s, ⟨f_s, ⟨rfl, h_s⟩ ⟩ ⟩,
    use [(T_s ∪ T_t'), (λ (b:β),
      if (b ∈ T_s) then (if (b ∈ T_t') then (f_s b ∩ (f_t' b)) else (f_s b))
      else (if (b ∈ T_t') then (f_t' b) else (∅ : set α)))],
    split,
    { ext a,
      simp_rw [set.mem_inter_iff, set.mem_Inter, finset.mem_union, or_imp_distrib],
      rw ← forall_and_distrib,
      split; intros h1 b; by_cases hbs : b ∈ T_s; by_cases hbt : b ∈ T_t'; specialize h1 b;
        simp only [hbs, hbt, if_true, if_false, true_implies_iff, and_self, false_implies_iff,
          and_true, true_and] at h1 ⊢,
      all_goals { exact h1, }, },
    intros b h_b,
    split_ifs with hbs hbt hbt,
    { refine h_pi b (f_s b) (h_s b hbs) (f_t' b) (h_t' b hbt) (set.nonempty.mono _ h_nonempty),
      exact set.inter_subset_inter (set.bInter_subset_of_mem hbs) (set.bInter_subset_of_mem hbt), },
    { exact h_s b hbs, },
    { exact h_t' b hbt, },
    { rw finset.mem_union at h_b,
      apply false.elim (h_b.elim hbs hbt), }, },
end

/- Every element of the π-system generated by an indexed union of a family of π-systems
is a finite intersection of elements from the π-systems.
For a total union version, see `mem_generate_pi_system_Union_elim`. -/
lemma mem_generate_pi_system_Union_elim' {α β} {g : β → set (set α)} {s : set β}
  (h_pi : ∀ b ∈ s, is_pi_system (g b)) (t : set α) (h_t : t ∈ generate_pi_system (⋃ b ∈ s, g b)) :
  ∃ (T : finset β) (f : β → set α), (↑T ⊆ s) ∧ (t = ⋂ b ∈ T, f b) ∧ (∀ b ∈ T, f b ∈ g b) :=
begin
  have : t ∈ generate_pi_system (⋃ (b : subtype s), (g ∘ subtype.val) b),
  { suffices h1 : (⋃ (b : subtype s), (g ∘ subtype.val) b) = (⋃ b ∈ s, g b), by rwa h1,
    ext x,
    simp only [exists_prop, set.mem_Union, function.comp_app, subtype.exists, subtype.coe_mk],
    refl },
  rcases @mem_generate_pi_system_Union_elim α (subtype s) (g ∘ subtype.val)
    (λ b, h_pi b.val b.property) t this with ⟨T, ⟨f, ⟨rfl, h_t'⟩⟩⟩,
  refine ⟨T.image subtype.val, function.extend subtype.val f (λ b : β, (∅ : set α)), by simp, _, _⟩,
  { ext a, split;
    { simp only [set.mem_Inter, subtype.forall, finset.set_bInter_finset_image],
      intros h1 b h_b h_b_in_T,
      have h2 := h1 b h_b h_b_in_T,
      revert h2,
      rw subtype.val_injective.extend_apply,
      apply id } },
  { intros b h_b,
    simp_rw [finset.mem_image, exists_prop, subtype.exists,
             exists_and_distrib_right, exists_eq_right] at h_b,
    cases h_b,
    have h_b_alt : b = (subtype.mk b h_b_w).val := rfl,
    rw [h_b_alt, subtype.val_injective.extend_apply],
    apply h_t',
    apply h_b_h },
end

section Union_Inter

variables {α ι : Type*}

/-! ### π-system generated by finite intersections of sets of a π-system family -/

/-- From a set of indices `S : set ι` and a family of sets of sets `π : ι → set (set α)`,
define the set of sets that can be written as `⋂ x ∈ t, f x` for some finset `t ⊆ S` and sets
`f x ∈ π x`. If `π` is a family of π-systems, then it is a π-system. -/
def pi_Union_Inter (π : ι → set (set α)) (S : set ι) : set (set α) :=
{s : set α | ∃ (t : finset ι) (htS : ↑t ⊆ S) (f : ι → set α) (hf : ∀ x, x ∈ t → f x ∈ π x),
  s = ⋂ x ∈ t, f x}

lemma pi_Union_Inter_singleton (π : ι → set (set α)) (i : ι) :
  pi_Union_Inter π {i} = π i ∪ {univ} :=
begin
  ext1 s,
  simp only [pi_Union_Inter, exists_prop, mem_union],
  refine ⟨_, λ h, _⟩,
  { rintros ⟨t, hti, f, hfπ, rfl⟩,
    simp only [subset_singleton_iff, finset.mem_coe] at hti,
    by_cases hi : i ∈ t,
    { have ht_eq_i : t = {i},
      { ext1 x, rw finset.mem_singleton, exact ⟨λ h, hti x h, λ h, h.symm ▸ hi⟩, },
      simp only [ht_eq_i, finset.mem_singleton, Inter_Inter_eq_left],
      exact or.inl (hfπ i hi), },
     { have ht_empty : t = ∅,
      { ext1 x,
        simp only [finset.not_mem_empty, iff_false],
        exact λ hx, hi (hti x hx ▸ hx), },
      simp only [ht_empty, Inter_false, Inter_univ, set.mem_singleton univ, or_true], }, },
  { cases h with hs hs,
    { refine ⟨{i}, _, λ _, s, ⟨λ x hx, _, _⟩⟩,
      { rw finset.coe_singleton, },
      { rw finset.mem_singleton at hx,
        rwa hx, },
      { simp only [finset.mem_singleton, Inter_Inter_eq_left], }, },
    { refine ⟨∅, _⟩,
      simpa only [finset.coe_empty, subset_singleton_iff, mem_empty_iff_false, is_empty.forall_iff,
        implies_true_iff, finset.not_mem_empty, Inter_false, Inter_univ, true_and, exists_const]
        using hs, }, },
end

lemma pi_Union_Inter_singleton_left (s : ι → set α) (S : set ι) :
  pi_Union_Inter (λ i, ({s i} : set (set α))) S
    = {s' : set α | ∃ (t : finset ι) (htS : ↑t ⊆ S), s' = ⋂ i ∈ t, s i} :=
begin
  ext1 s',
  simp_rw [pi_Union_Inter, set.mem_singleton_iff, exists_prop, set.mem_set_of_eq],
  refine ⟨λ h, _, λ ⟨t, htS, h_eq⟩, ⟨t, htS, s, λ _ _, rfl, h_eq⟩⟩,
  obtain ⟨t, htS, f, hft_eq, rfl⟩ := h,
  refine ⟨t, htS, _⟩,
  congr' with i x,
  simp_rw set.mem_Inter,
  exact ⟨λ h hit, by { rw ← hft_eq i hit, exact h hit, },
    λ h hit, by { rw hft_eq i hit, exact h hit, }⟩,
end

lemma generate_from_pi_Union_Inter_singleton_left (s : ι → set α) (S : set ι) :
  generate_from (pi_Union_Inter (λ k, {s k}) S) = generate_from {t | ∃ k ∈ S, s k = t} :=
begin
  refine le_antisymm (generate_from_le _) (generate_from_mono _),
  { rintro _ ⟨I, hI, f, hf, rfl⟩,
    refine finset.measurable_set_bInter _ (λ m hm, measurable_set_generate_from _),
    exact ⟨m, hI hm, (hf m hm).symm⟩, },
  { rintro _ ⟨k, hk, rfl⟩,
    refine ⟨{k}, λ m hm, _, s, λ i hi, _, _⟩,
    { rw [finset.mem_coe, finset.mem_singleton] at hm,
      rwa hm, },
    { exact set.mem_singleton _, },
    { simp only [finset.mem_singleton, set.Inter_Inter_eq_left], }, },
end

/-- If `π` is a family of π-systems, then `pi_Union_Inter π S` is a π-system. -/
lemma is_pi_system_pi_Union_Inter (π : ι → set (set α))
  (hpi : ∀ x, is_pi_system (π x)) (S : set ι) :
  is_pi_system (pi_Union_Inter π S) :=
begin
  rintros t1 ⟨p1, hp1S, f1, hf1m, ht1_eq⟩ t2 ⟨p2, hp2S, f2, hf2m, ht2_eq⟩ h_nonempty,
  simp_rw [pi_Union_Inter, set.mem_set_of_eq],
  let g := λ n, (ite (n ∈ p1) (f1 n) set.univ) ∩ (ite (n ∈ p2) (f2 n) set.univ),
  have hp_union_ss : ↑(p1 ∪ p2) ⊆ S,
  { simp only [hp1S, hp2S, finset.coe_union, union_subset_iff, and_self], },
  use [p1 ∪ p2, hp_union_ss, g],
  have h_inter_eq : t1 ∩ t2 = ⋂ i ∈ p1 ∪ p2, g i,
  { rw [ht1_eq, ht2_eq],
    simp_rw [← set.inf_eq_inter, g],
    ext1 x,
    simp only [inf_eq_inter, mem_inter_iff, mem_Inter, finset.mem_union],
    refine ⟨λ h i hi_mem_union, _, λ h, ⟨λ i hi1, _, λ i hi2, _⟩⟩,
    { split_ifs,
      exacts [⟨h.1 i h_1, h.2 i h_2⟩, ⟨h.1 i h_1, set.mem_univ _⟩,
        ⟨set.mem_univ _, h.2 i h_2⟩, ⟨set.mem_univ _, set.mem_univ _⟩], },
    { specialize h i (or.inl hi1),
      rw if_pos hi1 at h,
      exact h.1, },
    { specialize h i (or.inr hi2),
      rw if_pos hi2 at h,
      exact h.2, }, },
  refine ⟨λ n hn, _, h_inter_eq⟩,
  simp_rw g,
  split_ifs with hn1 hn2,
  { refine hpi n (f1 n) (hf1m n hn1) (f2 n) (hf2m n hn2) (set.nonempty_iff_ne_empty.2 (λ h, _)),
    rw h_inter_eq at h_nonempty,
    suffices h_empty : (⋂ i ∈ p1 ∪ p2, g i) = ∅,
      from (set.not_nonempty_iff_eq_empty.mpr h_empty) h_nonempty,
    refine le_antisymm (set.Inter_subset_of_subset n _) (set.empty_subset _),
    refine set.Inter_subset_of_subset hn _,
    simp_rw [g, if_pos hn1, if_pos hn2],
    exact h.subset, },
  { simp [hf1m n hn1], },
  { simp [hf2m n h], },
  { exact absurd hn (by simp [hn1, h]), },
end

lemma pi_Union_Inter_mono_left {π π' : ι → set (set α)} (h_le : ∀ i, π i ⊆ π' i) (S : set ι) :
  pi_Union_Inter π S ⊆ pi_Union_Inter π' S :=
λ s ⟨t, ht_mem, ft, hft_mem_pi, h_eq⟩, ⟨t, ht_mem, ft, λ x hxt, h_le x (hft_mem_pi x hxt), h_eq⟩

lemma pi_Union_Inter_mono_right {π : ι → set (set α)} {S T : set ι} (hST : S ⊆ T) :
  pi_Union_Inter π S ⊆ pi_Union_Inter π T :=
λ s ⟨t, ht_mem, ft, hft_mem_pi, h_eq⟩, ⟨t, ht_mem.trans hST, ft, hft_mem_pi, h_eq⟩

lemma generate_from_pi_Union_Inter_le {m : measurable_space α}
  (π : ι → set (set α)) (h : ∀ n, generate_from (π n) ≤ m) (S : set ι) :
  generate_from (pi_Union_Inter π S) ≤ m :=
begin
  refine generate_from_le _,
  rintros t ⟨ht_p, ht_p_mem, ft, hft_mem_pi, rfl⟩,
  refine finset.measurable_set_bInter _ (λ x hx_mem, (h x) _ _),
  exact measurable_set_generate_from (hft_mem_pi x hx_mem),
end

lemma subset_pi_Union_Inter {π : ι → set (set α)} {S : set ι} {i : ι} (his : i ∈ S) :
  π i ⊆ pi_Union_Inter π S :=
begin
  have h_ss : {i} ⊆ S,
  { intros j hj, rw mem_singleton_iff at hj, rwa hj, },
  refine subset.trans _ (pi_Union_Inter_mono_right h_ss),
  rw pi_Union_Inter_singleton,
  exact subset_union_left _ _,
end

lemma mem_pi_Union_Inter_of_measurable_set (m : ι → measurable_space α)
  {S : set ι} {i : ι} (hiS : i ∈ S) (s : set α)
  (hs : measurable_set[m i] s) :
  s ∈ pi_Union_Inter (λ n, {s | measurable_set[m n] s}) S :=
subset_pi_Union_Inter hiS hs

lemma le_generate_from_pi_Union_Inter {π : ι → set (set α)} (S : set ι) {x : ι} (hxS : x ∈ S) :
  generate_from (π x) ≤ generate_from (pi_Union_Inter π S) :=
generate_from_mono (subset_pi_Union_Inter hxS)

lemma measurable_set_supr_of_mem_pi_Union_Inter (m : ι → measurable_space α)
  (S : set ι) (t : set α) (ht : t ∈ pi_Union_Inter (λ n, {s | measurable_set[m n] s}) S) :
  measurable_set[⨆ i ∈ S, m i] t :=
begin
  rcases ht with ⟨pt, hpt, ft, ht_m, rfl⟩,
  refine pt.measurable_set_bInter (λ i hi, _),
  suffices h_le : m i ≤ (⨆ i ∈ S, m i), from h_le (ft i) (ht_m i hi),
  have hi' : i ∈ S := hpt hi,
  exact le_supr₂ i hi',
end

lemma generate_from_pi_Union_Inter_measurable_set (m : ι → measurable_space α) (S : set ι) :
  generate_from (pi_Union_Inter (λ n, {s | measurable_set[m n] s}) S) = ⨆ i ∈ S, m i :=
begin
  refine le_antisymm _ _,
  { rw ← @generate_from_measurable_set α (⨆ i ∈ S, m i),
    exact generate_from_mono (measurable_set_supr_of_mem_pi_Union_Inter m S), },
  { refine supr₂_le (λ i hi, _),
    rw ← @generate_from_measurable_set α (m i),
    exact generate_from_mono (mem_pi_Union_Inter_of_measurable_set m hi), },
end

end Union_Inter

namespace measurable_space
variable {α : Type*}

/-! ## Dynkin systems and Π-λ theorem -/

/-- A Dynkin system is a collection of subsets of a type `α` that contains the empty set,
  is closed under complementation and under countable union of pairwise disjoint sets.
  The disjointness condition is the only difference with `σ`-algebras.

  The main purpose of Dynkin systems is to provide a powerful induction rule for σ-algebras
  generated by a collection of sets which is stable under intersection.

  A Dynkin system is also known as a "λ-system" or a "d-system".
-/
structure dynkin_system (α : Type*) :=
(has : set α → Prop)
(has_empty : has ∅)
(has_compl : ∀ {a}, has a → has aᶜ)
(has_Union_nat : ∀ {f : ℕ → set α}, pairwise (disjoint on f) → (∀ i, has (f i)) → has (⋃ i, f i))

namespace dynkin_system

@[ext] lemma ext : ∀ {d₁ d₂ : dynkin_system α}, (∀ s : set α, d₁.has s ↔ d₂.has s) → d₁ = d₂
| ⟨s₁, _, _, _⟩ ⟨s₂, _, _, _⟩ h := have s₁ = s₂, from funext $ assume x, propext $ h x,
  by subst this

variable (d : dynkin_system α)

lemma has_compl_iff {a} : d.has aᶜ ↔ d.has a :=
⟨λ h, by simpa using d.has_compl h, λ h, d.has_compl h⟩

lemma has_univ : d.has univ :=
by simpa using d.has_compl d.has_empty

lemma has_Union {β} [countable β] {f : β → set α} (hd : pairwise (disjoint on f))
  (h : ∀ i, d.has (f i)) : d.has (⋃ i, f i) :=
by { casesI nonempty_encodable β, rw ← encodable.Union_decode₂, exact
  d.has_Union_nat (encodable.Union_decode₂_disjoint_on hd)
    (λ n, encodable.Union_decode₂_cases d.has_empty h) }

theorem has_union {s₁ s₂ : set α}
  (h₁ : d.has s₁) (h₂ : d.has s₂) (h : disjoint s₁ s₂) : d.has (s₁ ∪ s₂) :=
by { rw union_eq_Union, exact
  d.has_Union (pairwise_disjoint_on_bool.2 h) (bool.forall_bool.2 ⟨h₂, h₁⟩) }

lemma has_diff {s₁ s₂ : set α} (h₁ : d.has s₁) (h₂ : d.has s₂) (h : s₂ ⊆ s₁) : d.has (s₁ \ s₂) :=
begin
  apply d.has_compl_iff.1,
  simp [diff_eq, compl_inter],
  exact d.has_union (d.has_compl h₁) h₂ (disjoint_compl_left.mono_right h),
end

instance : has_le (dynkin_system α) :=
{ le          := λ m₁ m₂, m₁.has ≤ m₂.has }

lemma le_def {α} {a b : dynkin_system α} : a ≤ b ↔ a.has ≤ b.has := iff.rfl

instance : partial_order (dynkin_system α) :=
{ le_refl     := assume a b, le_rfl,
  le_trans    := assume a b c hab hbc, le_def.mpr (le_trans hab hbc),
  le_antisymm := assume a b h₁ h₂, ext $ assume s, ⟨h₁ s, h₂ s⟩,
  ..dynkin_system.has_le }

/-- Every measurable space (σ-algebra) forms a Dynkin system -/
def of_measurable_space (m : measurable_space α) : dynkin_system α :=
{ has       := m.measurable_set',
  has_empty := m.measurable_set_empty,
  has_compl := m.measurable_set_compl,
  has_Union_nat := assume f _ hf, m.measurable_set_Union f hf }

lemma of_measurable_space_le_of_measurable_space_iff {m₁ m₂ : measurable_space α} :
  of_measurable_space m₁ ≤ of_measurable_space m₂ ↔ m₁ ≤ m₂ :=
iff.rfl

/-- The least Dynkin system containing a collection of basic sets.
  This inductive type gives the underlying collection of sets. -/
inductive generate_has (s : set (set α)) : set α → Prop
| basic : ∀ t ∈ s, generate_has t
| empty : generate_has ∅
| compl : ∀ {a}, generate_has a → generate_has aᶜ
| Union : ∀ {f : ℕ → set α}, pairwise (disjoint on f) →
    (∀ i, generate_has (f i)) → generate_has (⋃ i, f i)

lemma generate_has_compl {C : set (set α)} {s : set α} : generate_has C sᶜ ↔ generate_has C s :=
by { refine ⟨_, generate_has.compl⟩, intro h, convert generate_has.compl h, simp }

/-- The least Dynkin system containing a collection of basic sets. -/
def generate (s : set (set α)) : dynkin_system α :=
{ has := generate_has s,
  has_empty := generate_has.empty,
  has_compl := assume a, generate_has.compl,
  has_Union_nat := assume f, generate_has.Union }

lemma generate_has_def {C : set (set α)} : (generate C).has = generate_has C := rfl

instance : inhabited (dynkin_system α) := ⟨generate univ⟩

/-- If a Dynkin system is closed under binary intersection, then it forms a `σ`-algebra. -/
def to_measurable_space (h_inter : ∀ s₁ s₂, d.has s₁ → d.has s₂ → d.has (s₁ ∩ s₂)) :=
{ measurable_space .
  measurable_set'      := d.has,
  measurable_set_empty := d.has_empty,
  measurable_set_compl := assume s h, d.has_compl h,
  measurable_set_Union := λ f hf,
    begin
      rw ←Union_disjointed,
      exact d.has_Union (disjoint_disjointed _)
        (λ n, disjointed_rec (λ t i h, h_inter _ _ h $ d.has_compl $ hf i) (hf n)),
    end }

lemma of_measurable_space_to_measurable_space
  (h_inter : ∀ s₁ s₂, d.has s₁ → d.has s₂ → d.has (s₁ ∩ s₂)) :
  of_measurable_space (d.to_measurable_space h_inter) = d :=
ext $ assume s, iff.rfl

/-- If `s` is in a Dynkin system `d`, we can form the new Dynkin system `{s ∩ t | t ∈ d}`. -/
def restrict_on {s : set α} (h : d.has s) : dynkin_system α :=
{ has       := λ t, d.has (t ∩ s),
  has_empty := by simp [d.has_empty],
  has_compl := assume t hts,
    have tᶜ ∩ s = ((t ∩ s)ᶜ) \ sᶜ,
      from set.ext $ assume x, by { by_cases x ∈ s; simp [h] },
    by { rw [this], exact d.has_diff (d.has_compl hts) (d.has_compl h)
      (compl_subset_compl.mpr $ inter_subset_right _ _) },
  has_Union_nat := assume f hd hf,
    begin
      rw [Union_inter],
      refine d.has_Union_nat _ hf,
      exact hd.mono (λ i j,
        disjoint.mono (inter_subset_left _ _) (inter_subset_left _ _)),
    end }

lemma generate_le {s : set (set α)} (h : ∀ t ∈ s, d.has t) : generate s ≤ d :=
λ t ht, ht.rec_on h d.has_empty
  (assume a _ h, d.has_compl h)
  (assume f hd _ hf, d.has_Union hd hf)

lemma generate_has_subset_generate_measurable {C : set (set α)} {s : set α}
  (hs : (generate C).has s) : measurable_set[generate_from C] s :=
generate_le (of_measurable_space (generate_from C)) (λ t, measurable_set_generate_from) s hs

lemma generate_inter {s : set (set α)}
  (hs : is_pi_system s) {t₁ t₂ : set α}
  (ht₁ : (generate s).has t₁) (ht₂ : (generate s).has t₂) : (generate s).has (t₁ ∩ t₂) :=
have generate s ≤ (generate s).restrict_on ht₂,
  from generate_le _ $ assume s₁ hs₁,
  have (generate s).has s₁, from generate_has.basic s₁ hs₁,
  have generate s ≤ (generate s).restrict_on this,
    from generate_le _ $ assume s₂ hs₂,
      show (generate s).has (s₂ ∩ s₁), from
        (s₂ ∩ s₁).eq_empty_or_nonempty.elim
        (λ h,  h.symm ▸ generate_has.empty)
        (λ h, generate_has.basic _ $ hs _ hs₂ _ hs₁ h),
  have (generate s).has (t₂ ∩ s₁), from this _ ht₂,
  show (generate s).has (s₁ ∩ t₂), by rwa [inter_comm],
this _ ht₁

/--
  **Dynkin's π-λ theorem**:
  Given a collection of sets closed under binary intersections, then the Dynkin system it
  generates is equal to the σ-algebra it generates.
  This result is known as the π-λ theorem.
  A collection of sets closed under binary intersection is called a π-system (often requiring
  additionnally that is is non-empty, but we drop this condition in the formalization).
-/
lemma generate_from_eq {s : set (set α)} (hs : is_pi_system s) :
  generate_from s = (generate s).to_measurable_space (λ t₁ t₂, generate_inter hs) :=
le_antisymm
  (generate_from_le $ assume t ht, generate_has.basic t ht)
  (of_measurable_space_le_of_measurable_space_iff.mp $
    by { rw [of_measurable_space_to_measurable_space],
    exact (generate_le _ $ assume t ht, measurable_set_generate_from ht) })

end dynkin_system

theorem induction_on_inter {C : set α → Prop} {s : set (set α)} [m : measurable_space α]
  (h_eq : m = generate_from s) (h_inter : is_pi_system s)
  (h_empty : C ∅) (h_basic : ∀ t ∈ s, C t) (h_compl : ∀ t, measurable_set t → C t → C tᶜ)
  (h_union : ∀ f : ℕ → set α, pairwise (disjoint on f) →
    (∀ i, measurable_set (f i)) → (∀ i, C (f i)) → C (⋃ i, f i)) :
  ∀ ⦃t⦄, measurable_set t → C t :=
have eq : measurable_set = dynkin_system.generate_has s,
  by { rw [h_eq, dynkin_system.generate_from_eq h_inter], refl },
assume t ht,
have dynkin_system.generate_has s t, by rwa [eq] at ht,
this.rec_on h_basic h_empty
  (assume t ht, h_compl t $ by { rw [eq], exact ht })
  (assume f hf ht, h_union f hf $ assume i, by { rw [eq], exact ht _ })

end measurable_space


/-
Copyright (c) 2021 Eric Rodriguez. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Eric Rodriguez
-/
import data.fintype.card
import logic.equiv.fin
import logic.equiv.embedding

/-!
# Number of embeddings

This file establishes the cardinality of `α ↪ β` in full generality.
-/

local notation `|` x `|` := finset.card x
local notation `‖` x `‖` := fintype.card x

open function
open_locale nat big_operators

namespace fintype

lemma card_embedding_eq_of_unique {α β : Type*} [unique α] [fintype β] [fintype (α ↪ β)] :
  ‖α ↪ β‖ = ‖β‖ := card_congr equiv.unique_embedding_equiv_result

/- Establishes the cardinality of the type of all injections between two finite types. -/
@[simp] theorem card_embedding_eq {α β} [fintype α] [fintype β] [fintype (α ↪ β)] :
  ‖α ↪ β‖ = (‖β‖.desc_factorial ‖α‖) :=
begin
  classical,
  unfreezingI { induction ‹fintype α› using fintype.induction_empty_option'
    with α₁ α₂ h₂ e ih α h ih },
  { letI := fintype.of_equiv _ e.symm,
    rw [← card_congr (equiv.embedding_congr e (equiv.refl β)), ih, card_congr e] },
  { rw [card_pempty, nat.desc_factorial_zero, card_eq_one_iff],
    exact ⟨embedding.of_is_empty, λ x, fun_like.ext _ _ is_empty_elim⟩ },
  { rw [card_option, nat.desc_factorial_succ, card_congr (embedding.option_embedding_equiv α β),
      card_sigma, ← ih],
    simp only [fintype.card_compl_set, fintype.card_range, finset.sum_const, finset.card_univ,
      smul_eq_mul, mul_comm] },
end

/- The cardinality of embeddings from an infinite type to a finite type is zero.
This is a re-statement of the pigeonhole principle. -/
@[simp] lemma card_embedding_eq_of_infinite {α β} [infinite α] [fintype β] [fintype (α ↪ β)] :
  ‖α ↪ β‖ = 0 :=
card_eq_zero_iff.mpr function.embedding.is_empty

end fintype


import tactic.gptf
import data.list.sigma

section gptf

example {α} (a : α) : a = a :=
begin
  refl
end

example : ∃ n : ℕ, 8 = 2*n :=
begin
  exact ⟨4, rfl⟩
end

example {P Q R : Prop} : P → (P → R) → R :=
begin
  intro h1, exact λ h2, h2 h1
end

example {p q r : Prop} (h₁ : p) (h₂ : q) : (p ∧ q) ∨ r :=
begin
  exact or.inl ⟨h₁, h₂⟩
end

example {P Q : Prop} : (¬ P) ∧ (¬ Q) → ¬ (P ∨ Q) :=
begin
  exact not_or_distrib.mpr -- `gptf {pfx := "exact"}`
end

example {P Q R : Prop} : (P ∧ Q) → ((P → R) → ¬ (Q → ¬ R)) :=
begin
  rintros ⟨h₁, h₂⟩ h₃, try {exact λ h, h₁ _ h}, rw [imp_not_comm],
  apply not_imp_not.mpr (λ con, _), exact id, apply con, apply h₃,
  apply h₁, exact h₂
end

example (n : ℕ) (m : ℕ) : nat.succ n < nat.succ n + 1  :=
begin
  {[smt] eblast_using  [nat.add_one], exact nat.lt_succ_self _}
end

example : ∀ (F1 F2 F3 : Prop), ((¬F1 ∧ F3) ∨ (F2 ∧ ¬F3)) → (F2 → F1) → (F2 → F3) →  ¬F2 :=
begin
  intros P Q R H₁ H₂ H₃ H₄,
  apply H₁.elim, -- `gptf {pfx := "apply"}`
  { assume h, simp * at * }, -- `gptf`
  cc -- `gptf`
end

example : ∀ (f : nat → Prop), f 2 → ∃ x, f x :=
begin
  exact λ f hf, ⟨_, hf⟩ -- by `gptf {pfx := "exact"}` :D
end

example {G : Type} [group G] (x y z : G) : (x * z) * (z⁻¹ * y) = x * y :=
begin
  simp [mul_assoc]
end
universes u v

example {α : Type u} {β : α → Type v} [_inst_1 : decidable_eq α] {a : α} {l₁ l₂ : list (sigma β)} :
  (list.kerase a l₁).kunion (list.kerase a l₂) = list.kerase a (l₁.kunion l₂) :=
begin
  induction l₁ generalizing l₂, case list.nil { refl }, simp
end

end gptf


-- ----------------------------------------------------
-- Ejercicio. Demostrar o refutar
--    ((∃x, P x) ∨ (∃x, Q x)) ⟷ (∃x, P x ∨ Q x)
-- ----------------------------------------------------

import tactic

variable  (U : Type)
variables (P Q : U → Prop)

-- 1ª demostración
example :
  ((∃x, P x) ∨ (∃x, Q x)) ↔ (∃x, P x ∨ Q x) :=
begin
  split,
  { rintro (⟨a, h1⟩ | ⟨a, h2⟩),
    { use a,
      left,
      exact h1, },
    { use a,
      right,
      exact h2, }},
  { rintro ⟨a, (h3 | h4)⟩,
    { left,
      use a,
      exact h3, },
    { right,
      use a,
      exact h4, }},
end

github-repos/lean/jaalonso-Logica_con_Lean/Logica_con_Lean-beb6765c6ff3c05590a03f45722eda0c815a25cd/src/2_LPO/Ejercicios/((∃x, P x) ∨ (∃x, Q x)) ⟷ (∃x, P x ∨ Q x).lean


import data.real.basic

#check le_refl
#check le_trans
#check @add_le_add

-- BEGIN

example (x : ℝ) : x ≤ x :=
begin
  apply le_refl,
end

/- in this context, the apply and exact tactics are equivalent -/
example (x : ℝ) : x ≤ x :=
begin
  exact le_refl x,
end

example (x y z : ℝ) (h₀ : x ≤ y) (h₁ : y ≤ z) : x ≤ z :=
begin
  apply le_trans,
  { apply h₀ },
  apply h₁,
end

example (x y z : ℝ) (h₀ : x ≤ y) (h₁ : y ≤ z) : x ≤ z :=
begin
  apply le_trans h₀,
  apply h₁,
end

example (x y z : ℝ) (h₀ : x ≤ y) (h₁ : y ≤ z) : x ≤ z :=
begin
  apply le_trans h₀ h₁,
end

example (x y z : ℝ) (h₀ : x ≤ y) (h₁ : y ≤ z) : x ≤ z :=
begin
  exact le_trans h₀ h₁,
end

-- END


import data.polynomial
import data.real.basic
import algebra.big_operators

noncomputable theory
open_locale big_operators

-- The basic building block in the Lagrange polynomial
-- this is (x - b)/(a - b)
def scaled_binomial (a b : ℝ) : polynomial ℝ :=
    ((1 : ℝ)/(a - b)) • (polynomial.X - polynomial.C b)

@[simp] lemma bin_zero (a b : ℝ) (h : a ≠ b) : polynomial.eval b (scaled_binomial a b) = 0 :=
begin
    unfold scaled_binomial, -- doesn't work without this
    rw [polynomial.eval_smul, polynomial.eval_sub, polynomial.eval_X, polynomial.eval_C],
    rw [sub_self, algebra.id.smul_eq_mul, mul_zero],
    done
end

@[simp] lemma bin_one (a b : ℝ) (h : a ≠ b) : polynomial.eval a (scaled_binomial a b) = 1 :=
begin
    unfold scaled_binomial, -- doesn't work without this
    rw [polynomial.eval_smul, polynomial.eval_sub, polynomial.eval_X, polynomial.eval_C],
    rw algebra.id.smul_eq_mul,
    have h1 : a - b ≠ 0, exact sub_ne_zero_of_ne h,
    exact div_mul_cancel 1 h1, done
end

-- This version, using scalar multiplication (`smul`) seems simplest.
-- Must add hypothesis that data points are distinct
def lagrange_interpolant (n : ℕ) (i : ℕ) (xData : ℕ → ℝ) : polynomial ℝ :=
    ∏ j in ( finset.range (n+1) \ {i} ), scaled_binomial (xData i) (xData j)

-- This has been PR'd into mathlib
variables  {ι : Type*} [decidable_eq ι]
lemma polynomial.eval_finset.prod (s : finset ι) (p : ι → polynomial ℝ) (x : ℝ) :
  polynomial.eval x (∏ j in s, p j) = ∏ j in s, polynomial.eval x (p j) :=
begin
    apply finset.induction_on s,
    { repeat {rw finset.prod_empty}, rw polynomial.eval_one },
    intros j s hj hpj,
    have h0 : ∏ i in insert j s, polynomial.eval x (p i) =
            (polynomial.eval x (p j)) * ∏ i in s, polynomial.eval x (p i),
    { apply finset.prod_insert hj },
    rw [h0, ← hpj],
    rw finset.prod_insert hj,
    rw polynomial.eval_mul, done
end

-- The Lagrange interpolant `Lᵢ x` is one for `x = xData i`
@[simp]
lemma lagrange_interpolant_one (n : ℕ) (xData : ℕ → ℝ) (i : ℕ) (hi: i ∈ finset.range (n+1))
    (hne : ∀ (i j : ℕ), i ≠ j → xData i ≠ xData j) :
    polynomial.eval (xData i) (lagrange_interpolant n i xData)= (1:ℝ) :=
begin
    unfold lagrange_interpolant,
    rw polynomial.eval_finset.prod,
    --simp only [bin_one], -- simp fails, need better lemma
    apply finset.prod_eq_one,
    intros j hj,
    have h0 : i ≠ j,
        { intro heq, rw heq at hj,
          rw [finset.mem_sdiff, finset.mem_singleton] at hj,
          exact hj.2 rfl
        },
    exact bin_one (xData i) (xData j) (hne i j h0),
    done
end

-- The Lagrange interpolant `Lᵢ x` is zero for `x = xData j, j ≠ i`
@[simp]
lemma lagrange_interpolant_zero (n : ℕ) (xData : ℕ → ℝ) (i : ℕ) (hi: i ∈ finset.range (n+1))
    (j : ℕ) (hj : j ∈ finset.range (n+1)) (hij : i ≠ j)
    (hne : ∀ (i j : ℕ), i ≠ j → xData i ≠ xData j) :
    polynomial.eval (xData j) (lagrange_interpolant n i xData) = (0:ℝ) :=
begin
    sorry,
end

--------------------- Scratch space below here

#eval (∑ k in ((finset.range 5) \ {0}), k)

-- These will be useful for defining the Lagrange polynomials
def binomial_R (a : ℝ) : polynomial ℝ := polynomial.X - polynomial.C a
-- To work with their values use this technique:
example : polynomial.eval (5 : ℝ) (binomial_R (2:ℝ)) = 3 :=
begin
    unfold binomial_R,  -- otherwise can't rw below
    rw polynomial.eval_sub,
    rw polynomial.eval_C,
    rw polynomial.eval_X,
    norm_num, done
end

-- Must show that one can commute polynomial.eval with finset.prod
-- So a lemma like this would be useful
lemma eval_comm_prod (n : ℕ) (pj : ℕ → polynomial ℝ) (x : ℝ) :
    polynomial.eval x ( ∏ j in finset.range n, pj j) =
    ∏ j in finset.range n, polynomial.eval x (pj j) :=
begin
    induction n with d hd,
    { -- base case n = 0
        repeat { rw finset.prod_range_zero _ },
        rw polynomial.eval_one,
    },
    { -- induction step
        rw [finset.prod_range_succ, polynomial.eval_mul, hd, finset.prod_range_succ  ],
    },
    done
end

-- Maybe even better, working with `smul`? Maybe not!
def lagrange_interpolant_v3 (n : ℕ) (i : ℕ) (xData : ℕ → ℝ): polynomial ℝ :=
    ∏ j in ( finset.range (n+1) \ {i} ),
    ((1 : ℝ)/(xData i - xData j)) • (binomial_R (xData j))

-- Either this way (working with ℕ):
def lagrange_interpolant_v2 (n : ℕ) (i : ℕ) (xData : ℕ → ℝ): polynomial ℝ :=
    ∏ j in ( finset.range (n+1) \ {i} ),
    (binomial_R (xData j)) * polynomial.C (1/(xData i - xData j))

-- Or this way (working with `fin`):
def lagrange_interpolant_v1 (n : ℕ) (i : fin (n+1) ) (xData : fin (n+1) → ℝ): polynomial ℝ :=
    ∏ j in ( finset.fin_range (n+1) \ { i } ),
    binomial_R (xData j) * polynomial.C (1/(xData i - xData j))

-- Check that I can work with this definition
def myX : ℕ → ℝ
| 0     := (1 : ℝ)
| 1     := (2 : ℝ)
| (n+2) := (5 : ℝ)

@[simp] lemma myX_0 : myX 0 = (1:ℝ) := rfl
@[simp] lemma myX_1 : myX 1 = (2:ℝ) := rfl
@[simp] lemma myX_all (n : ℕ): myX (nat.succ (nat.succ n)) = (5:ℝ) := rfl
@[simp] lemma myX_n (n : ℕ) (hn : 1 < n) : myX n = (5:ℝ) :=
begin
    -- should I use rec_on or something else instead of induction?
    induction n with d hd,
    { -- base case
        exfalso, linarith,
    },
    { -- induction step, how to best prove this?
        have h1 : 0 < d,
            rw nat.succ_eq_add_one at hn,
            linarith,
        have h2 : ∃ m : ℕ, d = m.succ,
        use d - 1, rw nat.succ_eq_add_one, omega, -- a little ℕ subtraction problem, thx `omega`!
        cases h2 with m hm,
        rw hm,
        exact myX_all m,
    },
    done
end

-- Maybe I can use this lemma below:
@[simp] lemma finset_range_2_0 : finset.range 2 \ {0} = {1} :=
begin
    have h1 : 2 = nat.succ 1, refl,
    rw [ h1, finset.range_succ, finset.range_one ],
    refl,
end
@[simp] lemma finset_range_2_1 : finset.range 2 \ {1} = {0} :=
begin
    have h1 : 2 = nat.succ 1, refl,
    rw h1,
    rw finset.range_succ,
    rw finset.range_one,
    refl,
end
@[simp] lemma finset_range_20 : finset.range 2 \ {0} = {1} := dec_trivial -- wow!

example : polynomial.eval (1 : ℝ) (lagrange_interpolant 1 0 myX) = 1 :=
begin
    unfold lagrange_interpolant,
    simp * at *, -- still doesn't use the simp lemmas for scaled_binomial
    have h : (1:ℝ) ≠ 2, linarith,
    exact bin_one 1 2 h,
end

-- The first interpolant (i=0) evaluated at the first point (x 0 = 1.0):
example : polynomial.eval (1 : ℝ) (lagrange_interpolant_v2 1 0 myX) = 1 :=
begin
    unfold lagrange_interpolant_v2,
    unfold finset.prod,
    have h1 : 1 + 1 = 2, refl,
    rw h1,
    simp * at *,
    unfold binomial_R,
    rw [polynomial.eval_sub, polynomial.eval_X, polynomial.eval_C],
    norm_num,
end
-- The first interpolant (i=0) evaluated at the first point (x 0 = 1.0):
example : polynomial.eval (1 : ℝ) (lagrange_interpolant_v3 1 0 myX) = (1 : ℝ) :=
begin
    unfold lagrange_interpolant_v3,
    unfold finset.prod,
    have h1 : 1 + 1 = 2, refl,
    rw h1,
    rw finset_range_2_0,
    unfold binomial_R, simp only [one_div_eq_inv, myX_0],
    simp * at *,
    norm_num,
end

-- To remember:
example (j : ℕ) (n : ℕ) (i : fin n) : j = i :=
begin
    sorry,  -- can coerce from `fin n` to `ℕ` but not the other way around. Of course...
end

-- Experiment with products of binomial terms
-- If the interpolation points are placed in a `finset`:
variable x : finset ℝ
#check finset.prod x binomial_R --this works
#check  finset.has_sdiff -- this can be used to remove elements
#check  finset.fin_range -- this returns a finset of `fin k`
-- Probably `fin n` would be even better, is there something similar for `fin`?
#check fin
#check fin.prod_univ_succ
#check  fin.sum_univ_eq_sum_range
#check  list.sum_of_fn
#check  list.prod_of_fn
#check polynomial.coeff_mul_X_sub_C
variables a b : ℝ
#check (binomial_R a) * (binomial_R b)
#check finset.range
#check polynomial.eval_mul

-- #lint-


import data.real.basic

variables a b c : ℝ

#check le_antisymm
#check le_min
#check le_trans
#check max_le
#check min_le_right a b
#check min_le_left b c
#check le_max_left
#check le_max_right

-- BEGIN

example : max a b = max b a :=
begin
  apply le_antisymm,
  { show max a b ≤ max b a,
    apply max_le,
      apply le_max_right,
      apply le_max_left },
  { show max b a ≤ max a b,
    apply max_le,
      apply le_max_right,
      apply le_max_left },
end

example : min (min a b) c = min a (min b c) :=
begin
  apply le_antisymm,
  { apply le_min,
    exact le_trans (min_le_left (min a b) c) (min_le_left a b),
    apply le_min,
    exact le_trans (min_le_left (min a b) c) (min_le_right a b),
    exact min_le_right (min a b) c,
  },
  { apply le_min,
    apply le_min,
    exact min_le_left a (min b c),
    exact le_trans (min_le_right a (min b c)) (min_le_left b c),
    exact le_trans (min_le_right a (min b c)) (min_le_right b c),
  },
end
-- END


/-
Copyright (c) 2021 Riccardo Brasca. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Riccardo Brasca
-/

import linear_algebra.finrank
import linear_algebra.free_module.finite.basic
import linear_algebra.matrix.to_lin

/-!
# Finite and free modules using matrices

We provide some instances for finite and free modules involving matrices.

## Main results

* `module.free.linear_map` : if `M` and `N` are finite and free, then `M →ₗ[R] N` is free.
* `module.finite.of_basis` : A free module with a basis indexed by a `fintype` is finite.
* `module.finite.linear_map` : if `M` and `N` are finite and free, then `M →ₗ[R] N`
  is finite.
-/

universes u v w

variables (R : Type u) (M : Type v) (N : Type w)

namespace module.free

section comm_ring

variables [comm_ring R] [add_comm_group M] [module R M] [module.free R M]
variables [add_comm_group N] [module R N] [module.free R N]

instance linear_map [module.finite R M] [module.finite R N] : module.free R (M →ₗ[R] N) :=
begin
  casesI subsingleton_or_nontrivial R,
  { apply module.free.of_subsingleton' },
  classical,
  exact of_equiv
    (linear_map.to_matrix (module.free.choose_basis R M) (module.free.choose_basis R N)).symm,
end

variables {R}

instance _root_.module.finite.linear_map [module.finite R M] [module.finite R N] :
  module.finite R (M →ₗ[R] N) :=
begin
  casesI subsingleton_or_nontrivial R,
  { apply_instance },
  classical,
  have f := (linear_map.to_matrix (choose_basis R M) (choose_basis R N)).symm,
  exact module.finite.of_surjective f.to_linear_map (linear_equiv.surjective f),
end

end comm_ring

section integer

variables [add_comm_group M] [module.finite ℤ M] [module.free ℤ M]
variables [add_comm_group N] [module.finite ℤ N] [module.free ℤ N]

instance _root_.module.finite.add_monoid_hom : module.finite ℤ (M →+ N) :=
module.finite.equiv (add_monoid_hom_lequiv_int ℤ).symm

instance add_monoid_hom : module.free ℤ (M →+ N) :=
begin
  letI : module.free ℤ (M →ₗ[ℤ] N) := module.free.linear_map _ _ _,
  exact module.free.of_equiv (add_monoid_hom_lequiv_int ℤ).symm
end

end integer

section comm_ring

open finite_dimensional

variables [comm_ring R] [strong_rank_condition R]
variables [add_comm_group M] [module R M] [module.free R M] [module.finite R M]
variables [add_comm_group N] [module R N] [module.free R N] [module.finite R N]

/-- The finrank of `M →ₗ[R] N` is `(finrank R M) * (finrank R N)`. -/
--TODO: this should follow from `linear_equiv.finrank_eq`, that is over a field.
lemma finrank_linear_hom : finrank R (M →ₗ[R] N) = (finrank R M) * (finrank R N) :=
begin
  classical,
  letI := nontrivial_of_invariant_basis_number R,
  have h := (linear_map.to_matrix (choose_basis R M) (choose_basis R N)),
  let b := (matrix.std_basis _ _ _).map h.symm,
  rw [finrank, dim_eq_card_basis b, ← cardinal.mk_fintype, cardinal.mk_to_nat_eq_card, finrank,
    finrank, rank_eq_card_choose_basis_index, rank_eq_card_choose_basis_index,
    cardinal.mk_to_nat_eq_card, cardinal.mk_to_nat_eq_card, fintype.card_prod, mul_comm]
end

end comm_ring

end module.free


/-
Copyright (c) 2018 Chris Hughes. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Chris Hughes, Johannes Hölzl, Scott Morrison, Jens Wagemaker

! This file was ported from Lean 3 source module data.polynomial.derivative
! leanprover-community/mathlib commit bbeb185db4ccee8ed07dc48449414ebfa39cb821
! Please do not edit these lines, except to modify the commit id
! if you have ported upstream changes.
-/
import Mathlib.Algebra.Hom.Iterate
import Mathlib.Data.Polynomial.Eval

/-!
# The derivative map on polynomials

## Main definitions
 * `Polynomial.derivative`: The formal derivative of polynomials, expressed as a linear map.

-/

noncomputable section

open Finset

open BigOperators Classical Polynomial

namespace Polynomial

universe u v w y z

variable {R : Type u} {S : Type v} {T : Type w} {ι : Type y} {A : Type z} {a b : R} {n : ℕ}

section Derivative

section Semiring

variable [Semiring R]

/-- `derivative p` is the formal derivative of the polynomial `p` -/
def derivative : R[X] →ₗ[R] R[X] where
  toFun p := p.sum fun n a => C (a * n) * X ^ (n - 1)
  map_add' p q := by
    dsimp only
    rw [sum_add_index] <;>
      simp only [add_mul, forall_const, RingHom.map_add, eq_self_iff_true, zero_mul,
        RingHom.map_zero]
  map_smul' a p := by
    dsimp; rw [sum_smul_index] <;>
      simp only [mul_sum, ← C_mul', mul_assoc, coeff_C_mul, RingHom.map_mul, forall_const, zero_mul,
        RingHom.map_zero, sum]
#align polynomial.derivative Polynomial.derivative

theorem derivative_apply (p : R[X]) : derivative p = p.sum fun n a => C (a * n) * X ^ (n - 1) :=
  rfl
#align polynomial.derivative_apply Polynomial.derivative_apply

theorem coeff_derivative (p : R[X]) (n : ℕ) : coeff (derivative p) n = coeff p (n + 1) * (n + 1) :=
  by
  rw [derivative_apply]
  simp only [coeff_X_pow, coeff_sum, coeff_C_mul]
  rw [sum, Finset.sum_eq_single (n + 1)]
  simp only [Nat.add_succ_sub_one, add_zero, mul_one, if_true, eq_self_iff_true]; norm_cast
  · intro b
    cases b
    · intros
      rw [Nat.cast_zero, mul_zero, zero_mul]
    · intro _ H
      rw [Nat.succ_sub_one, if_neg (mt (congr_arg Nat.succ) H.symm), mul_zero]
  · rw [if_pos (add_tsub_cancel_right n 1).symm, mul_one, Nat.cast_add, Nat.cast_one,
      mem_support_iff]
    intro h
    push_neg  at h
    simp [h]
#align polynomial.coeff_derivative Polynomial.coeff_derivative

--Porting note: removed `simp`: `simp` can prove it.
theorem derivative_zero : derivative (0 : R[X]) = 0 :=
  derivative.map_zero
#align polynomial.derivative_zero Polynomial.derivative_zero

@[simp]
theorem iterate_derivative_zero {k : ℕ} : (derivative^[k]) (0 : R[X]) = 0 := by
  induction' k with k ih
  · simp
  · simp [ih]
#align polynomial.iterate_derivative_zero Polynomial.iterate_derivative_zero

@[simp]
theorem derivative_monomial (a : R) (n : ℕ) :
    derivative (monomial n a) = monomial (n - 1) (a * n) := by
  rw [derivative_apply, sum_monomial_index, C_mul_X_pow_eq_monomial]
  simp
#align polynomial.derivative_monomial Polynomial.derivative_monomial

theorem derivative_C_mul_X (a : R) : derivative (C a * X) = C a := by
  simp [C_mul_X_eq_monomial, derivative_monomial, Nat.cast_one, mul_one]
set_option linter.uppercaseLean3 false in
#align polynomial.derivative_C_mul_X Polynomial.derivative_C_mul_X

theorem derivative_C_mul_X_pow (a : R) (n : ℕ) :
    derivative (C a * X ^ n) = C (a * n) * X ^ (n - 1) := by
  rw [C_mul_X_pow_eq_monomial, C_mul_X_pow_eq_monomial, derivative_monomial]
set_option linter.uppercaseLean3 false in
#align polynomial.derivative_C_mul_X_pow Polynomial.derivative_C_mul_X_pow

theorem derivative_C_mul_X_sq (a : R) : derivative (C a * X ^ 2) = C (a * 2) * X := by
  rw [derivative_C_mul_X_pow, Nat.cast_two, pow_one]
set_option linter.uppercaseLean3 false in
#align polynomial.derivative_C_mul_X_sq Polynomial.derivative_C_mul_X_sq

@[simp]
theorem derivative_X_pow (n : ℕ) : derivative (X ^ n : R[X]) = C (n : R) * X ^ (n - 1) := by
  convert derivative_C_mul_X_pow (1 : R) n <;> simp
set_option linter.uppercaseLean3 false in
#align polynomial.derivative_X_pow Polynomial.derivative_X_pow

--Porting note: removed `simp`: `simp` can prove it.
theorem derivative_X_sq : derivative (X ^ 2 : R[X]) = C 2 * X := by
  rw [derivative_X_pow, Nat.cast_two, pow_one]
set_option linter.uppercaseLean3 false in
#align polynomial.derivative_X_sq Polynomial.derivative_X_sq

@[simp]
theorem derivative_C {a : R} : derivative (C a) = 0 := by simp [derivative_apply]
set_option linter.uppercaseLean3 false in
#align polynomial.derivative_C Polynomial.derivative_C

theorem derivative_of_natDegree_zero {p : R[X]} (hp : p.natDegree = 0) : derivative p = 0 := by
  rw [eq_C_of_natDegree_eq_zero hp, derivative_C]
#align polynomial.derivative_of_nat_degree_zero Polynomial.derivative_of_natDegree_zero

@[simp]
theorem derivative_X : derivative (X : R[X]) = 1 :=
  (derivative_monomial _ _).trans <| by simp
set_option linter.uppercaseLean3 false in
#align polynomial.derivative_X Polynomial.derivative_X

@[simp]
theorem derivative_one : derivative (1 : R[X]) = 0 :=
  derivative_C
#align polynomial.derivative_one Polynomial.derivative_one

set_option linter.deprecated false in
--Porting note: removed `simp`: `simp` can prove it.
theorem derivative_bit0 {a : R[X]} : derivative (bit0 a) = bit0 (derivative a) := by simp [bit0]
#align polynomial.derivative_bit0 Polynomial.derivative_bit0

set_option linter.deprecated false in
--Porting note: removed `simp`: `simp` can prove it.
theorem derivative_bit1 {a : R[X]} : derivative (bit1 a) = bit0 (derivative a) := by simp [bit1]
#align polynomial.derivative_bit1 Polynomial.derivative_bit1

--Porting note: removed `simp`: `simp` can prove it.
theorem derivative_add {f g : R[X]} : derivative (f + g) = derivative f + derivative g :=
  derivative.map_add f g
#align polynomial.derivative_add Polynomial.derivative_add

--Porting note: removed `simp`: `simp` can prove it.
theorem derivative_X_add_C (c : R) : derivative (X + C c) = 1 := by
  rw [derivative_add, derivative_X, derivative_C, add_zero]
set_option linter.uppercaseLean3 false in
#align polynomial.derivative_X_add_C Polynomial.derivative_X_add_C

@[simp]
theorem iterate_derivative_add {f g : R[X]} {k : ℕ} :
    (derivative^[k]) (f + g) = (derivative^[k]) f + (derivative^[k]) g :=
  derivative.toAddMonoidHom.iterate_map_add _ _ _
#align polynomial.iterate_derivative_add Polynomial.iterate_derivative_add

--Porting note: removed `simp`: `simp` can prove it.
theorem derivative_sum {s : Finset ι} {f : ι → R[X]} :
    derivative (∑ b in s, f b) = ∑ b in s, derivative (f b) :=
  derivative.map_sum
#align polynomial.derivative_sum Polynomial.derivative_sum

--Porting note: removed `simp`: `simp` can prove it.
theorem derivative_smul {S : Type _} [Monoid S] [DistribMulAction S R] [IsScalarTower S R R] (s : S)
    (p : R[X]) : derivative (s • p) = s • derivative p :=
  derivative.map_smul_of_tower s p
#align polynomial.derivative_smul Polynomial.derivative_smul

@[simp]
theorem iterate_derivative_smul {S : Type _} [Monoid S] [DistribMulAction S R] [IsScalarTower S R R]
    (s : S) (p : R[X]) (k : ℕ) : (derivative^[k]) (s • p) = s • (derivative^[k]) p := by
  induction' k with k ih generalizing p
  · simp
  · simp [ih]
#align polynomial.iterate_derivative_smul Polynomial.iterate_derivative_smul

@[simp]
theorem iterate_derivative_C_mul (a : R) (p : R[X]) (k : ℕ) :
    (derivative^[k]) (C a * p) = C a * (derivative^[k]) p := by
  simp_rw [← smul_eq_C_mul, iterate_derivative_smul]
set_option linter.uppercaseLean3 false in
#align polynomial.iterate_derivative_C_mul Polynomial.iterate_derivative_C_mul

theorem of_mem_support_derivative {p : R[X]} {n : ℕ} (h : n ∈ p.derivative.support) :
    n + 1 ∈ p.support :=
  mem_support_iff.2 fun h1 : p.coeff (n + 1) = 0 =>
    mem_support_iff.1 h <| show p.derivative.coeff n = 0 by rw [coeff_derivative, h1, zero_mul]
#align polynomial.of_mem_support_derivative Polynomial.of_mem_support_derivative

theorem degree_derivative_lt {p : R[X]} (hp : p ≠ 0) : p.derivative.degree < p.degree :=
  (Finset.sup_lt_iff <| bot_lt_iff_ne_bot.2 <| mt degree_eq_bot.1 hp).2 fun n hp =>
    lt_of_lt_of_le (WithBot.some_lt_some.2 n.lt_succ_self) <|
      Finset.le_sup <| of_mem_support_derivative hp
#align polynomial.degree_derivative_lt Polynomial.degree_derivative_lt

theorem degree_derivative_le {p : R[X]} : p.derivative.degree ≤ p.degree :=
  if H : p = 0 then le_of_eq <| by rw [H, derivative_zero] else (degree_derivative_lt H).le
#align polynomial.degree_derivative_le Polynomial.degree_derivative_le

theorem natDegree_derivative_lt {p : R[X]} (hp : p.natDegree ≠ 0) :
    p.derivative.natDegree < p.natDegree := by
  cases' eq_or_ne (derivative p) 0 with hp' hp'
  · rw [hp', Polynomial.natDegree_zero]
    exact hp.bot_lt
  · rw [natDegree_lt_natDegree_iff hp']
    exact degree_derivative_lt fun h => hp (h.symm ▸ natDegree_zero)
#align polynomial.nat_degree_derivative_lt Polynomial.natDegree_derivative_lt

theorem natDegree_derivative_le (p : R[X]) : p.derivative.natDegree ≤ p.natDegree - 1 := by
  by_cases p0 : p.natDegree = 0
  · simp [p0, derivative_of_natDegree_zero]
  · exact Nat.le_pred_of_lt (natDegree_derivative_lt p0)
#align polynomial.nat_degree_derivative_le Polynomial.natDegree_derivative_le

@[simp]
theorem derivative_nat_cast {n : ℕ} : derivative (n : R[X]) = 0 := by
  rw [← map_natCast C n]
  exact derivative_C
#align polynomial.derivative_nat_cast Polynomial.derivative_nat_cast

--Porting note: new theorem
@[simp]
theorem derivative_ofNat (n : ℕ) [n.AtLeastTwo] : derivative (OfNat.ofNat n : R[X]) = 0 :=
  derivative_nat_cast

theorem iterate_derivative_eq_zero {p : R[X]} {x : ℕ} (hx : p.natDegree < x) :
    (Polynomial.derivative^[x]) p = 0 := by
  induction' h : p.natDegree using Nat.strong_induction_on with _ ih generalizing p x
  subst h
  obtain ⟨t, rfl⟩ := Nat.exists_eq_succ_of_ne_zero (pos_of_gt hx).ne'
  rw [Function.iterate_succ_apply]
  by_cases hp : p.natDegree = 0
  · rw [derivative_of_natDegree_zero hp, iterate_derivative_zero]
  have := natDegree_derivative_lt hp
  exact ih _ this (this.trans_le <| Nat.le_of_lt_succ hx) rfl
#align polynomial.iterate_derivative_eq_zero Polynomial.iterate_derivative_eq_zero

@[simp]
theorem iterate_derivative_C {k} (h : 0 < k) : (derivative^[k]) (C a : R[X]) = 0 :=
  iterate_derivative_eq_zero <| (natDegree_C _).trans_lt h
set_option linter.uppercaseLean3 false in
#align polynomial.iterate_derivative_C Polynomial.iterate_derivative_C

@[simp]
theorem iterate_derivative_one {k} (h : 0 < k) : (derivative^[k]) (1 : R[X]) = 0 :=
  iterate_derivative_C h
#align polynomial.iterate_derivative_one Polynomial.iterate_derivative_one

@[simp]
theorem iterate_derivative_x {k} (h : 1 < k) : (derivative^[k]) (X : R[X]) = 0 :=
  iterate_derivative_eq_zero <| natDegree_X_le.trans_lt h
set_option linter.uppercaseLean3 false in
#align polynomial.iterate_derivative_X Polynomial.iterate_derivative_x

theorem natDegree_eq_zero_of_derivative_eq_zero [NoZeroSMulDivisors ℕ R] {f : R[X]}
    (h : derivative f = 0) : f.natDegree = 0 := by
  rcases eq_or_ne f 0 with (rfl | hf)
  · exact natDegree_zero
  rw [natDegree_eq_zero_iff_degree_le_zero]
  by_contra' f_nat_degree_pos
  rw [← natDegree_pos_iff_degree_pos] at f_nat_degree_pos
  let m := f.natDegree - 1
  have hm : m + 1 = f.natDegree := tsub_add_cancel_of_le f_nat_degree_pos
  have h2 := coeff_derivative f m
  rw [Polynomial.ext_iff] at h
  rw [h m, coeff_zero, ← Nat.cast_add_one, ← nsmul_eq_mul', eq_comm, smul_eq_zero] at h2
  replace h2 := h2.resolve_left m.succ_ne_zero
  rw [hm, ← leadingCoeff, leadingCoeff_eq_zero] at h2
  exact hf h2
#align polynomial.nat_degree_eq_zero_of_derivative_eq_zero Polynomial.natDegree_eq_zero_of_derivative_eq_zero

theorem eq_c_of_derivative_eq_zero [NoZeroSMulDivisors ℕ R] {f : R[X]} (h : derivative f = 0) :
    f = C (f.coeff 0) :=
  eq_C_of_natDegree_eq_zero <| natDegree_eq_zero_of_derivative_eq_zero h
set_option linter.uppercaseLean3 false in
#align polynomial.eq_C_of_derivative_eq_zero Polynomial.eq_c_of_derivative_eq_zero

@[simp]
theorem derivative_mul {f g : R[X]} : derivative (f * g) = derivative f * g + f * derivative g :=
  calc
    derivative (f * g) =
        f.sum fun n a => g.sum fun m b => (n + m) • (C (a * b) * X ^ (n + m - 1)) :=
      by
      rw [mul_eq_sum_sum]
      trans; exact derivative_sum
      trans;
      · apply Finset.sum_congr rfl
        intro x _
        exact derivative_sum
      apply Finset.sum_congr rfl; intro n _; apply Finset.sum_congr rfl; intro m _
      trans
      · exact congr_arg _ C_mul_X_pow_eq_monomial.symm
      dsimp; rw [← smul_mul_assoc, smul_C, nsmul_eq_mul']; exact derivative_C_mul_X_pow _ _
    _ =
        f.sum fun n a =>
          g.sum fun m b =>
            n • (C a * X ^ (n - 1)) * (C b * X ^ m) + C a * X ^ n * m • (C b * X ^ (m - 1)) :=
      (sum_congr rfl fun n hn =>
        sum_congr rfl fun m hm => by
          cases n <;> cases m <;>
              simp_rw [add_smul, mul_smul_comm, smul_mul_assoc, X_pow_mul_assoc, ← mul_assoc, ←
                C_mul, mul_assoc, ← pow_add] <;>
            simp [Nat.add_succ, Nat.succ_add, Nat.succ_sub_one, zero_smul, add_comm])
    _ = derivative f * g + f * derivative g :=
      by
      conv =>
        rhs
        congr
        ·rw [← sum_C_mul_X_pow_eq g]
        ·rw [← sum_C_mul_X_pow_eq f]
      simp only [sum, sum_add_distrib, Finset.mul_sum, Finset.sum_mul, derivative_apply]
      simp_rw [← smul_mul_assoc, smul_C, nsmul_eq_mul']
      rw [Finset.sum_comm]
      congr 1
      rw [Finset.sum_comm]
#align polynomial.derivative_mul Polynomial.derivative_mul

theorem derivative_eval (p : R[X]) (x : R) :
    p.derivative.eval x = p.sum fun n a => a * n * x ^ (n - 1) := by
  simp_rw [derivative_apply, eval_sum, eval_mul_X_pow, eval_C]
#align polynomial.derivative_eval Polynomial.derivative_eval

@[simp]
theorem derivative_map [Semiring S] (p : R[X]) (f : R →+* S) :
    derivative (p.map f) = p.derivative.map f := by
  let n := max p.natDegree (map f p).natDegree
  rw [derivative_apply, derivative_apply]
  rw [sum_over_range' _ _ (n + 1) ((le_max_left _ _).trans_lt (lt_add_one _))]
  rw [sum_over_range' _ _ (n + 1) ((le_max_right _ _).trans_lt (lt_add_one _))]
  simp only [Polynomial.map_sum, Polynomial.map_mul, Polynomial.map_C, map_mul, coeff_map,
    map_natCast, Polynomial.map_nat_cast, Polynomial.map_pow, map_X]
  all_goals intro n; rw [zero_mul, C_0, zero_mul]
#align polynomial.derivative_map Polynomial.derivative_map

@[simp]
theorem iterate_derivative_map [Semiring S] (p : R[X]) (f : R →+* S) (k : ℕ) :
    (Polynomial.derivative^[k]) (p.map f) = ((Polynomial.derivative^[k]) p).map f := by
  induction' k with k ih generalizing p
  · simp
  · simp only [ih, Function.iterate_succ, Polynomial.derivative_map, Function.comp_apply]
#align polynomial.iterate_derivative_map Polynomial.iterate_derivative_map

theorem derivative_nat_cast_mul {n : ℕ} {f : R[X]} :
    derivative ((n : R[X]) * f) = n * derivative f := by
  simp
#align polynomial.derivative_nat_cast_mul Polynomial.derivative_nat_cast_mul

@[simp]
theorem iterate_derivative_nat_cast_mul {n k : ℕ} {f : R[X]} :
    (derivative^[k]) ((n : R[X]) * f) = n * (derivative^[k]) f := by
  induction' k with k ih generalizing f <;> simp [*]
#align polynomial.iterate_derivative_nat_cast_mul Polynomial.iterate_derivative_nat_cast_mul

theorem mem_support_derivative [NoZeroSMulDivisors ℕ R] (p : R[X]) (n : ℕ) :
    n ∈ (derivative p).support ↔ n + 1 ∈ p.support := by
  suffices ¬p.coeff (n + 1) * (n + 1 : ℕ) = 0 ↔ coeff p (n + 1) ≠ 0 by
    simpa only [mem_support_iff, coeff_derivative, Ne.def, Nat.cast_succ]
  rw [← nsmul_eq_mul', smul_eq_zero]
  simp only [Nat.succ_ne_zero, false_or_iff]
#align polynomial.mem_support_derivative Polynomial.mem_support_derivative

@[simp]
theorem degree_derivative_eq [NoZeroSMulDivisors ℕ R] (p : R[X]) (hp : 0 < natDegree p) :
    degree (derivative p) = (natDegree p - 1 : ℕ) := by
  apply le_antisymm
  · rw [derivative_apply]
    apply le_trans (degree_sum_le _ _) (Finset.sup_le _)
    intro n hn
    simp only [Nat.cast_withBot]
    apply le_trans (degree_C_mul_X_pow_le _ _) (WithBot.coe_le_coe.2 (tsub_le_tsub_right _ _))
    apply le_natDegree_of_mem_supp _ hn
  · refine' le_sup _
    rw [mem_support_derivative, tsub_add_cancel_of_le, mem_support_iff]
    · show ¬leadingCoeff p = 0
      rw [leadingCoeff_eq_zero]
      intro h
      rw [h, natDegree_zero] at hp
      exact lt_irrefl 0 (lt_of_le_of_lt (zero_le _) hp)
    exact hp
#align polynomial.degree_derivative_eq Polynomial.degree_derivative_eq

theorem coeff_iterate_derivative_as_prod_Ico {k} (p : R[X]) :
    ∀ m : ℕ, ((derivative^[k]) p).coeff m = (∏ i in Ico m.succ (m + k.succ), i) • p.coeff (m + k) :=
  by
  induction' k with k ih
  · simp  [add_zero, forall_const, one_smul, Ico_self, eq_self_iff_true,
      Function.iterate_zero_apply, prod_empty]
  · intro m
    rw [Function.iterate_succ_apply', coeff_derivative, ih (m + 1), ← Nat.cast_add_one, ←
      nsmul_eq_mul', smul_smul, mul_comm]
    apply congr_arg₂
    · have set_eq : Ico m.succ (m + k.succ.succ) = Ico (m + 1).succ (m + 1 + k.succ) ∪ {m + 1} :=
        by
        simp_rw [← Nat.Ico_succ_singleton, union_comm, Nat.succ_eq_add_one, add_comm (k + 1),
          add_assoc]
        rw [Ico_union_Ico_eq_Ico] <;> simp
      rw [set_eq, prod_union, prod_singleton]
      · rw [disjoint_singleton_right, mem_Ico]
        exact fun h => (Nat.lt_succ_self _).not_le h.1
    · exact congr_arg _ (Nat.succ_add m k)
#align polynomial.coeff_iterate_derivative_as_prod_Ico Polynomial.coeff_iterate_derivative_as_prod_Ico

theorem coeff_iterate_derivative_as_prod_range {k} (p : R[X]) :
    ∀ m : ℕ, ((derivative^[k]) p).coeff m = (∏ i in range k, (m + k - i)) • p.coeff (m + k) := by
  induction' k with k ih
  · simp
  intro m
  calc
    ((derivative^[k + 1]) p).coeff m =
        (∏ i in range k, (m + k.succ - i)) • p.coeff (m + k.succ) * (m + 1) :=
      by rw [Function.iterate_succ_apply', coeff_derivative, ih m.succ, Nat.succ_add, Nat.add_succ]
    _ = ((∏ i in range k, (m + k.succ - i)) * (m + 1)) • p.coeff (m + k.succ) := by
      rw [← Nat.cast_add_one, ← nsmul_eq_mul', smul_smul, mul_comm]
    _ = (∏ i in range k.succ, (m + k.succ - i)) • p.coeff (m + k.succ) := by
      rw [prod_range_succ, add_tsub_assoc_of_le k.le_succ, Nat.succ_sub le_rfl, tsub_self]
#align polynomial.coeff_iterate_derivative_as_prod_range Polynomial.coeff_iterate_derivative_as_prod_range

theorem iterate_derivative_mul {n} (p q : R[X]) :
    (derivative^[n]) (p * q) =
      ∑ k in range n.succ, (n.choose k • ((derivative^[n - k]) p * (derivative^[k]) q)) := by
  induction' n with n IH
  · simp [Finset.range]
  calc
    (derivative^[n + 1]) (p * q) =
        derivative (∑ k : ℕ in range n.succ,
            n.choose k • ((derivative^[n - k]) p * (derivative^[k]) q)) :=
      by rw [Function.iterate_succ_apply', IH]
    _ =
        (∑ k : ℕ in range n.succ, n.choose k • ((derivative^[n - k + 1]) p * (derivative^[k]) q)) +
          ∑ k : ℕ in range n.succ, n.choose k • ((derivative^[n - k]) p * (derivative^[k + 1]) q) :=
      by
      simp_rw [derivative_sum, derivative_smul, derivative_mul, Function.iterate_succ_apply',
        smul_add, sum_add_distrib]
    _ =
        (∑ k : ℕ in range n.succ,
              n.choose k.succ • ((derivative^[n - k]) p * (derivative^[k + 1]) q)) +
            1 • ((derivative^[n + 1]) p * (derivative^[0]) q) +
          ∑ k : ℕ in range n.succ, n.choose k • ((derivative^[n - k]) p * (derivative^[k + 1]) q) :=
      ?_
    _ =
        ((∑ k : ℕ in range n.succ, n.choose k • ((derivative^[n - k]) p * (derivative^[k + 1]) q)) +
            ∑ k : ℕ in range n.succ,
              n.choose k.succ • ((derivative^[n - k]) p * (derivative^[k + 1]) q)) +
          1 • ((derivative^[n + 1]) p * (derivative^[0]) q) :=
      by rw [add_comm, add_assoc]
    _ =
        (∑ i : ℕ in range n.succ,
            (n + 1).choose (i + 1) • ((derivative^[n + 1 - (i + 1)]) p * (derivative^[i + 1]) q)) +
          1 • ((derivative^[n + 1]) p * (derivative^[0]) q) :=
      by simp_rw [Nat.choose_succ_succ, Nat.succ_sub_succ, add_smul, sum_add_distrib]
    _ =
        ∑ k : ℕ in range n.succ.succ,
          n.succ.choose k • ((derivative^[n.succ - k]) p * (derivative^[k]) q) :=
      by rw [sum_range_succ' _ n.succ, Nat.choose_zero_right, tsub_zero]

  congr
  refine' (sum_range_succ' _ _).trans (congr_arg₂ (· + ·) _ _)
  · rw [sum_range_succ, Nat.choose_succ_self, zero_smul, add_zero]
    refine' sum_congr rfl fun k hk => _
    rw [mem_range] at hk
    congr
    rw [tsub_add_eq_add_tsub (Nat.succ_le_of_lt hk), Nat.succ_sub_succ]
  · rw [Nat.choose_zero_right, tsub_zero]
#align polynomial.iterate_derivative_mul Polynomial.iterate_derivative_mul

end Semiring

section CommSemiring

variable [CommSemiring R]

theorem derivative_pow_succ (p : R[X]) (n : ℕ) :
    derivative (p ^ (n + 1)) = C (n + 1 : R) * p ^ n * derivative p :=
  Nat.recOn n (by simp) fun n ih => by
    rw [pow_succ', derivative_mul, ih, Nat.add_one, mul_right_comm, C_add,
      add_mul, add_mul, pow_succ', ← mul_assoc, C_1, one_mul]; simp [add_mul]
#align polynomial.derivative_pow_succ Polynomial.derivative_pow_succ

theorem derivative_pow (p : R[X]) (n : ℕ) :
    derivative (p ^ n) = C (n : R) * p ^ (n - 1) * derivative p :=
  Nat.casesOn n (by rw [pow_zero, derivative_one, Nat.cast_zero, C_0, zero_mul, zero_mul]) fun n =>
    by rw [p.derivative_pow_succ n, n.succ_sub_one, n.cast_succ]
#align polynomial.derivative_pow Polynomial.derivative_pow

theorem derivative_sq (p : R[X]) : derivative (p ^ 2) = C 2 * p * derivative p := by
  rw [derivative_pow_succ, Nat.cast_one, one_add_one_eq_two, pow_one]
#align polynomial.derivative_sq Polynomial.derivative_sq

theorem dvd_iterate_derivative_pow (f : R[X]) (n : ℕ) {m : ℕ} (c : R) (hm : m ≠ 0) :
    (n : R) ∣ eval c ((derivative^[m]) (f ^ n)) := by
  obtain ⟨m, rfl⟩ := Nat.exists_eq_succ_of_ne_zero hm
  rw [Function.iterate_succ_apply, derivative_pow, mul_assoc, C_eq_nat_cast,
    iterate_derivative_nat_cast_mul, eval_mul, eval_nat_cast]
  exact dvd_mul_right _ _
#align polynomial.dvd_iterate_derivative_pow Polynomial.dvd_iterate_derivative_pow

theorem iterate_derivative_X_pow_eq_nat_cast_mul (n k : ℕ) :
    (derivative^[k]) (X ^ n : R[X]) = ↑(Nat.descFactorial n k : R[X]) * X ^ (n - k) := by
  induction' k with k ih
  · erw [Function.iterate_zero_apply, tsub_zero, Nat.descFactorial_zero, Nat.cast_one, one_mul]
  · rw [Function.iterate_succ_apply', ih, derivative_nat_cast_mul, derivative_X_pow, C_eq_nat_cast,
      Nat.succ_eq_add_one, Nat.descFactorial_succ, Nat.sub_sub, Nat.cast_mul];
    simp [mul_comm, mul_assoc, mul_left_comm]
set_option linter.uppercaseLean3 false in
#align polynomial.iterate_derivative_X_pow_eq_nat_cast_mul Polynomial.iterate_derivative_X_pow_eq_nat_cast_mul

theorem iterate_derivative_X_pow_eq_C_mul (n k : ℕ) :
    (derivative^[k]) (X ^ n : R[X]) = C (Nat.descFactorial n k : R) * X ^ (n - k) := by
  rw [iterate_derivative_X_pow_eq_nat_cast_mul n k, C_eq_nat_cast]
set_option linter.uppercaseLean3 false in
#align polynomial.iterate_derivative_X_pow_eq_C_mul Polynomial.iterate_derivative_X_pow_eq_C_mul

theorem iterate_derivative_X_pow_eq_smul (n : ℕ) (k : ℕ) :
    (derivative^[k]) (X ^ n : R[X]) = (Nat.descFactorial n k : R) • X ^ (n - k) := by
  rw [iterate_derivative_X_pow_eq_C_mul n k, smul_eq_C_mul]
set_option linter.uppercaseLean3 false in
#align polynomial.iterate_derivative_X_pow_eq_smul Polynomial.iterate_derivative_X_pow_eq_smul

theorem derivative_X_add_C_pow (c : R) (m : ℕ) :
    derivative ((X + C c) ^ m) = C (m : R) * (X + C c) ^ (m - 1) := by
  rw [derivative_pow, derivative_X_add_C, mul_one]
set_option linter.uppercaseLean3 false in
#align polynomial.derivative_X_add_C_pow Polynomial.derivative_X_add_C_pow

theorem derivative_X_add_C_sq (c : R) : derivative ((X + C c) ^ 2) = C 2 * (X + C c) := by
  rw [derivative_sq, derivative_X_add_C, mul_one]
set_option linter.uppercaseLean3 false in
#align polynomial.derivative_X_add_C_sq Polynomial.derivative_X_add_C_sq

theorem iterate_derivative_X_add_pow (n k : ℕ) (c : R) :
    (derivative^[k]) ((X + C c) ^ n) =
     ((∏ i in Finset.range k, (n - i) : ℕ) : R[X]) * (X + C c) ^ (n - k) := by
  induction' k with k IH
  · simp
  · simp only [Function.iterate_succ_apply', IH, derivative_mul, zero_mul, derivative_nat_cast,
      zero_add, Finset.prod_range_succ, C_eq_nat_cast, Nat.sub_sub, ← mul_assoc,
      derivative_X_add_C_pow, Nat.succ_eq_add_one, Nat.cast_mul]
set_option linter.uppercaseLean3 false in
#align polynomial.iterate_derivative_X_add_pow Polynomial.iterate_derivative_X_add_pow

theorem derivative_comp (p q : R[X]) : derivative (p.comp q) = derivative q * p.derivative.comp q :=
  by
  induction p using Polynomial.induction_on'
  · simp [*, mul_add]
  · simp only [derivative_pow, derivative_mul, monomial_comp, derivative_monomial, derivative_C,
      zero_mul, C_eq_nat_cast, zero_add, RingHom.map_mul]
    -- is there a tactic for this? (a multiplicative `abel`):
    rw [mul_comm (derivative q)]
    simp only [mul_assoc]
#align polynomial.derivative_comp Polynomial.derivative_comp

/-- Chain rule for formal derivative of polynomials. -/
theorem derivative_eval₂_C (p q : R[X]) :
    derivative (p.eval₂ C q) = p.derivative.eval₂ C q * derivative q :=
  Polynomial.induction_on p (fun r => by rw [eval₂_C, derivative_C, eval₂_zero, zero_mul])
    (fun p₁ p₂ ih₁ ih₂ => by
      rw [eval₂_add, derivative_add, ih₁, ih₂, derivative_add, eval₂_add, add_mul])
    fun n r ih => by
    rw [pow_succ', ← mul_assoc, eval₂_mul, eval₂_X, derivative_mul, ih, @derivative_mul _ _ _ X,
      derivative_X, mul_one, eval₂_add, @eval₂_mul _ _ _ _ X, eval₂_X, add_mul, mul_right_comm]
set_option linter.uppercaseLean3 false in
#align polynomial.derivative_eval₂_C Polynomial.derivative_eval₂_C

theorem derivative_prod {s : Multiset ι} {f : ι → R[X]} :
    derivative (Multiset.map f s).prod =
      (Multiset.map (fun i => (Multiset.map f (s.erase i)).prod * derivative (f i)) s).sum := by
  refine' Multiset.induction_on s (by simp) fun i s h => _
  rw [Multiset.map_cons, Multiset.prod_cons, derivative_mul, Multiset.map_cons _ i s,
    Multiset.sum_cons, Multiset.erase_cons_head, mul_comm (derivative (f i))]
  congr
  rw [h, ← AddMonoidHom.coe_mul_left, (AddMonoidHom.mulLeft (f i)).map_multiset_sum _,
    AddMonoidHom.coe_mul_left]
  simp only [Function.comp_apply, Multiset.map_map]
  refine' congr_arg _ (Multiset.map_congr rfl fun j hj => _)
  rw [← mul_assoc, ← Multiset.prod_cons, ← Multiset.map_cons]
  by_cases hij : i = j
  · simp [hij, ← Multiset.prod_cons, ← Multiset.map_cons, Multiset.cons_erase hj]
  · simp [hij]
#align polynomial.derivative_prod Polynomial.derivative_prod

end CommSemiring

section Ring

variable [Ring R]

--Porting note: removed `simp`: `simp` can prove it.
theorem derivative_neg (f : R[X]) : derivative (-f) = -derivative f :=
  LinearMap.map_neg derivative f
#align polynomial.derivative_neg Polynomial.derivative_neg

@[simp]
theorem iterate_derivative_neg {f : R[X]} {k : ℕ} : (derivative^[k]) (-f) = -(derivative^[k]) f :=
  (@derivative R _).toAddMonoidHom.iterate_map_neg _ _
#align polynomial.iterate_derivative_neg Polynomial.iterate_derivative_neg

--Porting note: removed `simp`: `simp` can prove it.
theorem derivative_sub {f g : R[X]} : derivative (f - g) = derivative f - derivative g :=
  LinearMap.map_sub derivative f g
#align polynomial.derivative_sub Polynomial.derivative_sub

--Porting note: removed `simp`: `simp` can prove it.

@[simp]
theorem iterate_derivative_sub {k : ℕ} {f g : R[X]} :
    (derivative^[k]) (f - g) = (derivative^[k]) f - (derivative^[k]) g := by
  induction' k with k ih generalizing f g <;> simp [*]
#align polynomial.iterate_derivative_sub Polynomial.iterate_derivative_sub

@[simp]
theorem derivative_int_cast {n : ℤ} : derivative (n : R[X]) = 0 := by
  rw [← C_eq_int_cast n]
  exact derivative_C
#align polynomial.derivative_int_cast Polynomial.derivative_int_cast

theorem derivative_int_cast_mul {n : ℤ} {f : R[X]} : derivative ((n : R[X]) * f) =
    n * derivative f := by
  simp
#align polynomial.derivative_int_cast_mul Polynomial.derivative_int_cast_mul

@[simp]
theorem iterate_derivative_int_cast_mul {n : ℤ} {k : ℕ} {f : R[X]} :
    (derivative^[k]) ((n : R[X]) * f) = n * (derivative^[k]) f := by
  induction' k with k ih generalizing f <;> simp [*]
#align polynomial.iterate_derivative_int_cast_mul Polynomial.iterate_derivative_int_cast_mul

end Ring

section CommRing

variable [CommRing R]

theorem derivative_comp_one_sub_X (p : R[X]) :
    derivative (p.comp (1 - X)) = -p.derivative.comp (1 - X) := by simp [derivative_comp]
set_option linter.uppercaseLean3 false in
#align polynomial.derivative_comp_one_sub_X Polynomial.derivative_comp_one_sub_X

@[simp]
theorem iterate_derivative_comp_one_sub_X (p : R[X]) (k : ℕ) :
    (derivative^[k]) (p.comp (1 - X)) = (-1) ^ k * ((derivative^[k]) p).comp (1 - X) := by
  induction' k with k ih generalizing p
  · simp
  · simp [ih (derivative p), iterate_derivative_neg, derivative_comp, pow_succ]
set_option linter.uppercaseLean3 false in
#align polynomial.iterate_derivative_comp_one_sub_X Polynomial.iterate_derivative_comp_one_sub_X

theorem eval_multiset_prod_X_sub_C_derivative {S : Multiset R} {r : R} (hr : r ∈ S) :
    eval r (derivative (Multiset.map (fun a => X - C a) S).prod) =
      (Multiset.map (fun a => r - a) (S.erase r)).prod := by
  nth_rw 1 [← Multiset.cons_erase hr]
  have := (evalRingHom r).map_multiset_prod (Multiset.map (fun a => X - C a) (S.erase r))
  simpa using this
set_option linter.uppercaseLean3 false in
#align polynomial.eval_multiset_prod_X_sub_C_derivative Polynomial.eval_multiset_prod_X_sub_C_derivative

theorem derivative_X_sub_C_pow (c : R) (m : ℕ) :
    derivative ((X - C c) ^ m) = C (m : R) * (X - C c) ^ (m - 1) := by
  rw [derivative_pow, derivative_X_sub_C, mul_one]
set_option linter.uppercaseLean3 false in
#align polynomial.derivative_X_sub_C_pow Polynomial.derivative_X_sub_C_pow

theorem derivative_X_sub_C_sq (c : R) : derivative ((X - C c) ^ 2) = C 2 * (X - C c) := by
  rw [derivative_sq, derivative_X_sub_C, mul_one]
set_option linter.uppercaseLean3 false in
#align polynomial.derivative_X_sub_C_sq Polynomial.derivative_X_sub_C_sq

theorem iterate_derivative_X_sub_pow (n k : ℕ) (c : R) :
    (derivative^[k]) ((X - C c) ^ n) = ((∏ i in Finset.range k, (n - i) : ℕ) : R[X]) *
    (X - C c) ^ (n - k) := by
  rw [sub_eq_add_neg, ← C_neg, iterate_derivative_X_add_pow]
set_option linter.uppercaseLean3 false in
#align polynomial.iterate_derivative_X_sub_pow Polynomial.iterate_derivative_X_sub_pow

end CommRing

end Derivative

end Polynomial


/-
Copyright (c) 2018 Chris Hughes. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Chris Hughes, Bhavik Mehta, Stuart Presnell

! This file was ported from Lean 3 source module data.nat.choose.basic
! leanprover-community/mathlib commit c3291da49cfa65f0d43b094750541c0731edc932
! Please do not edit these lines, except to modify the commit id
! if you have ported upstream changes.
-/
import Mathbin.Data.Nat.Factorial.Basic

/-!
# Binomial coefficients

> THIS FILE IS SYNCHRONIZED WITH MATHLIB4.
> Any changes to this file require a corresponding PR to mathlib4.

This file defines binomial coefficients and proves simple lemmas (i.e. those not
requiring more imports).

## Main definition and results

* `nat.choose`: binomial coefficients, defined inductively
* `nat.choose_eq_factorial_div_factorial`: a proof that `choose n k = n! / (k! * (n - k)!)`
* `nat.choose_symm`: symmetry of binomial coefficients
* `nat.choose_le_succ_of_lt_half_left`: `choose n k` is increasing for small values of `k`
* `nat.choose_le_middle`: `choose n r` is maximised when `r` is `n/2`
* `nat.desc_factorial_eq_factorial_mul_choose`: Relates binomial coefficients to the descending
  factorial. This is used to prove `nat.choose_le_pow` and variants. We provide similar statements
  for the ascending factorial.
* `nat.multichoose`: whereas `choose` counts combinations, `multichoose` counts multicombinations.
The fact that this is indeed the correct counting function for multisets is proved in
`sym.card_sym_eq_multichoose` in `data/sym/card`.
* `nat.multichoose_eq` : a proof that `multichoose n k = (n + k - 1).choose k`.
This is central to the "stars and bars" technique in informal mathematics, where we switch between
counting multisets of size `k` over an alphabet of size `n` to counting strings of `k` elements
("stars") separated by `n-1` dividers ("bars").  See `data/sym/card` for more detail.

## Tags

binomial coefficient, combination, multicombination, stars and bars
-/

open Nat

namespace Nat

#print Nat.choose /-
/-- `choose n k` is the number of `k`-element subsets in an `n`-element set. Also known as binomial
coefficients. -/
def choose : ℕ → ℕ → ℕ
  | _, 0 => 1
  | 0, k + 1 => 0
  | n + 1, k + 1 => choose n k + choose n (k + 1)
#align nat.choose Nat.choose
-/

#print Nat.choose_zero_right /-
@[simp]
theorem choose_zero_right (n : ℕ) : choose n 0 = 1 := by cases n <;> rfl
#align nat.choose_zero_right Nat.choose_zero_right
-/

#print Nat.choose_zero_succ /-
@[simp]
theorem choose_zero_succ (k : ℕ) : choose 0 (succ k) = 0 :=
  rfl
#align nat.choose_zero_succ Nat.choose_zero_succ
-/

#print Nat.choose_succ_succ /-
theorem choose_succ_succ (n k : ℕ) : choose (succ n) (succ k) = choose n k + choose n (succ k) :=
  rfl
#align nat.choose_succ_succ Nat.choose_succ_succ
-/

#print Nat.choose_eq_zero_of_lt /-
theorem choose_eq_zero_of_lt : ∀ {n k}, n < k → choose n k = 0
  | _, 0, hk => absurd hk (by decide)
  | 0, k + 1, hk => choose_zero_succ _
  | n + 1, k + 1, hk => by
    have hnk : n < k := lt_of_succ_lt_succ hk
    have hnk1 : n < k + 1 := lt_of_succ_lt hk
    rw [choose_succ_succ, choose_eq_zero_of_lt hnk, choose_eq_zero_of_lt hnk1]
#align nat.choose_eq_zero_of_lt Nat.choose_eq_zero_of_lt
-/

#print Nat.choose_self /-
@[simp]
theorem choose_self (n : ℕ) : choose n n = 1 := by
  induction n <;> simp [*, choose, choose_eq_zero_of_lt (lt_succ_self _)]
#align nat.choose_self Nat.choose_self
-/

#print Nat.choose_succ_self /-
@[simp]
theorem choose_succ_self (n : ℕ) : choose n (succ n) = 0 :=
  choose_eq_zero_of_lt (lt_succ_self _)
#align nat.choose_succ_self Nat.choose_succ_self
-/

#print Nat.choose_one_right /-
@[simp]
theorem choose_one_right (n : ℕ) : choose n 1 = n := by induction n <;> simp [*, choose, add_comm]
#align nat.choose_one_right Nat.choose_one_right
-/

#print Nat.triangle_succ /-
-- The `n+1`-st triangle number is `n` more than the `n`-th triangle number
theorem triangle_succ (n : ℕ) : (n + 1) * (n + 1 - 1) / 2 = n * (n - 1) / 2 + n :=
  by
  rw [← add_mul_div_left, mul_comm 2 n, ← mul_add, add_tsub_cancel_right, mul_comm]
  cases n <;> rfl; apply zero_lt_succ
#align nat.triangle_succ Nat.triangle_succ
-/

#print Nat.choose_two_right /-
/-- `choose n 2` is the `n`-th triangle number. -/
theorem choose_two_right (n : ℕ) : choose n 2 = n * (n - 1) / 2 :=
  by
  induction' n with n ih
  simp
  · rw [triangle_succ n]
    simp [choose, ih]
    rw [add_comm]
#align nat.choose_two_right Nat.choose_two_right
-/

#print Nat.choose_pos /-
theorem choose_pos : ∀ {n k}, k ≤ n → 0 < choose n k
  | 0, _, hk => by rw [Nat.eq_zero_of_le_zero hk] <;> exact by decide
  | n + 1, 0, hk => by simp <;> exact by decide
  | n + 1, k + 1, hk => by
    rw [choose_succ_succ] <;>
      exact add_pos_of_pos_of_nonneg (choose_pos (le_of_succ_le_succ hk)) (Nat.zero_le _)
#align nat.choose_pos Nat.choose_pos
-/

#print Nat.choose_eq_zero_iff /-
theorem choose_eq_zero_iff {n k : ℕ} : n.choose k = 0 ↔ n < k :=
  ⟨fun h => lt_of_not_ge (mt Nat.choose_pos h.symm.not_lt), Nat.choose_eq_zero_of_lt⟩
#align nat.choose_eq_zero_iff Nat.choose_eq_zero_iff
-/

#print Nat.succ_mul_choose_eq /-
theorem succ_mul_choose_eq : ∀ n k, succ n * choose n k = choose (succ n) (succ k) * succ k
  | 0, 0 => by decide
  | 0, k + 1 => by simp [choose]
  | n + 1, 0 => by simp
  | n + 1, k + 1 => by
    rw [choose_succ_succ (succ n) (succ k), add_mul, ← succ_mul_choose_eq, mul_succ, ←
      succ_mul_choose_eq, add_right_comm, ← mul_add, ← choose_succ_succ, ← succ_mul]
#align nat.succ_mul_choose_eq Nat.succ_mul_choose_eq
-/

#print Nat.choose_mul_factorial_mul_factorial /-
theorem choose_mul_factorial_mul_factorial : ∀ {n k}, k ≤ n → choose n k * k ! * (n - k)! = n !
  | 0, _, hk => by simp [Nat.eq_zero_of_le_zero hk]
  | n + 1, 0, hk => by simp
  | n + 1, succ k, hk => by
    cases' lt_or_eq_of_le hk with hk₁ hk₁
    · have h : choose n k * k.succ ! * (n - k)! = (k + 1) * n ! := by
        rw [← choose_mul_factorial_mul_factorial (le_of_succ_le_succ hk)] <;>
          simp [factorial_succ, mul_comm, mul_left_comm]
      have h₁ : (n - k)! = (n - k) * (n - k.succ)! := by
        rw [← succ_sub_succ, succ_sub (le_of_lt_succ hk₁), factorial_succ]
      have h₂ : choose n (succ k) * k.succ ! * ((n - k) * (n - k.succ)!) = (n - k) * n ! := by
        rw [← choose_mul_factorial_mul_factorial (le_of_lt_succ hk₁)] <;>
          simp [factorial_succ, mul_comm, mul_left_comm, mul_assoc]
      have h₃ : k * n ! ≤ n * n ! := Nat.mul_le_mul_right _ (le_of_succ_le_succ hk)
      rw [choose_succ_succ, add_mul, add_mul, succ_sub_succ, h, h₁, h₂, add_mul, tsub_mul,
        factorial_succ, ← add_tsub_assoc_of_le h₃, add_assoc, ← add_mul, add_tsub_cancel_left,
        add_comm]
    · simp [hk₁, mul_comm, choose, tsub_self]
#align nat.choose_mul_factorial_mul_factorial Nat.choose_mul_factorial_mul_factorial
-/

#print Nat.choose_mul /-
theorem choose_mul {n k s : ℕ} (hkn : k ≤ n) (hsk : s ≤ k) :
    n.choose k * k.choose s = n.choose s * (n - s).choose (k - s) :=
  by
  have h : (n - k)! * (k - s)! * s ! ≠ 0 := by apply_rules [mul_ne_zero, factorial_ne_zero]
  refine' mul_right_cancel₀ h _
  calc
    n.choose k * k.choose s * ((n - k)! * (k - s)! * s !) =
        n.choose k * (k.choose s * s ! * (k - s)!) * (n - k)! :=
      by
      rw [mul_assoc, mul_assoc, mul_assoc, mul_assoc _ s !, mul_assoc, mul_comm (n - k)!,
        mul_comm s !]
    _ = n ! := by
      rw [choose_mul_factorial_mul_factorial hsk, choose_mul_factorial_mul_factorial hkn]
    _ = n.choose s * s ! * ((n - s).choose (k - s) * (k - s)! * (n - s - (k - s))!) := by
      rw [choose_mul_factorial_mul_factorial (tsub_le_tsub_right hkn _),
        choose_mul_factorial_mul_factorial (hsk.trans hkn)]
    _ = n.choose s * (n - s).choose (k - s) * ((n - k)! * (k - s)! * s !) := by
      rw [tsub_tsub_tsub_cancel_right hsk, mul_assoc, mul_left_comm s !, mul_assoc,
        mul_comm (k - s)!, mul_comm s !, mul_right_comm, ← mul_assoc]

#align nat.choose_mul Nat.choose_mul
-/

#print Nat.choose_eq_factorial_div_factorial /-
theorem choose_eq_factorial_div_factorial {n k : ℕ} (hk : k ≤ n) :
    choose n k = n ! / (k ! * (n - k)!) :=
  by
  rw [← choose_mul_factorial_mul_factorial hk, mul_assoc]
  exact (mul_div_left _ (mul_pos (factorial_pos _) (factorial_pos _))).symm
#align nat.choose_eq_factorial_div_factorial Nat.choose_eq_factorial_div_factorial
-/

#print Nat.add_choose /-
theorem add_choose (i j : ℕ) : (i + j).choose j = (i + j)! / (i ! * j !) := by
  rw [choose_eq_factorial_div_factorial (Nat.le_add_left j i), add_tsub_cancel_right, mul_comm]
#align nat.add_choose Nat.add_choose
-/

#print Nat.add_choose_mul_factorial_mul_factorial /-
theorem add_choose_mul_factorial_mul_factorial (i j : ℕ) :
    (i + j).choose j * i ! * j ! = (i + j)! := by
  rw [← choose_mul_factorial_mul_factorial (Nat.le_add_left _ _), add_tsub_cancel_right,
    mul_right_comm]
#align nat.add_choose_mul_factorial_mul_factorial Nat.add_choose_mul_factorial_mul_factorial
-/

#print Nat.factorial_mul_factorial_dvd_factorial /-
theorem factorial_mul_factorial_dvd_factorial {n k : ℕ} (hk : k ≤ n) : k ! * (n - k)! ∣ n ! := by
  rw [← choose_mul_factorial_mul_factorial hk, mul_assoc] <;> exact dvd_mul_left _ _
#align nat.factorial_mul_factorial_dvd_factorial Nat.factorial_mul_factorial_dvd_factorial
-/

#print Nat.factorial_mul_factorial_dvd_factorial_add /-
theorem factorial_mul_factorial_dvd_factorial_add (i j : ℕ) : i ! * j ! ∣ (i + j)! :=
  by
  convert factorial_mul_factorial_dvd_factorial (le.intro rfl)
  rw [add_tsub_cancel_left]
#align nat.factorial_mul_factorial_dvd_factorial_add Nat.factorial_mul_factorial_dvd_factorial_add
-/

#print Nat.choose_symm /-
@[simp]
theorem choose_symm {n k : ℕ} (hk : k ≤ n) : choose n (n - k) = choose n k := by
  rw [choose_eq_factorial_div_factorial hk, choose_eq_factorial_div_factorial (Nat.sub_le _ _),
    tsub_tsub_cancel_of_le hk, mul_comm]
#align nat.choose_symm Nat.choose_symm
-/

#print Nat.choose_symm_of_eq_add /-
theorem choose_symm_of_eq_add {n a b : ℕ} (h : n = a + b) : Nat.choose n a = Nat.choose n b :=
  by
  convert Nat.choose_symm (Nat.le_add_left _ _)
  rw [add_tsub_cancel_right]
#align nat.choose_symm_of_eq_add Nat.choose_symm_of_eq_add
-/

#print Nat.choose_symm_add /-
theorem choose_symm_add {a b : ℕ} : choose (a + b) a = choose (a + b) b :=
  choose_symm_of_eq_add rfl
#align nat.choose_symm_add Nat.choose_symm_add
-/

#print Nat.choose_symm_half /-
theorem choose_symm_half (m : ℕ) : choose (2 * m + 1) (m + 1) = choose (2 * m + 1) m :=
  by
  apply choose_symm_of_eq_add
  rw [add_comm m 1, add_assoc 1 m m, add_comm (2 * m) 1, two_mul m]
#align nat.choose_symm_half Nat.choose_symm_half
-/

#print Nat.choose_succ_right_eq /-
theorem choose_succ_right_eq (n k : ℕ) : choose n (k + 1) * (k + 1) = choose n k * (n - k) :=
  by
  have e : (n + 1) * choose n k = choose n k * (k + 1) + choose n (k + 1) * (k + 1)
  rw [← right_distrib, ← choose_succ_succ, succ_mul_choose_eq]
  rw [← tsub_eq_of_eq_add_rev e, mul_comm, ← mul_tsub, add_tsub_add_eq_tsub_right]
#align nat.choose_succ_right_eq Nat.choose_succ_right_eq
-/

#print Nat.choose_succ_self_right /-
@[simp]
theorem choose_succ_self_right : ∀ n : ℕ, (n + 1).choose n = n + 1
  | 0 => rfl
  | n + 1 => by rw [choose_succ_succ, choose_succ_self_right, choose_self]
#align nat.choose_succ_self_right Nat.choose_succ_self_right
-/

#print Nat.choose_mul_succ_eq /-
theorem choose_mul_succ_eq (n k : ℕ) : n.choose k * (n + 1) = (n + 1).choose k * (n + 1 - k) :=
  by
  induction' k with k ih; · simp
  obtain hk | hk := le_or_lt (k + 1) (n + 1)
  ·
    rw [choose_succ_succ, add_mul, succ_sub_succ, ← choose_succ_right_eq, ← succ_sub_succ, mul_tsub,
      add_tsub_cancel_of_le (Nat.mul_le_mul_left _ hk)]
  rw [choose_eq_zero_of_lt hk, choose_eq_zero_of_lt (n.lt_succ_self.trans hk),
    MulZeroClass.zero_mul, MulZeroClass.zero_mul]
#align nat.choose_mul_succ_eq Nat.choose_mul_succ_eq
-/

#print Nat.ascFactorial_eq_factorial_mul_choose /-
theorem ascFactorial_eq_factorial_mul_choose (n k : ℕ) :
    n.ascFactorial k = k ! * (n + k).choose k :=
  by
  rw [mul_comm]
  apply mul_right_cancel₀ (factorial_ne_zero (n + k - k))
  rw [choose_mul_factorial_mul_factorial, add_tsub_cancel_right, ← factorial_mul_asc_factorial,
    mul_comm]
  exact Nat.le_add_left k n
#align nat.asc_factorial_eq_factorial_mul_choose Nat.ascFactorial_eq_factorial_mul_choose
-/

#print Nat.factorial_dvd_ascFactorial /-
theorem factorial_dvd_ascFactorial (n k : ℕ) : k ! ∣ n.ascFactorial k :=
  ⟨(n + k).choose k, ascFactorial_eq_factorial_mul_choose _ _⟩
#align nat.factorial_dvd_asc_factorial Nat.factorial_dvd_ascFactorial
-/

#print Nat.choose_eq_asc_factorial_div_factorial /-
theorem choose_eq_asc_factorial_div_factorial (n k : ℕ) :
    (n + k).choose k = n.ascFactorial k / k ! :=
  by
  apply mul_left_cancel₀ (factorial_ne_zero k)
  rw [← asc_factorial_eq_factorial_mul_choose]
  exact (Nat.mul_div_cancel' <| factorial_dvd_asc_factorial _ _).symm
#align nat.choose_eq_asc_factorial_div_factorial Nat.choose_eq_asc_factorial_div_factorial
-/

#print Nat.descFactorial_eq_factorial_mul_choose /-
theorem descFactorial_eq_factorial_mul_choose (n k : ℕ) : n.descFactorial k = k ! * n.choose k :=
  by
  obtain h | h := Nat.lt_or_ge n k
  · rw [desc_factorial_eq_zero_iff_lt.2 h, choose_eq_zero_of_lt h, MulZeroClass.mul_zero]
  rw [mul_comm]
  apply mul_right_cancel₀ (factorial_ne_zero (n - k))
  rw [choose_mul_factorial_mul_factorial h, ← factorial_mul_desc_factorial h, mul_comm]
#align nat.desc_factorial_eq_factorial_mul_choose Nat.descFactorial_eq_factorial_mul_choose
-/

#print Nat.factorial_dvd_descFactorial /-
theorem factorial_dvd_descFactorial (n k : ℕ) : k ! ∣ n.descFactorial k :=
  ⟨n.choose k, descFactorial_eq_factorial_mul_choose _ _⟩
#align nat.factorial_dvd_desc_factorial Nat.factorial_dvd_descFactorial
-/

#print Nat.choose_eq_descFactorial_div_factorial /-
theorem choose_eq_descFactorial_div_factorial (n k : ℕ) : n.choose k = n.descFactorial k / k ! :=
  by
  apply mul_left_cancel₀ (factorial_ne_zero k)
  rw [← desc_factorial_eq_factorial_mul_choose]
  exact (Nat.mul_div_cancel' <| factorial_dvd_desc_factorial _ _).symm
#align nat.choose_eq_desc_factorial_div_factorial Nat.choose_eq_descFactorial_div_factorial
-/

/-! ### Inequalities -/

#print Nat.choose_le_succ_of_lt_half_left /-
/-- Show that `nat.choose` is increasing for small values of the right argument. -/
theorem choose_le_succ_of_lt_half_left {r n : ℕ} (h : r < n / 2) : choose n r ≤ choose n (r + 1) :=
  by
  refine' le_of_mul_le_mul_right _ (lt_tsub_iff_left.mpr (lt_of_lt_of_le h (n.div_le_self 2)))
  rw [← choose_succ_right_eq]
  apply Nat.mul_le_mul_left
  rw [← Nat.lt_iff_add_one_le, lt_tsub_iff_left, ← mul_two]
  exact lt_of_lt_of_le (mul_lt_mul_of_pos_right h zero_lt_two) (n.div_mul_le_self 2)
#align nat.choose_le_succ_of_lt_half_left Nat.choose_le_succ_of_lt_half_left
-/

/-- Show that for small values of the right argument, the middle value is largest. -/
private theorem choose_le_middle_of_le_half_left {n r : ℕ} (hr : r ≤ n / 2) :
    choose n r ≤ choose n (n / 2) :=
  decreasingInduction
    (fun _ k a =>
      (eq_or_lt_of_le a).elim (fun t => t.symm ▸ le_rfl) fun h =>
        (choose_le_succ_of_lt_half_left h).trans (k h))
    hr (fun _ => le_rfl) hr
#align nat.choose_le_middle_of_le_half_left nat.choose_le_middle_of_le_half_left

#print Nat.choose_le_middle /-
/-- `choose n r` is maximised when `r` is `n/2`. -/
theorem choose_le_middle (r n : ℕ) : choose n r ≤ choose n (n / 2) :=
  by
  cases' le_or_gt r n with b b
  · cases' le_or_lt r (n / 2) with a h
    · apply choose_le_middle_of_le_half_left a
    · rw [← choose_symm b]
      apply choose_le_middle_of_le_half_left
      rw [div_lt_iff_lt_mul' zero_lt_two] at h
      rw [le_div_iff_mul_le' zero_lt_two, tsub_mul, tsub_le_iff_tsub_le, mul_two,
        add_tsub_cancel_right]
      exact le_of_lt h
  · rw [choose_eq_zero_of_lt b]
    apply zero_le
#align nat.choose_le_middle Nat.choose_le_middle
-/

/-! #### Inequalities about increasing the first argument -/

#print Nat.choose_le_succ /-
theorem choose_le_succ (a c : ℕ) : choose a c ≤ choose a.succ c := by
  cases c <;> simp [Nat.choose_succ_succ]
#align nat.choose_le_succ Nat.choose_le_succ
-/

#print Nat.choose_le_add /-
theorem choose_le_add (a b c : ℕ) : choose a c ≤ choose (a + b) c :=
  by
  induction' b with b_n b_ih
  · simp
  exact le_trans b_ih (choose_le_succ (a + b_n) c)
#align nat.choose_le_add Nat.choose_le_add
-/

#print Nat.choose_le_choose /-
theorem choose_le_choose {a b : ℕ} (c : ℕ) (h : a ≤ b) : choose a c ≤ choose b c :=
  add_tsub_cancel_of_le h ▸ choose_le_add a (b - a) c
#align nat.choose_le_choose Nat.choose_le_choose
-/

#print Nat.choose_mono /-
theorem choose_mono (b : ℕ) : Monotone fun a => choose a b := fun _ _ => choose_le_choose b
#align nat.choose_mono Nat.choose_mono
-/

/-! #### Multichoose

Whereas `choose n k` is the number of subsets of cardinality `k` from a type of cardinality `n`,
`multichoose n k` is the number of multisets of cardinality `k` from a type of cardinality `n`.

Alternatively, whereas `choose n k` counts the number of combinations,
i.e. ways to select `k` items (up to permutation) from `n` items without replacement,
`multichoose n k` counts the number of multicombinations,
i.e. ways to select `k` items (up to permutation) from `n` items with replacement.

Note that `multichoose` is *not* the multinomial coefficient, although it can be computed
in terms of multinomial coefficients. For details see https://mathworld.wolfram.com/Multichoose.html

TODO: Prove that `choose (-n) k = (-1)^k * multichoose n k`,
where `choose` is the generalized binomial coefficient.
<https://github.com/leanprover-community/mathlib/pull/15072#issuecomment-1171415738>

-/

#print Nat.multichoose /-
/--
`multichoose n k` is the number of multisets of cardinality `k` from a type of cardinality `n`. -/
def multichoose : ℕ → ℕ → ℕ
  | _, 0 => 1
  | 0, k + 1 => 0
  | n + 1, k + 1 => multichoose n (k + 1) + multichoose (n + 1) k
#align nat.multichoose Nat.multichoose
-/

#print Nat.multichoose_zero_right /-
@[simp]
theorem multichoose_zero_right (n : ℕ) : multichoose n 0 = 1 := by cases n <;> simp [multichoose]
#align nat.multichoose_zero_right Nat.multichoose_zero_right
-/

#print Nat.multichoose_zero_succ /-
@[simp]
theorem multichoose_zero_succ (k : ℕ) : multichoose 0 (k + 1) = 0 := by simp [multichoose]
#align nat.multichoose_zero_succ Nat.multichoose_zero_succ
-/

#print Nat.multichoose_succ_succ /-
theorem multichoose_succ_succ (n k : ℕ) :
    multichoose (n + 1) (k + 1) = multichoose n (k + 1) + multichoose (n + 1) k := by
  simp [multichoose]
#align nat.multichoose_succ_succ Nat.multichoose_succ_succ
-/

#print Nat.multichoose_one /-
@[simp]
theorem multichoose_one (k : ℕ) : multichoose 1 k = 1 :=
  by
  induction' k with k IH; · simp
  simp [multichoose_succ_succ 0 k, IH]
#align nat.multichoose_one Nat.multichoose_one
-/

#print Nat.multichoose_two /-
@[simp]
theorem multichoose_two (k : ℕ) : multichoose 2 k = k + 1 :=
  by
  induction' k with k IH; · simp
  simp [multichoose_succ_succ 1 k, IH]
  rw [add_comm]
#align nat.multichoose_two Nat.multichoose_two
-/

#print Nat.multichoose_one_right /-
@[simp]
theorem multichoose_one_right (n : ℕ) : multichoose n 1 = n :=
  by
  induction' n with n IH; · simp
  simp [multichoose_succ_succ n 0, IH]
#align nat.multichoose_one_right Nat.multichoose_one_right
-/

#print Nat.multichoose_eq /-
theorem multichoose_eq : ∀ n k : ℕ, multichoose n k = (n + k - 1).choose k
  | _, 0 => by simp
  | 0, k + 1 => by simp
  | n + 1, k + 1 =>
    by
    rw [multichoose_succ_succ, add_comm, Nat.succ_add_sub_one, ← add_assoc, Nat.choose_succ_succ]
    simp [multichoose_eq]
#align nat.multichoose_eq Nat.multichoose_eq
-/

end Nat


import data.fintype

structure finite_graph :=
(vertices : Type)
(vertices_are_finite : fintype vertices)
(edges : vertices → vertices → Prop)
(no_loops : ∀ v : vertices, ¬ (edges v v))
(edges_symm : ∀ v w : vertices, edges v w → edges w v)

open finite_graph

variable {G : finite_graph}

notation : v ` E ` w := edges v w -- notation for edges -- can be anything.

example (v w : G.vertices) : v E w → w E v := sorry -- curses

/-

Can we prove that a graph has a Hamilton cycle or Euler cycle, iff it's connected and all but at most 2 degrees are even
-/


import data.real.basic

theorem indonesia_MO_Probel_4_2017 (x y : ℝ):
(x^100 - y^100 = 2^99 * (x-y)) ∧ (x^200 - y^200 = 2^199 * (x-y)) →
((x,y) = (0,2) ∨  (x,y) = (2,0)) := sorry