Cartesian for-each at runtime

combinatorics

Useful when the number of arguments to a Cartesian product is determined at runtime.

Author

Bimal Gaudel

Published

November 15, 2022

Cartesian product

In mathematics, the Cartesian product of two sets \(A\) and \(B\) is the set: \[A\times B := \{(x,y) : x \in A,\ y \in B\}\] For two C++ vectors, \(\{0,1,2\}\) and \(\{3,4\}\), the product is a vector of two-tuples: \(\{(0,3),(0,4),(1,3),(1,4),(2,3),(2,4)\}\). The product of \(n\) vectors is easy to generate with nested for loops or template meta-programming, as long as \(n\) is known at compile time.

Cartesian product at runtime

Sometimes \(n\) is only known at runtime. If we knew its maximum, we could still pre-generate a function for each value of \(n\) via template meta-programming, but here we generate the product dynamically instead. We keep to vectors for simplicity; the scheme applies to other ‘vector-like’ types too.

Breadth-first algorithm (pseudo-code)

The breadth-first algorithm is straightforward to think of.

// procedure: extend_cp
// input: cps, es
// output: cps extended

result <- {}
for p in cps
  for e in es
    p_ <- p
    p_.append(e)
    result.append(p_)
return result

// procedure: cartProdBf
// input: vov vector<vector<T>> vector of vectors (for eg.)
// output: vov_ vector<vector<T>> cart prod result
if vov.empty
  return {}

//
// <| front |>
//  |   .   | . | . | . . . | . | (vov)
//          |<      tail       >|
//
init <- {{e} for e in vov.front} // cart prod seed

// see https://en.cppreference.com/w/cpp/algorithm/accumulate
return accumulate(vov.tail, init, extend_cp)

Better approach: depth-first algorithm

The breadth-first algorithm materializes every member of the product at once. Better to generate one member at a time and act on it (say, call a function on it). This depth-first algorithm does that with while loops and iterator arithmetic. I borrowed it from my colleague Andrey Asadchev’s work in TiledArray.

//
// filename: cartesian_foreach.hpp
//
// required headers:
//   - vector
//   - utility
//   - functional

template <typename R, typename F>
void cartesian_foreach(std::vector<R> const& rs, F&& call_back) {
  using It = decltype(std::begin(rs[0]));
  using T = typename R::value_type;

  if (rs.empty()) return;

  // a Cartesian product with an empty factor is empty
  for (auto&& r : rs)
    if (std::begin(r) == std::end(r)) return;

  std::vector<It> its, ends;
  its.reserve(rs.size());
  ends.reserve(rs.size());

  for (auto&& r : rs) {
    its.push_back(std::begin(r));
    ends.push_back(std::end(r));
  }

  while (its.front() != ends.front()) {
    std::vector<T> p;
    p.reserve(its.size());

    for (auto&& it: its)
      p.push_back(*it);

    // do something with the cartesian product
    std::invoke(std::forward<F>(call_back), p);

    size_t i = its.size();
    while (i > 0) {
      --i;
      ++its[i];
      if (i == 0) break;
      if (its[i] != ends[i]) break;
      its[i] = std::begin(rs[i]);
    }
  }
}

Example use

//
// file name: main.cpp
//
// required headers:
//   - cartesian_foreach.hpp
//   - vector
//   - iterator
//   - iostream

// prints a vector of printables
template <typename T>
std::ostream& operator<<(std::ostream& os, std::vector<T> const& vec) {
  os << "{";
  if (!vec.empty()) {
    os << vec.front();
    for (auto it = std::next(vec.begin()); it != vec.end(); ++it)
      os << ", " << *it;
  }
  os << "}";
  return os;
}

//
// main function
//
int main() {
  using std::cout;
  using std::endl;

  auto vov = std::vector<std::vector<int>>{{1, 2}, {3, 4}, {5, 6}};

  auto print_vec = [](auto const& v) {
    cout << v << endl;
  };

  cartesian_foreach(vov, print_vec);

  cartesian_foreach(decltype(vov){}, print_vec); // empty vov, no output

  return 0;
}

Output:

{1, 3, 5}
{1, 3, 6}
{1, 4, 5}
{1, 4, 6}
{2, 3, 5}
{2, 3, 6}
{2, 4, 5}
{2, 4, 6}