combinat::decomposableObjects – decomposable combinatorial objects

The library combinat::decomposableObjects(specification) provides functions for counting, generating, and drawing at random the decomposable combinatorial objects defined recursively by specification.

→ Examples

Creating the Type

combinat::decomposableObjects(specification, <Options>)

Parameters:

specification

A combinatorial specification

Options:

Labelled = math

Selects between labeled or unlabeled combinatorial objects. b must be TRUE or FALSE. The default value is FALSE.

Products = math

Choose the counting algorithm. Type is one of Naive, Holonomic. The default value is Holonomic for context free grammars, and Naive otherwise. See "count" for details.

MainNonTerminal = math

Specifies the main non terminal of the specification. A must be a non terminal appearing in the specification. By default, the first one (as returned by op(specification,[1,1]) is used). This option used to be incorrectly named MainTerminal.

Boustrophedonic = math

Specifies the order in which elements of products are generated; see “background” section for more details. b must be TRUE or FALSE. The default value is TRUE.

Float = math

Specifies whether floating point arithmetic should be used for fast approximate counting; see the example 8 for details. b must be TRUE or FALSE. The default value is FALSE.

Details:

 

[A = Prod(Z,Powerset(A))]

non plane trees

[B = Union(Z,Prod(B,B))]

plane binary trees

[C = Prod(Z,Sequence(C))]

plane general trees

[D = Powerset(Cycle(Z))]

permutations in cycle notation

[E = Powerset(Cycle(A)), A=Prod(Z,Powerset(A))]

functional graphs

[F = Powerset(Powerset(Z,MinLength=1))]

set partitions

[G = Union(Z,Prod(Z,Powerset(G,Length=3)))]

non plane ternary trees

[H = Union(Z,Powerset(H,MinLength=2))]

hierarchies

[L = Powerset(Powerset(Powerset(Z,MinLength=1),MinLength=1))]

3-balanced hierarchies

[M = Sequence(Powerset(Z,MinLength=1))]

surjections

 

 

[A = Multiset(Sequence(Z,MinLength=1))]

integer partitions

[B = Sequence(Union(Z,Z))]

binary sequences

[C = Cycle(Sequence(Z,MinLength=1))]

necklaces

[D = Prod(Z,Multiset(D))]

rooted unlabelled trees

[E = Powerset(Cycle(D)), D=Prod(Z,Powerset(D))]

random mappings patterns

[F = Union(Z,Multiset(F,Length=2))]

nonplane binary trees

[G = Union(Z,Multiset(G,Length=3))]

nonplane ternary trees

[H = Union(Z,Multiset(H,MinLength=2))]

unlabelled hierarchies

 

Superdomain

Dom::BaseDomain

Categories

Cat::CombinatorialClass

Axioms

Ax::systemRep

count – number of decomposable objects

(combinat::decomposableObjects(specification, <Options>))::count(nonnegative integer n, <A>)

Returns the number of objects of size n derived by the non terminal A in the specification.

By default, A is the main non terminal.

If Products is set to Naive, the counting is done by naive products of generating series; this gives a math complexity (not counting the coefficient growth). Those products could be done using a lazy Karatsuba-type algorithm with a complexity of O(n^3/2); however this is currently only partly implemented (Products will be Lazy).

If Products is set to Holonomic, the system will first precompute a linear recurrence between the coefficients of the generating series (see "recurrenceRelation"), and then use it for the counting; this gives a complexity of math. This is only possible for context free specifications. Also, for very large specifications (say more than 100 non terminals), the precomputation can become quite expensive; if one is only interested in counting for small values of math (say math), it may be preferable to stick to Naive or Lazy products.

generator – generator for decomposable objects

(combinat::decomposableObjects(specification, <Options>))::generator(nonnegative integer n, <A>)

Returns a generator for the objects of size n derived by the non terminal A in the specification.

By default, A is the main non terminal.

list – list of decomposable objects

(combinat::decomposableObjects(specification, <Options>))::list(nonnegative integer n, <A>)

Returns the list of the objects of size n derived by the non terminal A in the specification.

By default, A is the main non terminal.

Currently, this method is based on "unrank", and only works for context free grammars.

unrank – unranking of decomposable objects

(combinat::decomposableObjects(specification, <Options>))::unrank(nonnegative integer i, nonnegative integer n, <A>)

Returns the i-th object of size n derived by the non terminal A in the specification.

By default, A is the main non terminal.

Currently, this method only works for context free grammars; algorithm do exists for the other constructions, but are not yet implemented.

The worst case complexity is math with the Boustrophedonic order, and math otherwise.

random – random object

(combinat::decomposableObjects(specification, <Options>))::random(nonnegative integer n, <A>)

Returns a random object of size n derived by the non terminal A in the specification.

By default, A is the main non terminal.

The worst case complexity is math with the Boustrophedonic order, and math otherwise.

standardSpecification – specification in standard form

(combinat::decomposableObjects(specification, <Options>))::standardSpecification()

Returns the standard form of the specification; this is an equivalent specification which uses only the constructors Atom, Union, Prod, Theta, Int, Delta, DivX, and MultX. Theta is the pointing constructor, Int the formal inverse of Theta, and Delta is the generalized diagonal.

