A colouring argument in Lean 4

[2026-08-17 Mon] #permalink

Proof by colouring, or parity argument, is an essential tool for problem solving in combinatorics. One of its classical examples solves a puzzle called Tile Swap:

You’re tiling a floor with 4×1 and 2×2 tiles when you accidentally break one. A tile of the other shape is available. Show that it’s not possible to cover the floor by rearranging the tiles.

Here we present a formalized answer by colouring to the problem. The code has been written by Harmonics's Aristotle for Lean v4.28.0: shared link.

The code excerpt:

import Mathlib

open scoped BigOperators
open scoped Real
open scoped Nat
open scoped Classical
open scoped Pointwise

set_option maxHeartbeats 8000000
set_option maxRecDepth 4000
set_option synthInstance.maxHeartbeats 20000
set_option synthInstance.maxSize 128

set_option relaxedAutoImplicit false
set_option autoImplicit false

set_option pp.fullNames true
set_option pp.structureInstances true
set_option pp.coercions.types true
set_option pp.funBinderTypes true
set_option pp.letVarTypes true
set_option pp.piBinderTypes true

set_option grind.warning false

/-!
# Breaking a tile: a 2×2 tile and a 4×1 tile are never interchangeable

Suppose a floor (an arbitrary finite region of the square grid) has been tiled by
`4 × 1` tiles and `2 × 2` tiles, and that one tile breaks.  We show that the floor
cannot be retiled if the broken tile is replaced by a tile of the *other* shape.

The proof is the classical colouring argument: give the cell `(x, y)` the weight
`1 ∈ ZMod 2` when both coordinates are even and `0` otherwise.  Every `2 × 2` tile
carries total weight `1`, while every `4 × 1` tile carries total weight `0`.
Hence the parity of the number of `2 × 2` tiles is determined by the region alone,
and it changes when one `2 × 2` tile is traded for a `4 × 1` tile (or conversely).
-/

namespace BrokenTile

/-- The weight of a cell: `1` if both coordinates are even, `0` otherwise.
(Written multiplicatively in `ZMod 2`.) -/
def wt (c : ℤ × ℤ) : ZMod 2 := (1 + (c.1 : ZMod 2)) * (1 + (c.2 : ZMod 2))

/-- The total weight of a finite set of cells. -/
def wtSum (t : Finset (ℤ × ℤ)) : ZMod 2 := ∑ c ∈ t, wt c

/-- `t` is (the set of cells covered by) a `2 × 2` tile. -/
def IsSquareTile (t : Finset (ℤ × ℤ)) : Prop :=
  ∃ a b : ℤ, t = {(a, b), (a + 1, b), (a, b + 1), (a + 1, b + 1)}

/-- `t` is (the set of cells covered by) a `4 × 1` tile, in either orientation. -/
def IsBarTile (t : Finset (ℤ × ℤ)) : Prop :=
  (∃ a b : ℤ, t = {(a, b), (a + 1, b), (a + 2, b), (a + 3, b)}) ∨
  (∃ a b : ℤ, t = {(a, b), (a, b + 1), (a, b + 2), (a, b + 3)})

/-- `T` is a tiling of the region `S`: a family of pairwise disjoint tiles, each of
which is a `2 × 2` or a `4 × 1` tile, whose union is exactly `S`. -/
def IsTiling (S : Finset (ℤ × ℤ)) (T : Finset (Finset (ℤ × ℤ))) : Prop :=
  (∀ t ∈ T, IsSquareTile t ∨ IsBarTile t) ∧
  (T : Set (Finset (ℤ × ℤ))).PairwiseDisjoint id ∧
  S = T.biUnion id

/-- A `2 × 2` tile has odd weight. -/
theorem wtSum_of_isSquareTile {t : Finset (ℤ × ℤ)} (h : IsSquareTile t) : wtSum t = 1 := by
  obtain ⟨a, b, rfl⟩ := h
  rw [wtSum, Finset.sum_insert (by simp [Prod.ext_iff]), Finset.sum_insert (by simp [Prod.ext_iff]),
    Finset.sum_insert (by simp [Prod.ext_iff]), Finset.sum_singleton]
  simp only [wt]
  push_cast
  generalize ((a : ZMod 2)) = x
  generalize ((b : ZMod 2)) = y
  revert x y
  decide

/-- A `4 × 1` tile has even weight. -/
theorem wtSum_of_isBarTile {t : Finset (ℤ × ℤ)} (h : IsBarTile t) : wtSum t = 0 := by
  rcases h with ⟨a, b, rfl⟩ | ⟨a, b, rfl⟩ <;>
  · rw [wtSum, Finset.sum_insert (by simp [Prod.ext_iff]), Finset.sum_insert (by simp [Prod.ext_iff]),
      Finset.sum_insert (by simp [Prod.ext_iff]), Finset.sum_singleton]
    simp only [wt]
    push_cast
    generalize ((a : ZMod 2)) = x
    generalize ((b : ZMod 2)) = y
    revert x y
    decide

/-- The two tile shapes are genuinely different: no set of cells is both. -/
theorem not_isBarTile_of_isSquareTile {t : Finset (ℤ × ℤ)} (h : IsSquareTile t) :
    ¬ IsBarTile t := by
  intro hb
  have := wtSum_of_isBarTile hb
  rw [wtSum_of_isSquareTile h] at this
  exact one_ne_zero this

