#include <iostream>
#include <vector>
void cartesian (std::vector<std::vector<int>> const& items) {
auto n = items.size();
auto next = [&](std::vector<int> & x) {
for ( int i = 0; i < n; ++ i )
if ( ++x[i] == items[i].size() ) x[i] = 0;
else return true;
return false;
};
auto print = [&](std::vector<int> const& x) {
for ( int i = 0; i < n; ++ i )
std::cout << items[i][x[i]] << ",";
std::cout << "\b \n";
};
std::vector<int> x(n);
do print(x); while (next(x)); // Shazam!
}
int main () {
std::vector<std::vector<int>>
items { { 1, 2, 3 }, { 4, 5 }, { 6, 7, 8 } };
cartesian(items);
return 0;
}
The idea behind this is as follows.
Let n := items.size()
.
Let m_i := items[i].size()
, for all i
in {0,1,...,n-1}
.
Let M := {0,1,...,m_0-1} x {0,1,...,m_1-1} x ... x {0,1,...,m_{n-1}-1}
.
We first solve the simpler problem of iterating through M
. This is accomplished by the next
lambda. The algorithm is simply the "carrying" routine grade schoolers use to add 1, albeit with a mixed radix number system.
We use this to solve the more general problem by transforming a tuple x
in M
to one of the desired tuples via the formula items[i][x[i]]
for all i
in {0,1,...,n-1}
. We perform this transformation in the print
lambda.
We then perform the iteration with do print(x); while (next(x));
.
Now some comments on complexity, under the assumption that m_i > 1
for all i
:
- This algorithm requires
O(n)
space. Note that explicit construction of the Cartesian product takes O(m_0 m_1 m_2 ... m_{n-1}) >= O(2^n)
space. So this is exponentially better on space than any algorithm which requires all tuples to be stored simultaneously in memory.
- The
next
function takes amortized O(1)
time (by a geometric series argument).
- The
print
function takes O(n)
time.
- Hence, altogether, the algorithm has time complexity
O(n|M|)
and space complexity O(n)
(not counting the cost of storing items
).
An interesting thing to note is that if print
is replaced with a function which inspects on average only O(1)
coordinates per tuple rather than all of them, then time complexity falls to O(|M|)
, that is, it becomes linear time with respect to the size of the Cartesian product. In other words, avoiding the copy of the tuple each iterate can be meaningful in some situations.