Every NP problem shares a defining shape: somebody hands you a candidate answer, and you can check it in a reasonable amount of time. Verifying that a delivery route hits every stop, that a schedule violates no rules, that a circuit design wires up correctly — all fast. Producing the route, the schedule, the design in the first place can be vastly harder. The asymmetry between checking and producing is where most of the difficulty in software engineering hides. Build pipelines are checkers. Test suites are checkers. Code review is a checker. The hard, slow, expensive work — the actual writing, planning, designing — is on the producing side. When your team can describe a checker for a problem but cannot describe a producer, the problem is probably NP. Treat the estimate accordingly.
02
Checking is cheaper than building
The gap between verifying an answer and producing one is the whole reason consultants exist.
- A schedule checker is a tight loop over the constraints — one pass, no search. Producing a valid schedule from the same constraints is the expensive half and would call a solver. Same data, two complexity classes.
struct Shift { staff: u32, start: u32, end: u32 } fn schedule_is_valid(shifts: &[Shift], min_gap: u32) -> bool { let mut by_staff: std::collections::HashMap<u32, Vec<&Shift>> = Default::default(); for s in shifts { by_staff.entry(s.staff).or_default().push(s); } for assignments in by_staff.values_mut() { assignments.sort_by_key(|s| s.start); for w in assignments.windows(2) { if w[0].end + min_gap > w[1].start { return false; } } } true } -
HashMap<K, V>is Rust's hash table — a keyed lookup that takes a key and gives you back a value in roughly constant time.Vec<T>is a growable list ofTstored in one contiguous block. Combined here asHashMap<u32, Vec<&Shift>>, the map keys are staff IDs and each value is the list of that person's shifts. The reason for the grouping: the no-overlap rule only applies within one person's day — Alice's morning shift cannot conflict with Bob's afternoon. Group first, then we only have to worry about one person at a time.// What the grouped data looks like after the first loop: // // by_staff: // 17 ─► [Shift { start: 900, end: 1300, .. }, // Shift { start: 1400, end: 1800, .. }] // 42 ─► [Shift { start: 1000, end: 1400, .. }] // 91 ─► [Shift { start: 800, end: 1200, .. }, // Shift { start: 1230, end: 1700, .. }] // // .entry(key) is the canonical "get or insert" pattern. // .or_default() inserts an empty Vec the first time we see a staff ID. // .push(s) appends — Vec grows automatically. let mut by_staff: HashMap<u32, Vec<&Shift>> = HashMap::new(); for s in shifts { by_staff.entry(s.staff).or_default().push(s); } - Sort each person's shifts by start time, then walk consecutive pairs.
windows(2)is a slice helper that yields every overlapping pair:[a, b], then[b, c], then[c, d]. After sorting, if shiftw[0]ends at minute 1300 and shiftw[1]starts at minute 1310, the actual gap is 10 minutes. The rule "at leastmin_gapbetween shifts" meansw[1].start − w[0].end ≥ min_gap, which rearranges tow[0].end + min_gap ≤ w[1].start. If the left side is greater, the gap was violated and the schedule is invalid.// Sort puts the earliest start first; windows(2) walks adjacent pairs. assignments.sort_by_key(|s| s.start); for w in assignments.windows(2) { // Want: w[1].start - w[0].end >= min_gap // i.e.: w[0].end + min_gap <= w[1].start // Violation is the opposite: if w[0].end + min_gap > w[1].start { return false; } } // If no pair fails for any staff member, the schedule is valid. - One staff member's timeline. The earlier shift ends, then a buffer, then the next shift starts. When the buffer is at least
min_gap, the schedule is fine; when it is less, the check fails on this pair and the function short-circuits withfalse.min_gap = 30 minutes OK (gap = 60 min, passes) ─────────────────────────────────────────────────────────────► time w[0] w[1] ╔══════════════╗ gap = 60 min ╔══════════════╗ ║ 09:00 – ║◄─────────────────────► ║ 14:00 – ║ ║ 13:00 ║ ║ 18:00 ║ ╚══════════════╝ ╚══════════════╝ ↑ ↑ w[0].end = 13:00 w[1].start = 14:00 13:00 + 30 = 13:30 ≤ 14:00 ✓ FAIL (gap = 10 min, fails) ─────────────────────────────────────────────────────────────► time w[0] w[1] ╔══════════════╗ 10 min ╔══════════════╗ ║ 09:00 – ║◄──────► ║ 13:10 – ║ ║ 13:00 ║ ║ 17:10 ║ ╚══════════════╝ ╚══════════════╝ ↑ ↑ 13:00 + 30 = 13:30 > 13:10 ✗ return false