Cartesian product
vector of vectors
combinatorics
programming
algorithm

How can I create the cartesian product of a vector of vectors?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Creating the Cartesian product of a vector of vectors is a common combinatorics task in testing, search spaces, and configuration generation. The output contains every combination selecting one element from each input vector. Straightforward implementations work for small inputs, but product size grows multiplicatively and can explode quickly. A good solution should be clear, memory-aware, and capable of streaming combinations when full materialization is too large.

Core Sections

1. Iterative Cartesian product approach (C++)

cpp
1#include <vector>
2
3std::vector<std::vector<int>> cartesianProduct(const std::vector<std::vector<int>>& input) {
4    std::vector<std::vector<int>> result = { { } };
5
6    for (const auto& vec : input) {
7        std::vector<std::vector<int>> next;
8        for (const auto& prefix : result) {
9            for (int x : vec) {
10                auto combo = prefix;
11                combo.push_back(x);
12                next.push_back(std::move(combo));
13            }
14        }
15        result = std::move(next);
16    }
17
18    return result;
19}

This is simple and works well for moderate product sizes.

2. Recursive depth-first generation

cpp
1void dfs(const std::vector<std::vector<int>>& input, size_t idx,
2         std::vector<int>& current,
3         std::vector<std::vector<int>>& out) {
4    if (idx == input.size()) {
5        out.push_back(current);
6        return;
7    }
8    for (int x : input[idx]) {
9        current.push_back(x);
10        dfs(input, idx + 1, current, out);
11        current.pop_back();
12    }
13}

Recursion is expressive and easy to adapt for pruning logic.

3. Handle empty vectors explicitly

If any inner vector is empty, the Cartesian product is empty. Check early:

cpp
for (const auto& v : input) {
    if (v.empty()) return {};
}

This prevents unnecessary work.

4. Streaming instead of storing all combinations

For very large products, process each combination on the fly instead of building a huge output vector. Replace out.push_back with callback invocation.

cpp
auto consume = [](const std::vector<int>& combo) {
    // process combo
};

Streaming reduces peak memory pressure.

5. Complexity awareness

If vector sizes are n1, n2, ..., nk, output count is n1*n2*...*nk. Time and memory for full materialization scale with this product. Always estimate cardinality before execution.

6. Testing strategy

Add tests for:

  • single vector input
  • one empty inner vector
  • mixed vector lengths
  • large input where streaming mode is required

This ensures correctness across boundary scenarios.

Validation and production readiness

A reliable implementation is not complete until it is validated under realistic conditions. Add a minimal but representative test matrix that includes normal inputs, edge cases, and malformed data. For UI-focused topics, include at least one scenario for lifecycle or timing behavior (initial load, state transition, and cleanup) so regressions are detected when framework versions change. For infrastructure and tooling topics, run commands against a disposable environment before applying in production and capture expected outputs in documentation. This reduces ambiguity when teammates reproduce steps later.

Instrumentation is equally important. Add structured logs around the critical path, including input shape, selected branch decisions, and failure reasons. Keep logs concise and machine-parseable so alerts and dashboards can surface patterns quickly. If operations are expensive or remote (network, filesystem, container orchestration), include timeout handling and explicit retry policy with backoff. Silent retries without bounds are a common source of hidden incidents.

Finally, document assumptions and compatibility boundaries near the code or article examples: runtime versions, platform requirements, and known behavior differences across environments. Add a lightweight checklist for rollouts that covers dependency pinning, backup/rollback strategy, and smoke checks after deployment. Teams that treat these steps as part of the baseline implementation, not optional polish, usually see fewer production surprises and faster recovery when issues occur.

Common Pitfalls

  • Ignoring exponential growth of combination count.
  • Materializing full result when streaming would suffice.
  • Failing to handle empty inner vectors correctly.
  • Reusing mutable buffers incorrectly and corrupting results.
  • Missing boundary tests for single/empty input cases.

Summary

The Cartesian product of a vector of vectors is straightforward to generate with iterative or recursive patterns. The real challenge is growth management: output size increases multiplicatively, so memory-aware design is essential. Add early-empty checks, optional streaming, and boundary tests to keep implementation both correct and scalable.


Course illustration
Course illustration

All Rights Reserved.