Loading learning content...
Imagine you're scheduling a conference with hundreds of talks, dozens of rooms, and thousands of constraints: speaker availability, room capacity, equipment requirements, attendee conflicts, and sponsor preferences. Each decision affects others in complex ways. A naive approach would generate all possible schedules and filter valid ones—but with 200 talks and 20 rooms, you'd face 20²⁰⁰ possibilities, more than atoms in the observable universe.
This is not an abstract academic exercise. It's exactly the kind of problem that software engineers face daily: task scheduling, resource allocation, configuration management, game AI, and countless optimization challenges. What these problems share is a rich structure of constraints—rules that valid solutions must satisfy.
Constraint Satisfaction Problems (CSPs) provide a powerful framework for modeling these challenges. More than just a theoretical construct, CSPs give us a language to describe combinatorial problems precisely, and a toolkit of algorithms that exploit problem structure to find solutions efficiently. The connection to backtracking is deep: CSPs formalize what backtracking does intuitively, and open the door to systematic optimizations that transform intractable problems into solvable ones.
By the end of this page, you will understand the formal structure of constraint satisfaction problems, how to model real-world challenges as CSPs, the deep relationship between CSPs and backtracking, and why this framework enables powerful optimization techniques that we'll explore in subsequent pages.
A Constraint Satisfaction Problem (CSP) is a mathematical framework for describing problems where we must assign values to variables under certain restrictions. Formally, a CSP consists of three components:
1. Variables (X): A finite set of variables X = {X₁, X₂, ..., Xₙ}, each representing a decision to be made.
2. Domains (D): For each variable Xᵢ, a domain Dᵢ specifies the possible values that variable can take. The domain can be finite (like {1, 2, 3, 4}) or, in more advanced settings, infinite or continuous.
3. Constraints (C): A set of constraints, where each constraint specifies which combinations of values for a subset of variables are allowed. Constraints encode the "rules" of the problem.
A solution to a CSP is an assignment of values to all variables such that every constraint is satisfied. Many problems ask for:
CSPs separate the what from the how. You declare your variables, domains, and constraints—describing what makes a solution valid—and let algorithms figure out how to find solutions. This declarative approach is far more powerful than writing imperative search code, because the same CSP algorithms work across vastly different problem domains.
Constraint Types:
Constraints come in many forms, each with different properties that affect solving strategies:
Unary Constraints: Restrict a single variable's values. Example: "Variable X₁ cannot be 5" reduces X₁'s domain.
Binary Constraints: Restrict pairs of variables. Example: "X₁ ≠ X₂" (different values) or "X₁ < X₂" (ordering). Binary constraints are the most common and well-studied.
Higher-Order Constraints (n-ary): Restrict three or more variables simultaneously. Example: "All of X₁, X₂, X₃ must be different" (an all-different constraint).
Global Constraints: Capture complex patterns involving many variables. Examples include AllDifferent (all variables take distinct values), Sum (variables sum to a target), and Cardinality (counting occurrences). Global constraints are crucial for practical CSP solving because they enable specialized propagation algorithms.
| Constraint Type | Variables Involved | Example | Typical Usage |
|---|---|---|---|
| Unary | 1 | X₁ ≠ 3 | Domain restrictions, initial filtering |
| Binary (Equality) | 2 | X₁ = X₂ | Variable linking, symmetry |
| Binary (Inequality) | 2 | X₁ ≠ X₂ | Graph coloring, scheduling conflicts |
| Binary (Ordering) | 2 | X₁ < X₂ | Precedence, sequencing |
| AllDifferent | n | All variables distinct | Sudoku, Latin squares, assignment |
| Sum/Linear | n | X₁ + X₂ + X₃ = 10 | Resource allocation, knapsack |
| Element | 3+ | Y = Array[X] | Array indexing, lookups |
| Table/Extensional | any | Allowed tuples listed | Arbitrary relations |
Every CSP has an underlying structure that can be visualized as a constraint graph (for binary constraints) or a constraint hypergraph (for higher-order constraints). Understanding this structure is essential for choosing effective solving strategies.
For binary CSPs:
Example: Map Coloring
Consider coloring a map of Australia with three colors (Red, Green, Blue) such that no adjacent regions share a color:
The constraint graph mirrors the geographical adjacency graph—each edge represents a shared border.
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// CSP Representation for Map Coloringinterface CSP<T> { variables: string[]; domains: Map<string, T[]>; constraints: Constraint<T>[];} interface Constraint<T> { variables: string[]; // Variables involved in this constraint isSatisfied: (assignment: Map<string, T>) => boolean;} // Create the Australia map coloring CSPfunction createAustraliaCSP(): CSP<string> { const variables = ['WA', 'NT', 'SA', 'Q', 'NSW', 'V', 'T']; const colors = ['R', 'G', 'B']; // All regions can be any color initially const domains = new Map<string, string[]>(); for (const v of variables) { domains.set(v, [...colors]); } // Adjacent regions must have different colors const adjacencies: [string, string][] = [ ['WA', 'NT'], ['WA', 'SA'], ['NT', 'SA'], ['NT', 'Q'], ['SA', 'Q'], ['SA', 'NSW'], ['SA', 'V'], ['Q', 'NSW'], ['NSW', 'V'] ]; const constraints: Constraint<string>[] = adjacencies.map(([v1, v2]) => ({ variables: [v1, v2], isSatisfied: (assignment: Map<string, string>) => { const val1 = assignment.get(v1); const val2 = assignment.get(v2); // Constraint is satisfied if either is unassigned, or they differ if (val1 === undefined || val2 === undefined) return true; return val1 !== val2; } })); return { variables, domains, constraints };}Why Graph Structure Matters:
The constraint graph reveals crucial information about problem difficulty and solution strategies:
Connectivity: Disconnected components can be solved independently and solutions combined.
Density: Sparse graphs (few constraints) have more solutions; dense graphs are more constrained and often harder.
Tree-width: If the constraint graph is a tree (acyclic), the CSP can be solved in polynomial time! Graph-based algorithms exploit low tree-width for efficiency.
Cliques: Highly connected subgraphs indicate tightly coupled variables that must be carefully coordinated.
Understanding your problem's constraint structure is the first step to choosing effective algorithms. A problem that looks intractable might have exploitable structure that makes it easy.
One of the remarkable properties of the CSP framework is its universality. An enormous variety of problems can be naturally expressed as CSPs, from puzzles to scheduling to configuration. This means algorithms developed for CSPs apply broadly—a single solver can tackle thousands of different problem types.
Let's see how classic problems map to the CSP framework:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// N-Queens as a CSPfunction createNQueensCSP(n: number): CSP<number> { // Variables: Q1, Q2, ..., Qn (column position for queen in each row) const variables = Array.from({ length: n }, (_, i) => `Q${i + 1}`); // Domains: each queen can be in any column 1 to n const domains = new Map<string, number[]>(); for (const v of variables) { domains.set(v, Array.from({ length: n }, (_, i) => i + 1)); } const constraints: Constraint<number>[] = []; // For each pair of queens for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { const v1 = variables[i]; const v2 = variables[j]; const rowDiff = j - i; // Distance in rows constraints.push({ variables: [v1, v2], isSatisfied: (assignment: Map<string, number>) => { const col1 = assignment.get(v1); const col2 = assignment.get(v2); if (col1 === undefined || col2 === undefined) return true; // Same column check if (col1 === col2) return false; // Diagonal check: |col difference| must not equal row difference if (Math.abs(col1 - col2) === rowDiff) return false; return true; } }); } } return { variables, domains, constraints };} // For 8-Queens:// - 8 variables (one per row)// - Each domain has 8 values (columns 1-8)// - 28 pairwise constraints (8 choose 2 = 28)The same problem can often be modeled as a CSP in multiple ways. Better models lead to faster solutions. In N-Queens, using one variable per row (with column as value) is superior to using one variable per cell (with {queen/empty} values) because it implicitly enforces the one-queen-per-row constraint and has a much smaller search space.
Backtracking algorithms and CSPs are two sides of the same coin. Every backtracking algorithm can be viewed as a CSP solver, and every CSP naturally leads to a backtracking solution strategy. Understanding this connection clarifies both concepts and reveals optimization opportunities.
Standard Backtracking as CSP Solving:
The backtracking template we've studied—choose, explore, unchoose—maps directly to CSP operations:
This is exactly what backtracking does, just expressed in CSP vocabulary.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
// Generic backtracking solver for any CSPfunction backtrackingSearch<T>(csp: CSP<T>): Map<string, T> | null { return backtrack(new Map(), csp);} function backtrack<T>( assignment: Map<string, T>, csp: CSP<T>): Map<string, T> | null { // Base case: all variables assigned = solution found if (assignment.size === csp.variables.length) { return new Map(assignment); // Return copy of solution } // SELECT: Choose an unassigned variable const variable = selectUnassignedVariable(assignment, csp); // ORDER: Get domain values (possibly ordered by heuristics) const values = orderDomainValues(variable, assignment, csp); for (const value of values) { // CHOOSE: Assign value to variable assignment.set(variable, value); // CHECK: Verify constraints if (isConsistent(assignment, csp)) { // EXPLORE: Recurse on remaining problem const result = backtrack(assignment, csp); if (result !== null) { return result; // Solution found! } } // UNCHOOSE: Remove assignment for next attempt assignment.delete(variable); } // BACKTRACK: No value worked, return failure return null;} function selectUnassignedVariable<T>( assignment: Map<string, T>, csp: CSP<T>): string { // Simple: return first unassigned variable // Better: use MRV (minimum remaining values) heuristic for (const v of csp.variables) { if (!assignment.has(v)) return v; } throw new Error("No unassigned variable");} function orderDomainValues<T>( variable: string, assignment: Map<string, T>, csp: CSP<T>): T[] { // Simple: return domain in order // Better: use LCV (least constraining value) heuristic return csp.domains.get(variable) || [];} function isConsistent<T>( assignment: Map<string, T>, csp: CSP<T>): boolean { // Check all constraints involving assigned variables for (const constraint of csp.constraints) { // Only check constraints where all variables are assigned const allAssigned = constraint.variables.every(v => assignment.has(v)); if (allAssigned && !constraint.isSatisfied(assignment)) { return false; } } return true;}The Search Tree Perspective:
Every CSP defines a search tree that backtracking explores:
The depth of this tree equals the number of variables. The branching factor at each level equals the domain size of the chosen variable. The total search space without pruning is:
$$\text{Search space} = \prod_{i=1}^{n} |D_i|$$
For n variables each with domain size d, this gives d^n nodes—exponential in the number of variables. The goal of all CSP optimization techniques is to prune this tree, avoiding exploration of branches that cannot contain solutions.
CSPs are generally NP-complete (reducible from SAT). There's no polynomial-time algorithm that solves all CSPs. But intelligent pruning, constraint propagation, and heuristics can make many practical problems tractable. The difference between naive backtracking and optimized CSP solving can be the difference between years of computation and milliseconds.
While constraints define what is valid, ordering heuristics determine how we explore the search space. The order in which we assign variables and try values dramatically affects performance—often by orders of magnitude. Good heuristics make smart choices that either find solutions quickly or reveal failures early.
Variable Ordering: Which Variable to Assign Next?
Value Ordering: Which Value to Try First?
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
// Minimum Remaining Values (MRV) Variable Orderingfunction selectByMRV<T>( assignment: Map<string, T>, csp: CSP<T>, domains: Map<string, T[]> // Current reduced domains): string { let bestVariable: string | null = null; let minRemainingValues = Infinity; for (const variable of csp.variables) { if (assignment.has(variable)) continue; const remaining = domains.get(variable)?.length ?? 0; // Choose variable with fewest remaining values if (remaining < minRemainingValues) { minRemainingValues = remaining; bestVariable = variable; } } if (bestVariable === null) { throw new Error("No unassigned variable"); } return bestVariable;} // Least Constraining Value (LCV) Value Orderingfunction orderByLCV<T>( variable: string, assignment: Map<string, T>, csp: CSP<T>, domains: Map<string, T[]>): T[] { const values = domains.get(variable) || []; // Count how many values each choice rules out for neighbors const valueScores: Array<{ value: T; ruledOut: number }> = []; for (const value of values) { let ruledOut = 0; // Find neighbors (variables sharing a constraint with this one) for (const constraint of csp.constraints) { if (!constraint.variables.includes(variable)) continue; for (const neighbor of constraint.variables) { if (neighbor === variable || assignment.has(neighbor)) continue; // Count how many of neighbor's values would be ruled out const neighborDomain = domains.get(neighbor) || []; for (const nv of neighborDomain) { const testAssignment = new Map(assignment); testAssignment.set(variable, value); testAssignment.set(neighbor, nv); if (!constraint.isSatisfied(testAssignment)) { ruledOut++; } } } } valueScores.push({ value, ruledOut }); } // Sort by least constraining (fewest ruled out) first valueScores.sort((a, b) => a.ruledOut - b.ruledOut); return valueScores.map(vs => vs.value);}Notice that variable and value ordering use opposite philosophies. For variables, we're pessimistic: choose the most constrained variable to fail early if the path is doomed. For values, we're optimistic: choose the least constraining value to maximize our chances of success. This combination—fail-fast variable selection with keep-options-open value selection—is remarkably effective across diverse problems.
The most powerful optimization techniques for CSPs go beyond just ordering—they actively reduce domains based on constraint analysis. This is called constraint propagation or consistency enforcement.
The Key Insight:
When we make an assignment X₁ = v, other constraints become more restrictive. Values in other variables' domains that are now impossible can be removed before we try them. If a domain becomes empty, we know immediately that this path leads nowhere—no need to continue exploring.
Types of Consistency:
Node Consistency: Every value in a variable's domain satisfies all unary constraints on that variable. This is easy to enforce upfront by filtering domains.
Arc Consistency: For every pair of constrained variables (X, Y), every value in X's domain has at least one compatible value in Y's domain. If value x ∈ Dₓ has no compatible value in Dᵧ, we can remove x from Dₓ.
Path Consistency, k-Consistency, and beyond: Higher levels of consistency involve more variables simultaneously, offering more pruning but at higher computational cost.
Example: Sudoku with Arc Consistency
Consider a Sudoku where a cell has candidates {1, 2, 5}. If through propagation we discover:
We can immediately reduce this cell's domain to {2}. This single value then propagates to all cells in its row, column, and box—potentially solving large portions of the puzzle without any backtracking.
Propagation transforms Sudoku from a problem requiring extensive search into one often solvable by propagation alone. Most newspaper Sudokus are designed to be solvable with arc consistency and naked/hidden singles—no actual backtracking needed.
In the following pages, we'll explore pruning techniques, forward checking, and arc consistency in depth. These concepts transform CSP solving from exponential enumeration into intelligent constraint-driven search.
The CSP framework isn't just academic theory—it powers countless real-world systems. Understanding CSPs opens doors to solving practical problems across industries.
| Domain | Problem | Variables | Constraints |
|---|---|---|---|
| Transportation | Vehicle routing | Route segments | Capacity, time windows, driver hours |
| Manufacturing | Production scheduling | Task start times | Precedence, machine capacity, deadlines |
| Telecommunications | Frequency allocation | Channel assignments | Interference avoidance, coverage |
| Education | Exam timetabling | Exam slots | Room capacity, student conflicts, spreads |
| Software | Configuration management | Component versions | Compatibility, dependencies, features |
| Sports | Tournament scheduling | Match times/venues | Team availability, TV slots, travel |
| Retail | Shelf space allocation | Product placements | Visibility, affinity, capacity |
| Healthcare | Nurse rostering | Shift assignments | Coverage, preferences, regulations |
Companies like Google OR-Tools, IBM CPLEX, and Gurobi provide industrial-strength CSP/constraint programming solvers used by Fortune 500 companies. A single optimization improvement in airline crew scheduling or supply chain logistics can save millions of dollars annually. CSP is not just theory—it's a multi-billion dollar applied field.
We've established the theoretical and practical foundation for understanding constraint satisfaction problems. Let's consolidate the key insights:
What's Next:
Now that we understand the CSP framework, we're ready to explore the techniques that make CSP solving practical at scale. The next page dives deep into pruning techniques—strategies that eliminate entire branches of the search tree before they're explored, transforming exponential problems into tractable ones.
You now understand the constraint satisfaction framework—variables, domains, constraints, and their connection to backtracking search. This foundation prepares you to master the powerful optimization techniques ahead: pruning, forward checking, and arc consistency. The journey from exponential to practical begins here.