From 9af26e348f1ad95edf393f262808e881974896f4 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Tue, 27 Oct 2026 11:26:12 +0100 Subject: [PATCH 1/2] Make `EnumeratorOfCombinations(mset)` equal to `Combinations(mset)` EnumeratorOfCombinations( mset ) numbered the combinations by their multiplicities in mixed radix, an order that matched neither Combinations( mset ) nor EnumeratorOfCombinations( mset, k ). It now lists them in the lexicographic order of Combinations( mset ) and lies in the same family, so the two compare equal. Like the enumerator with k, it is known to be strictly sorted, prints as a call, and lets dictionaries hash lists of small integers by content. Position returns fail for unsorted lists, which it used to accept. Suggested by Reinis Cirpons. Ranking and unranking use the number of sub-multisets of each suffix of Set( mset ). Sets and Position get faster; A[i] for a multiset gets slower, most with many distinct elements, as each step divides by such a count rather than by a small radix. gap> s:=EnumeratorOfCombinations([1..18]);; gap> m:=EnumeratorOfCombinations(Concatenation([1..9],[1..9],[1..9]));; gap> M:=EnumeratorOfCombinations(Concatenation([1..1000],[1..1000]));; Before: gap> l:=List([1..Length(s)],i->s[i]);; time; 653 gap> List(l,c->Position(s,c));; time; 899 gap> l:=List([1..Length(m)],i->m[i]);; time; 476 gap> List(l,c->Position(m,c));; time; 765 gap> l:=List([1..1000],i->M[QuoInt(i*Length(M),1000)]);; time; 299 gap> List(l,c->Position(M,c));; time; 299 After: gap> l:=List([1..Length(s)],i->s[i]);; time; 345 gap> List(l,c->Position(s,c));; time; 258 gap> l:=List([1..Length(m)],i->m[i]);; time; 505 gap> List(l,c->Position(m,c));; time; 454 gap> l:=List([1..1000],i->M[QuoInt(i*Length(M),1000)]);; time; 557 gap> List(l,c->Position(M,c));; time; 192 Assisted-by: Generative AI --- lib/combinat.gd | 13 +-- lib/combinat.gi | 210 +++++++++++++++++++++++++---------- tst/testinstall/combinat.tst | 46 ++++++++ 3 files changed, 206 insertions(+), 63 deletions(-) diff --git a/lib/combinat.gd b/lib/combinat.gd index 6394ca2a13..0cd5f91e90 100644 --- a/lib/combinat.gd +++ b/lib/combinat.gd @@ -341,11 +341,10 @@ DeclareGlobalFunction("Combinations"); ## all combinations. ##

## returns an -## for the same combinations. -## With second argument k, it is strictly sorted. -##

-## The ordering of combinations from these functions can be different and also -## different from the list returned by . +## for the same combinations; it is equal to the strictly sorted list +## Combinations( mset[, k] ). +## produces them in a different order, +## without k by increasing size. ##

## ## gap> m:=[1..15];; Add(m, 15); @@ -356,9 +355,9 @@ DeclareGlobalFunction("Combinations"); ## 49152 ## gap> cm := EnumeratorOfCombinations(m);; ## gap> cm[1000]; -## [ 1, 2, 3, 6, 7, 8, 9, 10 ] +## [ 1, 2, 3, 4, 5, 7, 9, 10, 12, 13, 15, 15 ] ## gap> Position(cm, [1,13,15,15]); -## 36866 +## 24572 ## gap> cm := EnumeratorOfCombinations([1..30], 6); ## EnumeratorOfCombinations( [ 1 .. 30 ], 6 ) ## gap> Length(cm); diff --git a/lib/combinat.gi b/lib/combinat.gi index 1921c48111..e635c9ec46 100644 --- a/lib/combinat.gi +++ b/lib/combinat.gi @@ -363,8 +363,121 @@ BindGlobal("EnumeratorOfCombinations_k", function(mset, k) return enum; end); +# All combinations are numbered in lexicographic order, in which a list +# comes before its extensions. Let S[i] = Product(max{[i..l]} + 1) be the +# number of sub-multisets of els{[i..l]}. A combination with d[i] copies +# of els[i], the last nonzero one d[t], comes after its Sum_i d[i] proper +# prefixes and, for each i < t, after the (max[i] - d[i]) * S[i+1] +# combinations that agree with it below els[i] and have more copies of +# els[i]. For a set of size n, S[i] = 2^(n+1-i). +BindGlobal("ElementNumber_AllCombinations_set", function(enum, num) + local set, s, r, comb, i; + if num > enum!.size then + Error("Index ", num, " not bound, expected at most ", enum!.size); + fi; + set := enum!.set; + s := enum!.size; + r := num-1; + comb := []; + i := 0; + while r > 0 do + i := i+1; + s := QuoInt(s, 2); + # r is the rank among the combinations of set{[i..n]}: [ ], the s + # with set[i], then the s-1 nonempty ones of set{[i+1..n]} + if r <= s then + Add(comb, set[i]); + r := r-1; + else + r := r-s; + fi; + od; + return comb; +end); + +BindGlobal("NumberElement_AllCombinations_set", function(enum, comb) + local set, n, num, prev, x, p; + if not IsDenseList(comb) then + return fail; + fi; + set := enum!.set; + n := Length(set); + num := enum!.size + Length(comb) + 1; + prev := 0; + # with positions p_1 < .. < p_k, the sum of 2^(n-i) over the i < p_k + # other than the p_j is 2^n - 2^(n-p_k) - Sum_j 2^(n-p_j) + for x in comb do + p := Position(set, x); + if p = fail or p <= prev then + return fail; + fi; + num := num - 2^(n-p); + prev := p; + od; + return num - 2^(n-prev); +end); + +BindGlobal("ElementNumber_AllCombinations_mset", function(enum, num) + local els, max, s, r, comb, i, b; + if num > enum!.size then + Error("Index ", num, " not bound, expected at most ", enum!.size); + fi; + els := enum!.els; + max := enum!.max; + s := enum!.size; + r := num-1; + comb := []; + i := 0; + while r > 0 do + i := i+1; + s := QuoInt(s, max[i]+1); + # r is the rank among the combinations of els{[i..l]}: els[i]^a for + # a = 0 .. max[i], then for a = max[i], .., 0 the s-1 combinations of + # els[i]^a followed by a nonempty one of els{[i+1..l]} + if r <= max[i] then + Append(comb, ListWithIdenticalEntries(r, els[i])); + r := 0; + else + b := QuoInt(r-max[i]-1, s-1); + Append(comb, ListWithIdenticalEntries(max[i]-b, els[i])); + r := r - max[i] - b*(s-1); + fi; + od; + return comb; +end); + +BindGlobal("NumberElement_AllCombinations_mset", function(enum, comb) + local els, max, d, prev, x, p, num, i; + if not IsDenseList(comb) then + return fail; + fi; + els := enum!.els; + max := enum!.max; + d := 0*max; + prev := 1; + for x in comb do + if x <> els[prev] then + p := Position(els, x); + if p = fail or p < prev then + return fail; + fi; + prev := p; + fi; + if d[prev] = max[prev] then + return fail; + fi; + d[prev] := d[prev]+1; + od; + # Sum_{i 1 then Error("usage: EnumeratorOfCombinations( [, ] )"); elif Length(arg) = 1 then @@ -373,61 +486,46 @@ InstallGlobalFunction(EnumeratorOfCombinations, function(mset, arg...) fi; return EnumeratorOfCombinations_k(mset, arg[1]); fi; - c := Collected(mset); - max := List(c, a-> a[2]); - els := List(c, a-> a[1]); - l := Length(max); - mods := max+1; - size := Product(mods); - # a combination can contain els[i] from 0 to max[i] times (mods[i] - # possibilities), we number the combination that contains a[i] times els[i] - # for all i by n = 1 + sum_i a[i]*m[i] where m[i] = prod_(j size then - Error("Index ", n, " not bound."); - fi; - comb := EmptyPlist(l); - n := n-1; - for i in [1..l] do - comb[i] := n mod mods[i]; - n := (n - comb[i])/mods[i]; - od; - res := []; - for i in [1..l] do - for j in [1..comb[i]] do - Add(res, els[i]); - od; - od; - return res; - end; - NumberElement := function(enu, comb) - local c, d, pos, n, a, i; - if not IsList(comb) then - return fail; - fi; - c := Collected(comb); - d := 0*max; - for a in c do - pos := PositionSorted(els, a[1]); - if not IsBound(els[pos]) or els[pos] <> a[1] or a[2] > max[pos] then - return fail; - else - d[pos] := a[2]; - fi; - od; - n := 0; - for i in [l,l-1..1] do - n := n*mods[i] + d[i]; - od; - return n+1; - end; - return EnumeratorByFunctions(ListsFamily, rec( - ElementNumber := ElementNumber, - NumberElement := NumberElement, - els := els, - Length := x->size, - max := max)); + els := Set(mset); + # [ ] and nonempty lists lie in different families + if IsEmpty(els) then + fam := FamilyObj([ [] ]); + else + fam := ListsFamily; + fi; + if Length(els) = Length(mset) then + enum := EnumeratorByFunctions(fam, rec( + ElementNumber := ElementNumber_AllCombinations_set, + NumberElement := NumberElement_AllCombinations_set, + Length := enum -> enum!.size, + PrintObj := function(enum) + Print("EnumeratorOfCombinations( "); + PrintObj(enum!.set); + Print(" )"); + end, + set := els, + size := 2^Length(els))); + else + max := List(Collected(mset), a -> a[2]); + enum := EnumeratorByFunctions(fam, rec( + ElementNumber := ElementNumber_AllCombinations_mset, + NumberElement := NumberElement_AllCombinations_mset, + Length := enum -> enum!.size, + PrintObj := function(enum) + Print("EnumeratorOfCombinations( "); + PrintObj(Concatenation(List([1..Length(enum!.els)], + i -> ListWithIdenticalEntries(enum!.max[i], enum!.els[i])))); + Print(" )"); + end, + els := els, + max := max, + size := Product(max+1))); + fi; + SetIsSSortedList(enum, true); + if IsRange(els) or ForAll(els, IsSmallIntRep) then + SetFilterObj(enum, IsEnumeratorOfSmallIntLists); + fi; + return enum; end); BindGlobal("NextIterator_Combinations_set", function(it) diff --git a/tst/testinstall/combinat.tst b/tst/testinstall/combinat.tst index afc1ff896d..2c97912c5d 100644 --- a/tst/testinstall/combinat.tst +++ b/tst/testinstall/combinat.tst @@ -130,6 +130,52 @@ gap> for n in [ 0 .. 10 ] do > Error( "different elements" ); > fi; > od; +gap> for mset in Concatenation( [ [ 1 .. 10 ], [ 5, 3, 1 ], "abracadabra", +> [ (1,2), (), (1,2,3) ], [ (1,2), (), (1,2), (1,2,3) ] ], +> List( [ 0 .. 12 ], +> n -> List( [ 1 .. n ], i -> Random( [ 1 .. n ] ) ) ) ) do +> comb1:= Combinations( mset ); +> enum:= EnumeratorOfCombinations( mset ); +> if not ( HasIsSSortedList( enum ) and IsSSortedList( enum ) +> and Length( enum ) = Length( comb1 ) +> and AsList( enum ) = comb1 and enum = comb1 +> and List( comb1, c -> Position( enum, c ) ) +> = [ 1 .. Length( comb1 ) ] ) then +> Error( "wrong enumerator for ", mset ); +> fi; +> od; +gap> enum:= EnumeratorOfCombinations( [ 2, 1, 1 ] ); +EnumeratorOfCombinations( [ 1, 1, 2 ] ) +gap> AsList( enum ); +[ [ ], [ 1 ], [ 1, 1 ], [ 1, 1, 2 ], [ 1, 2 ], [ 2 ] ] +gap> IsEnumeratorOfSmallIntLists( enum ); +true +gap> List( [ [ 2, 1 ], [ 1, 1, 1 ], [ 3 ], [ 1, 1, 2, 2 ], 1 ], +> c -> Position( enum, c ) ); +[ fail, fail, fail, fail, fail ] +gap> enum[ 7 ]; +Error, Index 7 not bound, expected at most 6 +gap> enum:= EnumeratorOfCombinations( [ 1 .. 100 ] ); +EnumeratorOfCombinations( [ 1 .. 100 ] ) +gap> enum[ 2^99 + 1 ]; +[ 1, 100 ] +gap> enum[ 2^100 ]; +[ 100 ] +gap> Position( enum, [ 2 ] ) = 2^99 + 2; +true +gap> List( [ [ 2, 1 ], [ 1, 1 ], [ 101 ], [ 1 .. 101 ], 1 ], +> c -> Position( enum, c ) ); +[ fail, fail, fail, fail, fail ] +gap> mset:= Concatenation( [ 1 .. 50 ], [ 1 .. 50 ] );; +gap> enum:= EnumeratorOfCombinations( mset );; +gap> Length( enum ) = 3^50; +true +gap> enum[ 3^50 ]; +[ 50, 50 ] +gap> Position( enum, [ 1, 2, 2, 50 ] ) = 3^49 + 3^48 + 2; +true +gap> enum[ 3^49 + 3^48 + 2 ]; +[ 1, 2, 2, 50 ] gap> for mset in [ [], [ 1 .. 7 ], [ 5, 3, 1 ], [ 1, 1, 2, 3, 3, 3, 4 ], > "abracadabra", [ (1,2), (), (1,2), (1,2,3) ] ] do > for k in [ 0 .. Length( mset ) + 1 ] do From aa135c01f17dad1eb922f0bf8c8e4c0474024bd9 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Fri, 30 Oct 2026 18:58:22 +0100 Subject: [PATCH 2/2] combinat: Clarify the EnumeratorOfCombinations docs Say that the enumerator, not the combinations, equals the sorted list, and define the notation used by the ranking comment. Assisted-by: Generative AI --- lib/combinat.gd | 4 ++-- lib/combinat.gi | 14 ++++++++------ tst/testinstall/combinat.tst | 2 ++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/combinat.gd b/lib/combinat.gd index 0cd5f91e90..717a8c2fdf 100644 --- a/lib/combinat.gd +++ b/lib/combinat.gd @@ -341,8 +341,8 @@ DeclareGlobalFunction("Combinations"); ## all combinations. ##

## returns an -## for the same combinations; it is equal to the strictly sorted list -## Combinations( mset[, k] ). +## for the same combinations; the enumerator is equal to the strictly sorted +## list Combinations( mset[, k] ). ## produces them in a different order, ## without k by increasing size. ##

diff --git a/lib/combinat.gi b/lib/combinat.gi index e635c9ec46..6173f4d5ec 100644 --- a/lib/combinat.gi +++ b/lib/combinat.gi @@ -364,12 +364,14 @@ BindGlobal("EnumeratorOfCombinations_k", function(mset, k) end); # All combinations are numbered in lexicographic order, in which a list -# comes before its extensions. Let S[i] = Product(max{[i..l]} + 1) be the -# number of sub-multisets of els{[i..l]}. A combination with d[i] copies -# of els[i], the last nonzero one d[t], comes after its Sum_i d[i] proper -# prefixes and, for each i < t, after the (max[i] - d[i]) * S[i+1] -# combinations that agree with it below els[i] and have more copies of -# els[i]. For a set of size n, S[i] = 2^(n+1-i). +# comes before its extensions. Let els = Set(mset), l = Length(els), let +# max[i] be the multiplicity of els[i] in mset, and let +# S[i] = Product(max{[i..l]} + 1) be the number of sub-multisets of mset +# with entries in els{[i..l]}. A combination with d[i] copies of els[i], +# the last nonzero one d[t], comes after its Sum_i d[i] proper prefixes +# and, for each i < t, after the (max[i] - d[i]) * S[i+1] combinations +# that agree with it below els[i] and have more copies of els[i]. For a +# set of size n, S[i] = 2^(n+1-i). BindGlobal("ElementNumber_AllCombinations_set", function(enum, num) local set, s, r, comb, i; if num > enum!.size then diff --git a/tst/testinstall/combinat.tst b/tst/testinstall/combinat.tst index 2c97912c5d..5b428b6829 100644 --- a/tst/testinstall/combinat.tst +++ b/tst/testinstall/combinat.tst @@ -155,6 +155,8 @@ gap> List( [ [ 2, 1 ], [ 1, 1, 1 ], [ 3 ], [ 1, 1, 2, 2 ], 1 ], [ fail, fail, fail, fail, fail ] gap> enum[ 7 ]; Error, Index 7 not bound, expected at most 6 +gap> EnumeratorOfCombinations( [ 1 .. 3 ] )[ 9 ]; +Error, Index 9 not bound, expected at most 8 gap> enum:= EnumeratorOfCombinations( [ 1 .. 100 ] ); EnumeratorOfCombinations( [ 1 .. 100 ] ) gap> enum[ 2^99 + 1 ];