/-- The weight of the tiled region equals, mod 2, the number of `2 × 2` tiles used. -/
theorem wtSum_eq_card_squares {S : Finset (ℤ × ℤ)} {T : Finset (Finset (ℤ × ℤ))}
    (h : IsTiling S T) : wtSum S = ((T.filter IsSquareTile).card : ZMod 2) := by
  obtain ⟨hshape, hdisj, rfl⟩ := h
  rw [wtSum, Finset.sum_biUnion hdisj]
  simp only [id_eq]
  rw [← Finset.sum_filter_add_sum_filter_not T IsSquareTile]
  have h1 : ∑ t ∈ T.filter IsSquareTile, ∑ c ∈ t, wt c
      = ((T.filter IsSquareTile).card : ZMod 2) := by
    have key : ∀ t ∈ T.filter IsSquareTile, (∑ c ∈ t, wt c) = 1 := by
      intro t ht
      exact wtSum_of_isSquareTile (Finset.mem_filter.mp ht).2
    rw [Finset.sum_congr rfl key]
    simp
  have h2 : ∑ t ∈ T.filter (fun t => ¬ IsSquareTile t), ∑ c ∈ t, wt c = 0 := by
    refine Finset.sum_eq_zero (fun t ht => ?_)
    obtain ⟨htT, hns⟩ := Finset.mem_filter.mp ht
    exact wtSum_of_isBarTile ((hshape t htT).resolve_left hns)
  rw [h1, h2, add_zero]

/-- **Parity invariant.** Any two tilings of the same region use the same number of
`2 × 2` tiles modulo `2`. -/
theorem card_squares_modEq {S : Finset (ℤ × ℤ)} {T₁ T₂ : Finset (Finset (ℤ × ℤ))}
    (h₁ : IsTiling S T₁) (h₂ : IsTiling S T₂) :
    (T₁.filter IsSquareTile).card ≡ (T₂.filter IsSquareTile).card [MOD 2] := by
  have : (((T₁.filter IsSquareTile).card : ℕ) : ZMod 2)
      = (((T₂.filter IsSquareTile).card : ℕ) : ZMod 2) := by
    rw [← wtSum_eq_card_squares h₁, ← wtSum_eq_card_squares h₂]
  exact (ZMod.natCast_eq_natCast_iff _ _ _).mp this

/-- **The broken tile cannot be replaced by one of the other shape.**

If a region `S` is tiled by `T₁`, and `T₂` is another tiling of the same region using
one `2 × 2` tile fewer and one `4 × 1` tile more (or vice versa), we get a
contradiction: so after breaking a tile, no rearrangement using a tile of the other
shape can cover the floor. -/
theorem cannot_swap_tile_shapes {S : Finset (ℤ × ℤ)} {T₁ T₂ : Finset (Finset (ℤ × ℤ))}
    (h₁ : IsTiling S T₁) (h₂ : IsTiling S T₂)
    (hswap :
      ((T₂.filter IsSquareTile).card + 1 = (T₁.filter IsSquareTile).card ∧
        (T₁.filter IsBarTile).card + 1 = (T₂.filter IsBarTile).card) ∨
      ((T₁.filter IsSquareTile).card + 1 = (T₂.filter IsSquareTile).card ∧
        (T₂.filter IsBarTile).card + 1 = (T₁.filter IsBarTile).card)) :
    False := by
  have hpar := card_squares_modEq h₁ h₂
  unfold Nat.ModEq at hpar
  rcases hswap with ⟨h, -⟩ | ⟨h, -⟩ <;> omega

/-- Sanity check: the notion of tiling is not vacuous — a single `2 × 2` tile tiles
the `2 × 2` region, and it uses exactly one square tile. -/
example :
    IsTiling ({(0,0), (1,0), (0,1), (1,1)} : Finset (ℤ × ℤ))
      {({(0,0), (0+1,0), (0,0+1), (0+1,0+1)} : Finset (ℤ × ℤ))} ∧
    (({({(0,0), (0+1,0), (0,0+1), (0+1,0+1)} : Finset (ℤ × ℤ))} :
        Finset (Finset (ℤ × ℤ))).filter IsSquareTile).card = 1 := by
  refine ⟨⟨?_, ?_, ?_⟩, ?_⟩
  · intro t ht
    simp only [Finset.mem_singleton] at ht
    exact Or.inl ⟨0, 0, by rw [ht]⟩
  · simp
  · simp
  · rw [Finset.filter_singleton, if_pos ⟨0, 0, rfl⟩]
    simp

end BrokenTile

Archives

2026-08-07: The Sylvester-Gallai theorem in Lean 4

2026-05-13: Minasp v0.6.11 released for Mew v6.11

2024-04-11: Running Torch for R with CUDA in a Docker container

2024-03-17: Introducing Minasp, a Nix package of Mew

2024-03-02: How to view a figure plotted by Plotly R from macOS Terminal

2024-02-20: How to cite references in Org Mode with Zotero

2024-02-18: Heads up for endangered "404 Not Found" pages

2024-02-17: Finding another blog about Nix: Nixcademy

2024-02-12: Writing blog articles with Org Mode

2023: Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

2022: Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

2021: Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

2020: Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

2019: Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

2018: Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

2017: Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

2016: Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

2015: Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

2014: Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

2013: Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

2012: Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

2011: Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

2010: Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

2009: Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

2008: Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

2007: Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

2006: Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec


© 2006-2026 fixedpoint.jp