Set Cover Problem
Hitting Set Problem
Numpy Programming
Combinatorial Optimization
Python Algorithms

Set Cover or Hitting Set; Numpy, Least element combinations to make up full set

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

Introduction

Set cover and hitting set are classic combinatorial optimization problems. In set cover, you choose the fewest subsets whose union covers the universe. In hitting set, you choose the fewest elements that intersect every subset. They are dual problems and both are NP-hard, so practical solutions often use greedy approximations or integer programming.

With NumPy, you can model subsets as boolean incidence matrices and implement fast greedy heuristics. This gives good results for medium-size instances where exact search is expensive.

Core Sections

1. Encode subsets as a boolean matrix

Let rows represent subsets and columns represent elements in the universe.

python
1import numpy as np
2
3# 4 subsets over universe elements 0..5
4A = np.array([
5    [1, 0, 1, 0, 1, 0],
6    [0, 1, 1, 0, 0, 1],
7    [1, 1, 0, 1, 0, 0],
8    [0, 0, 1, 1, 1, 0],
9], dtype=bool)

For set cover with candidate subsets, transpose representation as needed.

2. Greedy approximation for set cover

At each step choose the subset that covers the most uncovered elements.

python
1def greedy_set_cover(subsets, universe_size):
2    uncovered = np.ones(universe_size, dtype=bool)
3    chosen = []
4
5    while uncovered.any():
6        gains = np.array([(s & uncovered).sum() for s in subsets])
7        idx = gains.argmax()
8        if gains[idx] == 0:
9            break
10        chosen.append(idx)
11        uncovered &= ~subsets[idx]
12
13    return chosen, uncovered
14
15subsets = [A[0], A[1], A[2], A[3]]
16chosen, uncovered = greedy_set_cover(subsets, 6)
17print("chosen subsets:", chosen, "uncovered left:", uncovered.sum())

This is fast and often near-optimal, though not guaranteed minimal.

3. Greedy approximation for hitting set

For hitting set, choose elements that hit the most currently unhit subsets.

python
1def greedy_hitting_set(incidence):
2    # incidence: rows=subsets, cols=elements
3    remaining_rows = np.ones(incidence.shape[0], dtype=bool)
4    picked_cols = []
5
6    while remaining_rows.any():
7        # count how many remaining subsets each element hits
8        counts = (incidence[remaining_rows].sum(axis=0)).astype(int)
9        c = int(counts.argmax())
10        if counts[c] == 0:
11            break
12        picked_cols.append(c)
13        # remove subsets hit by chosen element
14        hits = incidence[:, c]
15        remaining_rows &= ~hits
16
17    return picked_cols, remaining_rows

4. Exact solution with ILP when needed

If optimality matters and instance size is manageable, solve via integer linear programming (for example OR-Tools or PuLP). Use greedy result as baseline and upper bound.

5. Add weighting and costs

Real systems often assign cost to subsets/elements. Modify greedy score from raw coverage count to gain-per-cost ratio.

Common Pitfalls

  • Expecting greedy output to always be the minimal exact set cover/hitting set.
  • Building incidence arrays with inconsistent indexing between subsets and elements.
  • Ignoring infeasible cases where some universe elements are never coverable.
  • Using Python loops for large matrices instead of vectorized NumPy operations.
  • Forgetting weighted variants when subset/element costs differ significantly.

Summary

Set cover and hitting set are hard optimization problems, but NumPy-based greedy heuristics provide practical solutions quickly. Model data as boolean incidence matrices, choose maximum gain at each step, and validate coverage after selection. For strict optimality, escalate to ILP on manageable instances. This workflow balances computational cost and solution quality for many real engineering use cases.

To make this guidance robust in day-to-day engineering work, treat it as an executable checklist instead of one-time reading material. Capture the expected environment, dependency versions, runtime flags, and validation commands in your repository so every contributor can reproduce the same behavior from a clean setup. This is especially important when onboarding new developers, rotating on-call ownership, or debugging incidents under time pressure. Documentation that includes concrete commands, expected outputs, and failure interpretation prevents repeat confusion and shortens recovery time.

It is also worth adding at least one automated guardrail in CI that validates the highest-risk assumption described in the article. Depending on the topic, that guardrail may be a smoke test, policy check, schema validation, benchmark threshold, import check, or integration assertion against a minimal fixture. The goal is to fail fast when environment drift or configuration changes reintroduce old errors. Teams that convert troubleshooting knowledge into small, repeatable checks reduce operational noise and keep this class of issue from returning every sprint.


Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design