The Cartesian product of a sequence of m lists L1, ..., Ln, with list i having ni elements, is given by the sequence of Πi ni lists [L11,...,Lm1], ..., [L1n1,...,Lmnm], where Ljk is the k-th element of the j-th list.

If the right-most element in the lists varies the fastest, the sequence is in row-major order, if the left-most elements varies the fastest, the sequence is in column-major order. With one exception (noted) all the routines given below use row-major order.

Because a Cartesian product can be large, Maple provides a procedure, combinat[cartprod], that generates an iterator for the Cartesian product. Each call to this iterator returns the next value in the sequence. Here is a procedure that uses cartprod to iterate through each list in the Cartesian product of its arguments, passing each generated list to the function f.

Note: This procedure, as well as most of the following, uses the seq parameter modifier, a mechanism introduced in Maple 11. The parameter L is assigned an expression sequence consisting of the following lists. Here it is equivalent to using the older args[2..-1], provided the remaining arguments are lists.

use_combinat := proc(f, L::seq(list))
local T;
uses combinat;
    T := cartprod([L]);
    while not T['finished'] do
        f(T['nextvalue']());
    end do;
    NULL;
end proc:

By assigning f to print its argument, we can demonstrate that this works as intended:

f := curry(printf,"  %a"):
lsts := [$1..3],[a,b]:
use_combinat(f,lsts): printf("\n"):
  [1, a]  [1, b]  [2, a]  [2, b]  [3, a]  [3, b]

Using combinat[cartprod] is a straight-forward and effective way to step through the entire set without having to allocate memory for the set. The iterator itself, however, requires some time to execute and if the loop itself is otherwise fast—say it calls a compiled procedure—then the time spent evaluating the iterator can be significant.

The Maple built-in procedure rtable_scanblock applies a function to each index of an rtable. If each dimension of the rtable is a range that corresponds to the number of elements in each of the lists, then there is a simple mapping from the indices of the rtable to the sublists in the Cartesian product. We can use this, then, to effectively iterate over the Cartesian product. Because the rtable is created with the storage=sparse option, the memory allocation is minimal. Here is a test procedure that implements this technique.

use_scanblock := proc(f)
local a,dims,n,L;
    L := args[2..-1]:
    n := nargs-1;
    dims := seq(1..nops(a), a in L);
    rtable_scanblock(rtable(dims, 'storage=sparse')
                     , [dims]
                     , proc(val,indx,data)
                       local i;
                           f([seq(L[i][indx[i]], i=1..n)]);
                       end proc
                    );
    NULL;
end proc:

use_scanblock(f,lsts): printf("\n"):
  [1, a]  [1, b]  [2, a]  [2, b]  [3, a]  [3, b]

The following tabulates, for each procedure, the time and memory required to iterate through each element of a Cartesian product of lists. Four different inputs are tested. In each case, the rtable_scanblock technique is faster (about 2×) and uses less memory than combinat[cartprod].

Iterate through the Cartesian product of N lists of E elements,
generating E^N iterations.  At each step, call the (unassigned)
global function f with the generated list.

gcfreq = 10^7

               +-------- timing -------+--------- memory ---------+
256^2          | maple (s) | total (s) | used (MiB) | alloc (MiB) | gctimes
---------------+-----------+-----------+------------+-------------+--------
use_combinat   |     1.77  |     1.90  |    29.98   |     23.75   |    0
use_scanblock  |     0.93  |     0.95  |    19.23   |     13.87   |    0
---------------+-----------+-----------+------------+-------------+--------
10^5           |           |           |            |             |        
---------------+-----------+-----------+------------+-------------+--------
use_combinat   |     3.38  |     3.47  |    47.84   |     31.74   |    1
use_scanblock  |     1.84  |     1.93  |    28.49   |     22.68   |    0
---------------+-----------+-----------+------------+-------------+--------
3^10           |           |           |            |             |        
---------------+-----------+-----------+------------+-------------+--------
use_combinat   |     2.73  |     2.81  |    37.70   |     31.49   |    0
use_scanblock  |     1.40  |     1.45  |    21.51   |     16.06   |    0
---------------+-----------+-----------+------------+-------------+--------
2^16           |           |           |            |             |        
---------------+-----------+-----------+------------+-------------+--------
use_combinat   |     3.99  |     4.13  |    49.50   |     32.62   |    1
use_scanblock  |     2.06  |     2.08  |    25.63   |     21.00   |    0
---------------------------------------------------------------------------

While iterators are useful for reducing the memory footprint, at times it is necessary to instantiate the Cartesian product as a single entity. Maple, alas, does not provide a function for doing so (this might make be a nice addition to the ListTools package). It is not difficult to create a procedure, the interesting part is making it fast. The following site has a few examples of implementations; I have used them below with minor modifications, along with some of my own contributions.

The first example uses combinat[cartprod], called Πi ni times, to generate the Cartesian product.

CartProdDeiterated := proc(L::seq(list))
local C,i,S;
    C := combinat['cartprod']([L]);
    [seq(C['nextvalue'](), i=1..mul(nops(S), S=[L]))]
end proc:

The next example uses a modification of the rtable_scanblock technique. Instead of calling rtable_scanblock, it generates an rtable with an initializer that maps each index to the corresponding entry in the Cartesian product. The elements of the rtable are then converted to a list by using seq.

CartRtable := proc(L::seq(list))
local i,a,n;
    n := nargs;
    [seq(a, a in rtable(seq(1..nops(a), a in args)
                        ,() -> [seq(L[i][args[i]],i=1..n)]
                        ,'order = C_order'
                       ))];
end proc:

Here is an offering from Carl DeVore (aka Carl Love). I've modified it slightly to use the seq parameter modifier previously mentioned. This routine produces the Cartesian product in column-major order; that is, the left-most element in the lists varies the fastest. All other routines produce the elements in row-major order.

CartProdAsList := proc(L::seq(list))
option `Copyright (C) 2002, Carl DeVore.  All rights reserved.`;
local q,i,N;
    N := map(nops, [L]);
    [seq([seq(L[i][1+irem(q,N[i],'q')], i= 1..nargs)], q=0..`*`(N[])-1)]
end proc:

Here is a suggestion from Helmut Kahovec, also slightly modified.

CartProdRecursive := proc(L::seq(list))
local i,j;
option `Copyright (C) 2001, Helmut Kahovec. All rights reserved.`;
    if nargs=2 then
        [ seq(seq([`if`(i::list,i[],i),
                   `if`(j::list,j[],j)
                  ] , j in args[2] )
              , i in L[1] ) ];

    elif nargs>2 then
        procname( procname(args[1..2]), args[3..-1] );
    else
        args;
    end if;
end proc:

The previous code has a few minor flaws and permits a few optimizations. Here is a non-recursive version that corrects the flaws and improves its performance.

CartProdNonrecursive := proc(L::seq(list))
local i,j,X,lst;
option `Copyright (C) 2007, Joseph Riel. All rights reserved.`;
    if nargs=0 then return [];
    elif nargs=1 then return map(`[]`,L);
    end if;
    X := [seq(seq([i,j], j in L[2] ), i in L[1] )] ;
    for lst in L[3..-1] do
        X := [seq(seq([i[],j], j in lst ), i in X )];
    end do;
    X;
end proc:

Here is an interesting version that I originally devised as a way to write an add-like procedure that summed over multiple dimensions, converted here to use seq to generate the Cartesian product. Because foldl is not a built-in, it is possible to improve it further, however, the resulting speed gains were trivial (less than five percent).

CartProdSeq := proc(L::seq(list))
local Seq,i,j;
option `Copyright (C) 2007, Joseph Riel. All rights reserved.`;
    eval([subs(Seq=seq, foldl(Seq
                              , [cat(i,1..nargs)]
                              , seq(cat(i,j)=L[j],j=nargs..1,-1)
                             ))]);
end proc:
To better understand what it does, run it without the eval statement, or do
test := subs(eval=''eval'',op(CartProdSeq)):
test([1,2,3],[a,b]); %;
            eval([seq(seq([i1, i2], i2 = [a, b]), i1 = [1, 2, 3])])
             [[1, a], [1, b], [2, a], [2, b], [3, a], [3, b]]

The following tabulates, for each procedure, the time and memory required to generate the Cartesian product of a sequence of lists. Four different inputs are tested. For all cases the CartProdSeq is the fastest and uses the least memory.

Compute the cartesian product of N lists of E elements
to get a list of E^N sublists of N elements

gcfreq = 10^7

                     +-------- timing -------+--------- memory ---------+
256^2                | maple (s) | total (s) | used (MiB) | alloc (MiB) | gctimes
---------------------+-----------+-----------+------------+-------------+--------
CartProdDeiterated   |     1.30  |     1.33  |    23.06   |     19.50   |    0
CartProdRtable       |     0.54  |     0.55  |    10.00   |      7.50   |    0
CartProdAsList       |     0.77  |     0.86  |    14.81   |     10.81   |    0
CartProdRecursive    |     0.25  |     0.26  |     7.79   |      5.75   |    0
CartProdNonrecursive |     0.13  |     0.13  |     5.77   |      3.75   |    0
CartProdSeq          |     0.12  |     0.13  |     5.76   |      3.75   |    0
---------------------+-----------+-----------+------------+-------------+--------
10^5                 |           |           |            |             |        
---------------------+-----------+-----------+------------+-------------+--------
CartProdDeiterated   |     2.82  |     2.94  |    43.31   |     32.74   |    1
CartProdRtable       |     1.30  |     1.36  |    19.85   |     14.93   |    0
CartProdAsList       |     1.94  |     1.98  |    27.67   |     21.93   |    0
CartProdRecursive    |     0.74  |     0.74  |    18.41   |     13.56   |    0
CartProdNonrecursive |     0.47  |     0.49  |    14.78   |      9.94   |    0
CartProdSeq          |     0.32  |     0.35  |    13.53   |      9.37   |    0
---------------------+-----------+-----------+------------+-------------+--------
3^10                 |           |           |            |             |        
---------------------+-----------+-----------+------------+-------------+--------
CartProdDeiterated   |     2.28  |     2.34  |    31.04   |     27.99   |    0
CartProdRtable       |     1.08  |     1.10  |    12.54   |     10.56   |    0
CartProdAsList       |     1.85  |     1.87  |    24.33   |     20.68   |    0
CartProdRecursive    |     0.66  |     0.73  |    18.49   |     13.37   |    0
CartProdNonrecursive |     0.43  |     0.46  |    14.54   |     10.19   |    0
CartProdSeq          |     0.24  |     0.27  |    10.56   |      8.25   |    0
---------------------+-----------+-----------+------------+-------------+--------
2^16                 |           |           |            |             |        
---------------------+-----------+-----------+------------+-------------+--------
CartProdDeiterated   |     3.67  |     3.80  |    44.03   |     35.24   |    1
CartProdRtable       |     1.64  |     1.67  |    17.02   |     14.56   |    0
CartProdAsList       |     3.06  |     3.18  |    35.65   |     30.74   |    0
CartProdRecursive    |     1.15  |     1.31  |    28.41   |     23.31   |    0
CartProdNonrecursive |     0.78  |     0.83  |    23.88   |     19.25   |    0
CartProdSeq          |     0.44  |     0.48  |    18.21   |     14.00   |    0
---------------------------------------------------------------------------------

Please Wait...