Overview
Here is a refinement of @Leonid's approach that is a bit faster. The basic idea is to create a TuplesFunction
that encapsulates the tuples information, and can be applied to Part
type specs. That is, instead of using:
Tuples[lists][[part]]
you would use:
tf = TuplesFunction[lists];
tf[part]
The goal is to make TuplesFunction[__]
as small as possible, and TuplesFunction[__][part]
as fast as possible.
Decompose
As described in other answers, it is possible to make a function that takes an integer index and the lengths of the lists, and returns the indices of the lists needed to construct the corresponding tuple. The built-in functions that can do this are NumberDecompose
and IntegerDigits
with MixedRadix
. However, since this is a function that accepts an integer (the index) and a list of integers (the lengths of the lists), and returns a list of integers (the indices of each list needed to construct the tuple), we can use Compile
:
decompose = Compile[{{n, _Integer}, {d, _Integer, 1}},
Module[{c=n, q},
Table[
q = Quotient[c, i];
c = Mod[c, i];
q,
{i, d}
]
],
RuntimeAttributes->{Listable}
];
The function decompose
is modeled after NumberDecompose
hence the second argument should be a basis
, as described in the NumberDecompose
documentation. Here is a check between decompose
and NumberDecompose
:
decompose[103, {49, 7, 1}]
NumberDecompose[103, {49, 7, 1}]
{2, 0, 5}
{2, 0, 5}
and a little speed comparison:
r1 = decompose[Range[10,100], {49, 7, 1}];//RepeatedTiming
r2 = NumberDecompose[#, {49, 7, 1}]& /@ Range[10, 100]; //RepeatedTiming
r1 === r2
{0.000029, Null}
{0.0019, Null}
True
So, compilation definitely helps here. Now, rather than relying on the Listable
attribute to get decompose to work with lists of integers, it is possible to create a compiled function that accepts lists:
decomposeList = Compile[{{n, _Integer, 1}, {d, _Integer, 1}},
Module[{c=n, q},
Table[
q = Quotient[c, i];
c = Mod[c, i];
q,
{i, d}
]
]
];
Let's compare the two compiled functions:
r1 = decompose[Range[1000], {10000, 100, 1}]; //RepeatedTiming
r2 = decomposeList[Range[1000], {10000, 100, 1}]; //RepeatedTiming
r1 === Transpose[r2]
{0.000089, Null}
{0.000057, Null}
True
Even faster, although the returned result is transposed. This actually turns out to be a good thing.
Index decomposition to tuple
Next, we need to convert the list of indices into a tuple. In the following examples I use the lists {Range[10], Range[5], Range[7]}
so that tuples extraction is obvious. For a single list of indices, we can use MapThread
:
MapThread[Part, {{Range[10], Range[5], Range[7]}, {2, 4, 3}}]
{2, 4, 3}
If we have multiple lists of indices, we can use MapThread
again, but this time we need to transpose first:
Transpose @ MapThread[
Part,
{
{Range[10], Range[5], Range[7]},
Transpose[{{2,4,3}, {5,2,3}, {7,1,2}}]
}
]
{{2, 4, 3}, {5, 2, 3}, {7, 1, 2}}
Span support
One final enhancement. It would be nice to use Span
in the definition of TuplesFunction
. To do this, we need a way to convert a Span
specification to a list of indices. Here is a function to do this:
toList[Span[a_, b_, c_:1], max_] := With[
{
x = Replace[a, {All->1, UpTo[x_]:>Min[x,max]}],
y = Replace[b, {All->max, -1->max, UpTo[x_]:>Min[x,max]}]
},
Range[x, y, Replace[c, {All -> If[x<=y, 1, -1], Except[_Integer]->1}]]
]
A few examples:
toList[1 ;; ;; 2, 10]
toList[UpTo[13] ;; -1, 20]
toList[UpTo[23] ;; UpTo[8], 20]
toList[UpTo[23] ;; UpTo[8] ;; All, 20]
{1, 3, 5, 7, 9}
{13, 14, 15, 16, 17, 18, 19, 20}
{}
{20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8}
TuplesFunction
Those are the pieces we need to use in the TuplesFunction
definition. The following code block is complete. It has all the definitions for TuplesFunction
as well as a summary box format and the earlier definitions for decompose, decomposeList and toList;
TuplesFunction[lists_] := With[{lens = Length /@ lists},
TuplesFunction[
lists,
Reverse @ FoldList[Times, 1, Reverse @ Rest @ lens]
]
]
TuplesFunction[lists_, basis_][index_Integer] := With[
{decomp = 1 + decompose[index-1, basis]},
MapThread[Part, {lists, decomp}]
]
TuplesFunction[lists_, basis_][indices:{__Integer}] := With[
{decomp = 1 + decomposeList[indices-1, basis]},
Transpose @ MapThread[Part, {lists, decomp}]
]
TuplesFunction[lists_, basis_][span_Span] := With[
{r = toList[span, Times @@ Length /@ lists]},
TuplesFunction[lists, basis][r]
]
MakeBoxes[t:TuplesFunction[lists_List, ___], StandardForm] ^:= Module[
{lens=Length/@lists},
BoxForm`ArrangeSummaryBox[
TuplesFunction,
t,
BarChart[lens,ImageSize->30,Axes->False],
{
BoxForm`MakeSummaryItem[{"Count: ",Times@@lens}, StandardForm],
BoxForm`MakeSummaryItem[{"Length: ", lens}, StandardForm]
},
{},
StandardForm,
"Interpretable"->True
]
]
decompose = Compile[{{n, _Integer}, {d, _Integer, 1}},
Module[{c=n, q},
Table[
q = Quotient[c, i];
c = Mod[c, i];
q,
{i, d}
]
],
RuntimeAttributes->{Listable}
];
decomposeList = Compile[{{n, _Integer, 1}, {d, _Integer, 1}},
Module[{c=n, q},
Table[
q = Quotient[c, i];
c = Mod[c, i];
q,
{i, d}
]
]
];
toList[Span[a_, b_, c_:1], max_] := With[
{
x = Replace[a, {All->1, UpTo[x_]:>Min[x,max]}],
y = Replace[b, {All->max, -1->max, UpTo[x_]:>Min[x,max]}]
},
Range[x, y, Replace[c, {All -> If[x<=y, 1, -1], Except[_Integer]->1}]]
]
Here is an example:
tf = TuplesFunction[{Range[4], Range[2], Range[3]}]
r1 = Tuples[{Range[4], Range[2], Range[3]}][[5 ;; 15]];
r2 = tf[5 ;; 15];
r1 === r2
TuplesFunction[{{1, 2, 3, 4}, {1, 2}, {1, 2, 3}}, {6, 3, 1}]
True
Here is a timing comparison between using Tuples
and TuplesFunction
:
r1 = Tuples[{Range[100], Range[100], Range[100]}]; //RepeatedTiming
tf = TuplesFunction[{Range[100], Range[100], Range[100]}]
r2 = tf /@ Partition[Range[10^6], 1000]; //RepeatedTiming
r1 === Flatten[r2, 1]
{0.0066, Null}
TuplesFunction[CompressedData["
1:eJzt0bVCQgEAQNFnYwN2oIKY2N1gYyt2Kzrr/2+cxdkfeMMZ7nyTxd/CT3kQ
BBV8U8ZfV1JFNTVEqKWOehpopIlmosSI00IrbbTTQSdddNNDLwn66GeAJCkG
STPEMCOMMsY4GSaYZIppZphljnkWWGSJZVZYZY11Nthkiyw5ttlhlz32OeCQ
PEccc8IpZ5xzwSVXFLjmhlvuuOeBR5545oVX3njng0+KfIU/wh/hj39/lADu
ED1K
"], {10000, 100, 1}]
{0.11, Null}
True
A little over an order of magnitude slower than just using Tuples
, but the memory footprint is far less. Here is a memory comparison:
r1 = Total @ Tuples[{Range[100], Range[100], Range[100]}]; //MaxMemoryUsed
r2 = Sum[Total @ tf[ Span[1000l + 1, 1000(l+1)] ], {l, 0, 999}]; //MaxMemoryUsed
r1 === r2
24003312
253096
True
One final example:
tf = TuplesFunction[{Range[10^4], Range[10^4], Range[10^4]}];
tf[10^9 -10 ;; 10^9 + 10] //AbsoluteTiming
{0.000086, {{10, 10000, 9990}, {10, 10000, 9991}, {10, 10000, 9992}, {10,
10000, 9993}, {10, 10000, 9994}, {10, 10000, 9995}, {10, 10000, 9996}, {10,
10000, 9997}, {10, 10000, 9998}, {10, 10000, 9999}, {10, 10000,
10000}, {11, 1, 1}, {11, 1, 2}, {11, 1, 3}, {11, 1, 4}, {11, 1, 5}, {11, 1,
6}, {11, 1, 7}, {11, 1, 8}, {11, 1, 9}, {11, 1, 10}}}
Tuples
, from which lazyOuter
is pretty easy to get. Unfortunately, that answer relies on undocumented functionality, so I can't at the moment recommend it for anything more than an illustration, since it is not based on officially supported features. $\endgroup$