standardSpecificationCount – specification in standard form

(combinat::decomposableObjects(specification, <Options>))::standardSpecificationCount()

Returns a simplified form of the specification in standard form which is equivalent for counting purposes.

This is done by sorting the operands of Prod and Union (the operations + and * on generating series are commutative!), erasing the names of the singletons and atoms and assigning them with a default weight if they do not have one yet. Then, the productions that are identical are factored out using Alias. Further optimizations would be possible; in particular, productions that are recursively equivalent are not detected yet.

algebraicEquation – algebraic equation for the generating series

(combinat::decomposableObjects(specification, <Options>))::algebraicEquation(<{A}>)

Returns an algebraic equation satisfied by the generating series for the non terminal A.

Computing this algebraic equation is only possible if the specification is context free; FAIL is returned if this is not the case.

recurrenceRelation – recurrence relation for the coefficients of the generating series

(combinat::decomposableObjects(specification, <Options>))::recurrenceRelation(<{A}>, <identifier u>, <identifier u>)

Returns a recurrence relation between the coefficients of the generating series for the non terminal A.

The recurrence relation is given in the form of an expression such as u(n) - n*u(n-1) - u(n-2), it means that the number of objects of size n is equal to n times the number of objects of size n-1 plus the number of objects of size n-2.

Computing the recurrence relation is only possible if the specification is context free; FAIL is returned if this is not the case.

generateCode – automatic C code generation

(combinat::decomposableObjects(specification, <Options>))::generateCode(string filename)

Generate a C program in the file filename which can be used for efficient counting and random generation of objects.

To compile the program, the GMP library version 4.0 or higher is required; aside from this, any C compiler should work. Admitting that filename is blah.c, here is the command line for the gcc compiler: gcc blah.c -lgmp -o blah.

Once compiled, the program can be used completely independently from MuPAD with the command: blah n [k seed]. This writes to the standard output k random objects of size n (or FAIL if no object of size n exists), one per line. If k is 0, then the number of objects of size n is output instead. A third parameter seed can be provided to initialize the GMP random generator.

There are restrictions on the specifications that the C program can manage, in particular for complicated use of Alias. An error is raised if this is the case.

Currently the counting algorithm uses naive multiplication of generating series and whose time complexity is math, and memory complexity math, not taking into account the coefficient growth. As for the "count" method, this could be reduced to math for context free grammars and to math otherwise. The random generation algorithm is of complexity math. For quasi-uniform random purpose, the counting could be done with floating point numbers (or better interval arithmetic), to ensure constant time arithmetic operations.

Example 1:

We create a domain for complete binary trees:

BinaryNaive:=combinat::decomposableObjects({B=Union(Z,Prod(B,B))}):

 

Standard form of specification

BinaryNaive::standardSpecification()

math

The number of binary tree of size 0 to 10

BinaryNaive::count(i) $i=0..10

math

A random tree of size 1 and size 2

BinaryNaive::random(i)$ i=1..2

math

List of all objects of size 4 satisfying the specification

BinaryNaive::list(4)

math

Example 2:

This specification is the Schroeder tree with Naive product:

SchroederNaive:=combinat::decomposableObjects(

{B=Union(Z,Prod(Z,B),Prod(Z,B,B))}):

 

Standard form of specification

SchroederNaive::standardSpecification()

math

The number of Schroeder tree of size 0 to 10

SchroederNaive::count(i) $i=0..10

math

A random tree of size 1 and size 2

SchroederNaive::random(i)$ i=1..2

math

Example 3:

This specification defines the Schroeder trees with Naive and Holonomic product:

SchroederHolonomic:=combinat::decomposableObjects(

{B=Union(Z,Prod(Z,B),Prod(Z,B,B))},Products=Holonomic):

SchroederNaive:=combinat::decomposableObjects(

{B=Union(Z,Prod(Z,B),Prod(Z,B,B))}):

 

Time necessary to count the number of Schroeder tree of size 500 according to whether you use the holonomic products or not

time(SchroederNaive::count(500));time(SchroederHolonomic::count(500));

                              11340

 

                               30

 

Time necessary to generate a random Schroeder tree of size 500 according to whether you use holonomic products or not

time(SchroederNaive::random(500));

time(SchroederHolonomic::random(500));

                              820

 

                               490

 

Example 4:

We demonstrate how to use the functions generator and list.

Shroeder:=combinat::decomposableObjects(

{B=Union(Z,Prod(Z,B),Prod(Z,B,B))}):

 

Shroeder::count(5)

math

All objects of size 5 satisfying the specification

Shroeder::list(5)

math

We build and use a generator for those objects:

f:=Shroeder::generator(5):

 

f() $ i =0..9

math

Example 5:

We create a domain for labeled and unlabeled complete binary trees:

BinaryUnlabeled:=combinat::decomposableObjects(

{B=Union(Z,Prod(B,B))},Labelled=FALSE):

BinaryLabeled:=combinat::decomposableObjects(

{B=Union(Z,Prod(B,B))},Labelled=TRUE):

 

