Zulip Chat Archive
Stream: mathlib4
Topic: Idea: Helper for speeding up Decidable on symmetric props
Chris Wong (Feb 16 2025 at 13:28):
The Decidable
instance for Injective
(docs#Fintype.decidableInjectiveFintype) takes the straightforward approach – it checks that every pair of inputs is mapped to distinct outputs.
However, this does twice as much work as necessary: if we've checked (a, b)
, we don't need to check (b, a)
.
I wonder if there's an easy way to skip the redundant pairs, perhaps using docs#Sym2 ?
I'm not sure how common it is to prove injectivity etc. with decide
, but cutting the runtime in half sounds pretty cool.
(Context is writing this proof and thinking about why it might be slow :smile: )
Edward van de Meent (Feb 16 2025 at 14:05):
At best you're still only halving the number of checks tho
Edward van de Meent (Feb 16 2025 at 14:07):
I guess the best way to go about this might be to transform it by saying that for symmetric relations, xs.Pairwise r
iff (xs : Finset).Pairwise r
, at which point I think using decide should only do the lower triangle...
Edward van de Meent (Feb 16 2025 at 14:08):
Chris Wong (Feb 16 2025 at 14:24):
I'm not sure what you mean by List.Pairwise. We're talking about Fintype here so it's already a Finset.
Edward van de Meent (Feb 16 2025 at 14:34):
I'm thinking that since it eventually is a quotient of lists by permutations, you may be able to make it reduce like this
Chris Wong (Feb 16 2025 at 14:36):
Hmm, that seems like a lot of work. I'm hoping we can just go decidable_of_iff'
with docs#Sym2.fromRel_prop or similar and get the speedup for free.
Aaron Liu (Feb 16 2025 at 15:07):
This wasn't that hard, only tricky part was getting access to the underlying list. Also, removed [DecidableEq α]
import Mathlib.Data.Fintype.Basic
theorem Set.pairwise_list_iff_of_symmetric_of_nodup {α : Type*} {r : α → α → Prop} (hr : Symmetric r) {xs : List α} (hxs : xs.Nodup) :
{x | x ∈ xs}.Pairwise r ↔ xs.Pairwise r := by
induction xs with
| nil => simp
| cons x xs ih =>
rw [List.nodup_cons] at hxs
rw [List.set_of_mem_cons, Set.pairwise_insert, List.pairwise_cons, ih hxs.right, and_comm]
apply and_congr_left'
apply forall₂_congr
intro b hb
have : x ≠ b := by rintro rfl; exact hxs.left hb
rw [eq_true this, true_implies]
apply and_iff_left_of_imp
apply hr
instance {α β : Type*} [Fintype α] [DecidableEq β] : DecidablePred (@Function.Injective α β) := fun f ↦
Quotient.recOnSubsingleton (motive := (· = Finset.univ.val → Decidable f.Injective)) Finset.univ.val
(fun (xs : List α) (h : Multiset.ofList xs = Finset.univ.val) ↦ decidable_of_iff (xs.Pairwise (f · ≠ f ·)) <| by
have hd : xs.Nodup := by
rw [← Multiset.coe_nodup, h]
exact Finset.univ.nodup
rw [← Set.pairwise_list_iff_of_symmetric_of_nodup (fun _ _ h ↦ h.symm) hd, Function.injective_iff_pairwise_ne]
simp_rw [← Multiset.mem_coe, h, Finset.mem_val, Finset.mem_univ, Set.setOf_true, Set.pairwise_univ]) rfl
Chris Wong (Feb 16 2025 at 23:13):
Nice! Looks like this can apply to any symmetric reflexive relation.
Chris Wong (Feb 16 2025 at 23:18):
I guess the two advantages with the List.Pairwise approach are
- We skip explicitly constructing the set of unordered pairs
- We skip checking the diagonal (which lets us drop DecidableEq on the input type)
I'm curious about the performance impact of (1).
I believe we can address (2) in the Sym2 approach using docs#Finset.offDiag (or an alternative implementation that drops DecidableEq?) and docs#Finset.decidableDforallFinset .
Eric Wieser (Feb 16 2025 at 23:25):
I think at least one of @Yury G. Kudryashov and I have a DecidableEq-free version of offDiag, which is also more efficient
Chris Wong (Feb 16 2025 at 23:31):
That's cool! Unfortunately Finset.offDiag returns ordered pairs. We'll need something that combines Finset.offDiag and docs#Finset.sym2 I think
Chris Wong (Feb 17 2025 at 01:16):
I'm coming back around to Aaron/Edward's approach... we can package it up in def Finset.SymmetricPairwise := Quotient.lift List.Pairwise ...
that hides the quotient shenanigans
Kyle Miller (Feb 17 2025 at 01:28):
@Chris Wong If you work down through Finset.sym2
, Multiset.sym2
, and List.sym2
, the definition is
protected def List.sym2 : List α → List (Sym2 α)
| [] => []
| x :: xs => (x :: xs).map (fun y => s(x, y)) ++ xs.sym2
If you modify this to be
protected def List.offDiagSym2 : List α → List (Sym2 α)
| [] => []
| x :: xs => xs.map (fun y => s(x, y)) ++ xs.offDiagSym2
I think that gives you everything off the diagonal.
It shouldn't be so bad to prove that xs.offDiagSym2 = xs.sym2.filter (fun s => \neg s.IsDiag)
, and then you can build everything up again. Once you have Finset.offDiagSym2
, you can make a Fintype instance for {s : Sym2 _ // \neg s.IsDiag }
. One that doesn't use DecidableEq.
Kyle Miller (Feb 17 2025 at 01:33):
@Aaron Liu's instance is more straightforward and has less overhead. It avoids creating a list of all the unordered pairs, and instead it goes straight to computing.
Still, it'd be worth having the deleted diagonal sym2 theory worked out more.
Chris Wong (Feb 17 2025 at 02:13):
Another optimization is, if f
is expensive, we want to avoid evaluating it repeatedly, so we want to check (xs.map f).Nodup
instead. Which seems harder to generalize.
Chris Wong (Feb 17 2025 at 02:40):
:thinking:
import Mathlib
open List in
theorem Function.injective_iff_lift_nodup_map_univ {α β} [Fintype α] (f : α → β) :
Injective f ↔
Quotient.lift
(fun xs => (xs.map f).Nodup)
(fun xs ys (h : xs ~ ys) => by simp [(h.map f).nodup_iff])
Finset.univ.val :=
sorry
example {α β} (f : α → β) [Fintype α] [DecidableEq β] : Decidable (Function.Injective f) :=
decidable_of_iff' _ (Function.injective_iff_lift_nodup_map_univ f)
Yury G. Kudryashov (Feb 17 2025 at 05:17):
One more optimization (at least, for large lists) in case β
is a LinearOrder
: use Chain' (. < .) ((xs.map f).mergeSort (. ≤ .))
.
Chris Wong (Feb 17 2025 at 06:04):
Look ma! No quotients!
import Mathlib
variable {α : Type*} {β : Type*}
/--
A function `f` is injective on a `Finset` `s` if the image of `s` under `f` has no duplicates.
-/
theorem Finset.injOn_iff_nodup_map_val (f : α → β) (s : Finset α) :
Set.InjOn f s ↔ (s.val.map f).Nodup := by
induction s using Finset.cons_induction with
| empty => simp
| cons x xs hx h =>
simp [Set.injOn_insert (show x ∉ (xs : Set α) by simpa)]
tauto
/--
A function `f` with a finite domain is injective if its range has no duplicates.
This is useful for computation, as the right-hand side is usually more efficient.
-/
theorem Function.injective_iff_nodup_map_univ [Fintype α] (f : α → β) :
Injective f ↔ (Finset.univ.val.map f).Nodup := by
simpa [Set.injective_iff_injOn_univ] using Finset.injOn_iff_nodup_map_val f Finset.univ
example (f : α → β) [Fintype α] [DecidableEq β] : Decidable (Function.Injective f) :=
decidable_of_iff' _ (Function.injective_iff_nodup_map_univ f)
Chris Wong (Feb 17 2025 at 07:27):
#21975 :octopus:
Last updated: May 02 2025 at 03:31 UTC