Optimization and Operations Research Methods Questions
Prescriptive analytics: formulating decisions as optimization problems — linear and integer programming, constraint-based modeling, objective functions, and trade-offs between optimality and tractability. Applied to allocation, scheduling, routing, pricing, and supply/demand problems.
Explain Donald Knuth's Dancing Links (DLX) algorithm for solving exact cover problems and why it is effective for Sudoku. Describe the circular doubly linked list data structure, how cover and uncover operations remove and restore rows and columns efficiently, and discuss trade-offs compared to standard recursive backtracking with bitmasks.
Sample Answer
Donald Knuth's Dancing Links (DLX) is a highly efficient technique for solving exact-cover problems (like Sudoku) using an exact-cover formulation and a sparse, circular doubly linked list to represent the binary constraint matrix.
Core idea
- Model the problem as a binary matrix where each row is a candidate (e.g., placing digit d in cell (r,c)) and each column is a constraint (e.g., "cell r,c filled", "row r has digit d", "col c has digit d", "block b has digit d").
- The goal: choose rows so each column has exactly one 1.
Data structure: circular doubly linked lists
- Each 1 in the matrix is a node with pointers: left, right (across its row) and up, down (within its column). Each column has a header node that tracks column size (count of remaining 1s).
- Rows and columns are circularly linked so removals and restorations are local pointer updates with no array shifting.
Cover and uncover operations (why they’re efficient)
- cover(column C):
- Remove C's header from the header list (unlink left/right).
- For each node down the column C, for each node right in that row, unlink that node from its column (up/down) and decrement its column count.
- This removes all rows that conflict with choosing a row that covers C.
- uncover(column C):
- Reverse the above in exact opposite order: for each node up the column, for each node left in that row, relink node into its column and increment counts; finally relink C into header list.
- Because operations are pointer manipulations only, they are O(k) where k is number of ones touched. Uncover is exact inverse—guarantees correctness and efficient backtracking without copying state.
Why effective for Sudoku
- Sudoku maps to an exact-cover instance with ~324 columns and up to 999 = 729 candidate rows; matrix is very sparse.
- DLX excels on sparse matrices with many interdependent constraints because cover/uncover removes large swaths of candidates cheaply and precisely.
- Column heuristic: choose the column with smallest size (fewest candidate rows) — strong pruning that DLX supports cheaply via header node counts.
Trade-offs vs. standard recursive backtracking with bitmasks
- Performance: DLX often outperforms basic backtracking because it prunes globally and updates constraints incrementally; the minimum-column heuristic is equivalent to MRV but applied to exact-cover columns.
- Memory: DLX uses O(number of 1s) nodes (a few thousand for Sudoku) and pointer overhead; bitmask approaches use compact integer masks (very memory-light).
- Implementation complexity: DLX is more complex to implement and reason about (pointer operations, correct uncover order). Bitmask backtracking is simpler, easier to test, and often fast enough with good heuristics.
- Constant factors: bitmask operations are highly optimized (bitwise ops), so for small puzzles a well-tuned bitmask solver may match or beat a naive DLX unless DLX is implemented carefully.
- Extensibility: DLX generalizes to arbitrary exact-cover problems; bitmask solvers are tailored to specific constraint structure (e.g., Sudoku) and may be simpler to adapt within that domain.
Example pseudo-step for solving Sudoku with DLX
- Build columns for 4 constraint types (cell, row-digit, col-digit, block-digit).
- Create a row for each possible (r,c,d) setting 4 ones in the appropriate columns.
- Run recursive search:
- If header list empty -> solution found.
- Choose column with smallest size.
- cover(col); for each row r in col: select r, cover every column where r has a 1, recurse, then uncover in reverse order; uncover(col).
Summary
DLX is powerful because its linked representation supports constant-time local modifications and exact reversible backtracking. For sparse, highly constrained problems like Sudoku it provides strong pruning and often superior search performance; trade-offs include implementation complexity and pointer/memory overhead versus the simplicity and compactness of bitmask-based backtracking.
Design a backtracking-based solver for small k-SAT instances that includes unit propagation and pure literal elimination. Describe the data structures for clauses and literals, how to implement efficient incremental unit propagation during recursion, watch-list or clause-count techniques, and branching heuristics that choose variables with high impact.
Sample Answer
Approach summary:
Build a recursive DPLL-style solver with incremental unit propagation, pure-literal elimination, two-watch-literal scheme for clauses, a trail with reversible assignments, and a branching heuristic (Jeroslow-Wang + activity/VSIDS-style decay).
Data structures:
- Clause: list/array of signed literals (int: var_id * 2 + sign). Store clause length and two watch indices.
- Watches: array mapping literal -> list of clause IDs watching that literal.
- Assignment arrays:
- value[var] ∈ {UNASSIGNED, TRUE, FALSE}
- reason[var] = clause_id that implied the assignment (or -1 for decision)
- level[var] = decision level
- Trail: stack of assigned vars in order; level pointers: stack of indices marking decision level starts.
- Occurrence counts / activity[var]: float counter for heuristic.
Incremental unit propagation (two-watch scheme):
- When assigning literal ℓ false, iterate watch list for ¬ℓ:
- For each clause c watching ¬ℓ, try to find another unwatched literal ℓ' in c that is not currently assigned false.
- If found, move watch from ¬ℓ to ℓ' (update watches lists).
- Else if the other watched literal is assigned false -> clause is unsatisfied: conflict.
- Else the other watched literal is unassigned -> this clause becomes unit: enqueue implied assignment for that literal; set reason[c], push to trail.
- For each clause c watching ¬ℓ, try to find another unwatched literal ℓ' in c that is not currently assigned false.
- Maintain a propagation queue (FIFO) of newly implied literals; process until empty or conflict.
- On backtrack: pop trail to saved trail size, undo value/reason/level; no need to revert watches because watches are only moved forward when some watched literal becomes false; on undo we may restore watched state lazily (common implementation keeps watches consistent because watches depend on current assignment; when backtracking to a level where earlier watch moves are invalid, a future scan will reestablish watches). Simpler: when moving watch, record a small log to revert watch moves on backtrack.
Pure-literal elimination:
- Maintain counters per literal (positive/negative occurrences in currently active clauses). After propagation at a level, scan variables with one polarity only (count>0 and opposite==0) and assign them permanently (at current level) to satisfy; treat like unit propagation (enqueue). Update counts when clauses become satisfied or removed by assignments.
Branching heuristic:
- Initialize activity using Jeroslow-Wang scores: sum(2^{- |c|}) over clauses containing literal.
- Maintain activity[var] (sum of its two literal activities). On each conflict, bump activities for variables in conflict clause and decay periodically (VSIDS). Choose the unassigned var with highest activity; break ties by highest occurrence in short clauses.
- Consider lookahead: for small k-SAT, try limited unit-propagation simulation of setting var=true/false and choose side causing most propagations / early conflict (impact-based branching).
Pseudo-code (propagate core) in Python:
def propagate():
while queue:
lit = queue.pop()
for cid in watches[neg(lit)][:]: # iterate a shallow copy
clause = clauses[cid]
if clause.is_satisfied(): continue
# find new watch
found = False
for l in clause.lits:
if l == neg(lit): continue
if value[var(l)] != FALSE:
# move watch: replace neg(lit) by l
watches[neg(lit)].remove(cid)
watches[l].append(cid)
found = True
break
if not found:
other = clause.other_watch(neg(lit))
if value[var(other)] == FALSE:
return conflict_clause(cid)
if value[var(other)] == UNASSIGNED:
assign(var(other), sign(other), reason=cid)
queue.append(other)
return None
Trade-offs and engineering notes:
- Two-watch literal gives near-linear propagation cost; logging watch moves (or using copy-on-write) makes backtracking O(#moves) reversible.
- Pure-literal scanning is cheap for small k but expensive at large scale; do it optionally at root-level or periodically.
- For very small instances, aggressive lookahead and clause learning (1st UIP) yield big wins; for simplicity you can omit learning but keep conflict-driven activity updates.
- Edge cases: tautological clauses, duplicate literals, empty clause detection, initial unit clauses must be enqueued before search, variable ordering when many ties.
Complexity:
- Propagation per assignment is amortized O(1) per watched clause visit; overall search exponential in worst-case but efficient for small k due to pruning.
This design balances simplicity, efficiency, and engineering practicality for small k-SAT solver implementation in C++/Java/Python.
Design a production-ready CSP enumerator library API that uses recursive backtracking under the hood and supports heuristics MRV, degree heuristic, forward checking, and AC-3. Describe API functions, data models for variables/domains/constraints, incremental state updates for assignments (undo stacks), parallelization strategy, observability/metrics and performance tuning knobs.
Sample Answer
Requirements (brief): library to enumerate CSP solutions production-ready: stable API, pluggable heuristics (MRV, degree, forward checking, AC-3), incremental state with undo, parallel enumeration, observability, and tuning.
API surface (idiomatic, language-agnostic)
- CSP.Builder(): build problem
- addVariable(name, domain) / addVariables(Map)
- addConstraint(name, scope: List<Var>, predicate: (List<Value>) -> bool, metadata)
- setHeuristics({valueOrder?, varOrder?: [MRV, Degree], inference?: [None, ForwardChecking, AC3]})
- setMaxSolutions(n), setTimeout(ms), setParallelism(k)
- onSolution(callback), onProgress(callback(metrics))
- build() -> CSP
- CSP.solve(ctx) -> SolverHandle
- SolverHandle.cancel(), pause(), resume(), status()
- metrics(): currentNodes, backtracks, pruned, queueSize, avgPropTime
- Enumerator API: next(), stream(), toList(limit)
Core data models
- Variable {id, name, domain: DomainRef}
- DomainRef: immutable base domain + current bitmap/reference to active values (supports copy-on-write)
- Constraint {id, scope: list of var ids, checkFunc, type: extensional/intensional, adjacencies}
- Assignment: array mapping varId->value or UNASSIGNED
Algorithm & incremental state
- Recursive backtracking engine with explicit stack frames:
- Frame: varId, iterator over ordered domain values, saved changes index
- Global trail/undo stack: records (type, targetRef, oldState) lightweight diffs (e.g., domain bit flips)
- When assign var v = x: push assignment; apply inference:
- Forward checking: remove incompatible values from neighbor domains and push diffs to trail
- AC-3: enqueue arcs affected; apply arc consistency, pushing domain removals to trail
- On backtrack: pop trail to checkpoint recorded at frame start to restore domains and assignments
- Heuristics:
- MRV: select var with smallest current domain size (maintain heap keyed by domain-size with updates on domain change)
- Degree tie-breaker: precomputed static degree, secondary sort
- Value ordering: user-provided comparator or default (no order)
Parallelization strategy
- Portfolio + search-space splitting:
- At top-level, expand first N branches sequentially to depth D to produce disjoint subproblems (work units)
- Worker threads/processes take units from work queue; each has local copy-on-write state (domains can be shared read-only until mutated)
- Use cooperative cancellation and periodic checkpointing to central metrics
- Avoid locking hot paths: work-stealing queue, per-worker trail
- For distributed: serialize units (variables assignments + reduced domains) over RPC
Observability & metrics
- Built-in metrics exposed via callbacks, Prometheus client, and logs:
- nodesVisited, backtracks, failures, domainPrunes, AC3Invocations, avgPropTime, solutionsFound, memoryUsage, per-worker utilization
- Events: assignmentApplied, domainPruned, backtrack, solutionFound
- Tracing: optional span per solve with tags for heuristics and timing
Performance knobs
- maxParallelWorkers, splittingDepth, maxQueueSize, inferenceLevel (None/ForwardChecking/AC3), enableCaching (nogoods), nogoodLimit, varHeapUpdateStrategy (lazy vs eager), memoryLimit, checkpointInterval
- Tunables for AC-3: arc-queue ordering, incremental enqueue predicates
- Profiling mode to record hot constraints and domain-change hotspots
Reliability & production concerns
- Deterministic mode (fixed seed, deterministic ordering)
- Resource-aware cancellation and graceful shutdown
- Safe serialization for pause/resume and distributed execution
- Comprehensive test suite: randomized tests, contrived worst-cases, and performance benchmarks
This design balances algorithmic correctness (undo trail, AC-3/FC) with production concerns (parallelism, observability, and tunables) and provides a compact, extensible API for engineers to integrate and optimize.
Explain constraint propagation techniques used to accelerate CSPs and backtracking solvers: forward checking, arc consistency (AC-3), maintaining arc consistency (MAC), and MRV (minimum remaining values). For each technique describe algorithmic steps, how it prunes domains, and how to integrate it into a recursive backtracking solver. Illustrate with Sudoku or graph coloring.
Sample Answer
Forward checking, AC-3, MAC and MRV are complementary techniques that reduce search in backtracking CSP solvers by pruning inconsistent values early. Below I describe each: algorithmic steps, how domains get pruned, and how to integrate into a recursive backtracker (illustrated with Sudoku / graph coloring).
Forward Checking
- Steps: when you assign variable X = v, for every unassigned neighbor Y (sharing a constraint), remove values from Y’s domain that conflict with v. If any domain becomes empty, backtrack.
- Pruning: only removes values directly inconsistent with the most recent assignment (one-step lookahead).
- Integration: in backtrack(assignments):
- choose variable (possibly MRV)
- for each value in domain:
- push a copy/list of pruned (for restoration)
- perform forward check and prune neighbor domains
- recurse if no domain emptied
- undo prunes on return
- Example: Sudoku — assign a digit to a cell; remove that digit from peers’ domains.
Arc Consistency (AC-3)
- Steps: Maintain a queue of arcs (Xi, Xj). For an arc (Xi,Xj), revise Xi: remove values in Xi’s domain that have no compatible value in Xj’s domain. If Xi’s domain shrinks, enqueue all arcs (Xk,Xi) for neighbors Xk.
- Pruning: enforces consistency across arcs until fixed point; can remove values that forward checking misses because it propagates beyond immediate neighbors.
- Integration: apply AC-3 as preprocessing to reduce domains; also can be invoked after each assignment (but running full AC-3 every time can be costlier).
Maintaining Arc Consistency (MAC)
- Steps: MAC applies AC-3 (or similar AC algorithm) incrementally after each assignment, using current domains and only relevant arcs.
- Pruning: strongest among these — enforces global arc consistency after each assignment, often dramatically reducing future branching.
- Integration: in backtrack, after choosing value for X, run MAC (AC-3 limited to arcs affected). If any domain becomes empty, backtrack. Restore domains on backtrack (record pruned values).
Minimum Remaining Values (MRV) heuristic
- Steps: select next variable with smallest domain (fewest legal values). Break ties with degree heuristic (most constraints).
- Pruning: MRV itself doesn’t prune domains but drastically reduces branching factor and exposes failures earlier, improving the effectiveness of forward checking / MAC.
- Integration: use MRV to pick variable at each recursion level.
Putting it together (workflow)
- Preprocess: run AC-3 once to trim domains.
- In backtrack:
- pick var with MRV
- for each value (optionally order by least-constraining-value):
- assign, record prunes
- run forward checking or MAC (prefer MAC when cost justified)
- recurse
- undo prunes and unassign
Trade-offs
- Forward checking is cheap per assignment but weaker.
- AC-3 as preprocessing is cheap and helpful; full MAC is strongest but more CPU per node.
- MRV multiplies benefits by failing early.
Practical tip: For Sudoku, AC-3 + MRV + least-constraining-value with backtracking solves puzzles quickly. For graph coloring, MAC+MRV is effective when colors/domains are small.
That is every published Optimization and Operations Research Methods question for Software Engineer so far. Browse the other topics in this category, or practice this one interactively.