Number of objects of size 0 to 10 satisfying the specification

BinaryUnlabeled::count(i)$i=0..10;

BinaryLabeled::count(i)$i=0..10;

math

math

Example 6:

The grammar is not checked for validity. For example, the following grammar describes infinitely many objects of size zero (Prod(B,B), Prod(B, Prod(B,B)), Prod(B, Prod(B, Prod(...)))). The recursion formulas for counting are not well-founded, and MuPAD goes into an infinite loop when trying to compute the number of objects of size math.

loop := combinat::decomposableObjects(

    [A = Prod(B, Union(A, B)),

     B = Epsilon(B)]):

loop::count(0):

Error: Recursive definition [See ?MAXDEPTH];

during evaluation of 'DOM_VAR(0,1)::count_functions[A]'

 

It would be better to emit a more meaningful error message; however, it is non trivial to check automatically whether a grammar is well-founded or not. For the moment, please consider any weird error message like the one above as a good indicator that the grammar might be invalid.

Example 7:

We use the mechanics of combinat::decomposableObjects to present the classical and striking “balls and bars” model for integer compositions. A composition like [2,1,3] can be represented as a string of balls and bars by using sequences of balls, separated by bars, here: "ooIoIooo". Such strings can be described by the grammar math.

ballsAndBars :=

combinat::decomposableObjects(

   [

    Ball         = Atom("o"),

    Bar          = Epsilon("I"),

    BB           = Union(Ball,

                         Alias(Prod(Ball, BB),op),

                         Alias(Prod(Ball, Bar, BB),op)

                        ),

    BallsAndBars = Alias(BB, _concat)

   ], MainNonTerminal = BallsAndBars):

There are math balls and bars strings with math balls:

ballsAndBars::count(i) $ i = 1..10

math

This can be seen directly from the recurrence relation:

ballsAndBars::recurrenceRelation()

math

Here are all the compositions of math:

ballsAndBars::list(3)

math

Example 8:

It is possible to do weighted enumeration by assigning weights to the atoms and epsilons. Then, the result of S::count(n) is the sum over each object of size n of the product of the weights of its atoms and epsilons.

In theory, it would be possible to use weights in any ring; however, this is an experimental feature, and there are some non documented restrictions on the rings. Also beware that the generating operations like "random", "unrank", "list" and "generator" rely on the standard definition of "count". So, a domain representing a decomposable combinatorial class can't be used simultaneously for advanced counting and for generation.

As a first application, we count unlabelled ordered trees by total number of nodes and number of internal nodes. To achieve this, we assign a weight of math to leaves and of math to internal nodes:

S :=

combinat::decomposableObjects(

   [T    = Union(Leaf, Prod(InternalNode, Sequence(T, MinLength = 1))),

    Leaf = Atom(Leaf, 1),

    InternalNode = Atom(InternalNode, q)],

   CoeffRing = Dom::ExpressionField()):

 

The CoeffRing option specifies that the recurrence relation precomputations should take place in Dom::ExpressionField(). Now, S::count(n) computes the generating series by number of internal nodes of the trees with math nodes altogether:

expand(S::count(4))

math

This means that, among the trees on math nodes, one has a single internal node, three have two internal nodes, and one has three internal nodes.

Another application of this feature is for doing approximate counting. To this end, we redefine the previous domain, but this time with a weight of math:

S :=

combinat::decomposableObjects(

   [T    = Union(Leaf, Prod(InternalNode, Sequence(T, MinLength = 1))),

    Leaf = Atom(Leaf, 1.0),

    InternalNode = Atom(InternalNode, 1.0)],

   Products=Naive):

 

Now, count returns a floating point approximation of the number of objects having a given size:

[S::count(i) $ i = 0..10]

math

S::count(501)

math

The bonus is that approximate counting is much faster, as there is no coefficient growth. However, in order to use the fast holonomic counting method, it is safer to use exact arithmetic for precomputing the recurrence relation between, and only then to switch to approximate counting. This is what we do now:

  S :=

combinat::decomposableObjects(

   [T    = Union(Leaf, Prod(InternalNode, Sequence(T, MinLength = 1))),

    Leaf = Atom(Leaf, 1.0, 1),

    InternalNode = Atom(InternalNode, 1.0, 1)]):

Note that the nodes now have two weights: one for approximate computations, and the other for exact computations. Of course those two weights should be coherent. Here is the recurrence relation obtained by the precomputation:

S::recurrenceRelation()

math

Now, one can compute quite large values:

S::count(20000)

                       7.900503708e12033

 

Alternatively, one can simply use the Float which does exactly this:

S :=

combinat::decomposableObjects(

   [T    = Union(Leaf, Prod(InternalNode, Sequence(T, MinLength = 1))),

    Leaf = Atom(Leaf),

    InternalNode = Atom(InternalNode)],

   Float):

[S::count(i) $ i = 0..10]

math

A possible extension would be to allow the constructors like Prodto apply any transformation on the weights of their operands. A typical application of this is for computing the generating series of binary trees of a given size with respect to their maximal depth.

Background: