DEV Community

Cover image for Building a Constraint Solver: Teaching a Computer How to Reason About Impossible Things
Derek Mwale
Derek Mwale

Posted on

Building a Constraint Solver: Teaching a Computer How to Reason About Impossible Things

There is a particular kind of programming problem that feels almost philosophical.

You don't know the answer.

You don't even know exactly what the answer should look like.

You only know the rules.

A teacher needs to schedule classes without putting two classes in the same room.

A Sudoku board needs every row, column, and region to contain unique numbers.

A delivery system needs to assign vehicles without exceeding capacity.

A university needs to assign students to courses while respecting prerequisites and room limits.

A compiler needs to satisfy constraints while generating code.

A cloud scheduler needs to place workloads onto machines without exceeding CPU and memory.

A configuration system needs to find a combination of versions that are mutually compatible.

In all of these problems, you aren't really asking:

"What is the answer?"

You're asking:

"Can I find an assignment of values that satisfies every rule?"

That is the fundamental idea behind a constraint solver.

And once you understand how to build one, you begin to see something fascinating.

A constraint solver is a tiny machine for reasoning.

You give it variables.

You give it possible values.

You give it rules.

And the machine searches the space of possibilities until it discovers an assignment that doesn't violate those rules.

The architecture looks deceptively simple:

                 CONSTRAINT SOLVER

                    Variables
                        |
                        v
                 Possible Values
                        |
                        v
                   Constraints
                        |
                        v
                 +-------------+
                 |   Search    |
                 +-------------+
                        |
              +---------+---------+
              |                   |
           Violation            Success
              |                   |
              v                   v
          Backtrack             Solution
              |
              v
        Try another value
Enter fullscreen mode Exit fullscreen mode

But inside that simple diagram is one of the most beautiful ideas in computer science:

Instead of explicitly knowing the answer, we define the space of valid answers and systematically eliminate everything impossible.

Let's build one.


1. What Exactly Is a Constraint Solver?

A constraint satisfaction problem, commonly called a CSP, consists of three fundamental components:

CSP = Variables + Domains + Constraints
Enter fullscreen mode Exit fullscreen mode

Variables

Variables are the things we need to determine.

For example:

X
Y
Z
Enter fullscreen mode Exit fullscreen mode

Domains

A domain is the set of values a variable can take.

For example:

X ∈ {1, 2, 3}
Y ∈ {1, 2, 3}
Z ∈ {1, 2, 3}
Enter fullscreen mode Exit fullscreen mode

Constraints

Constraints define which combinations are allowed.

For example:

X != Y
Y != Z
X != Z
Enter fullscreen mode Exit fullscreen mode

The solver's job is to find:

X = ?
Y = ?
Z = ?
Enter fullscreen mode Exit fullscreen mode

such that every constraint is satisfied.

One valid solution is:

X = 1
Y = 2
Z = 3
Enter fullscreen mode Exit fullscreen mode

The important thing is that we never explicitly told the computer:

X = 1
Y = 2
Z = 3
Enter fullscreen mode Exit fullscreen mode

We gave it the rules.

The solver discovered the assignment.

That's the entire game.


2. A Constraint Solver Is Basically Controlled Brute Force

Let's start with the simplest possible approach.

Suppose:

X ∈ {1, 2, 3}
Y ∈ {1, 2, 3}
Enter fullscreen mode Exit fullscreen mode

and:

X != Y
Enter fullscreen mode Exit fullscreen mode

There are nine possible assignments:

(1,1)
(1,2)
(1,3)
(2,1)
(2,2)
(2,3)
(3,1)
(3,2)
(3,3)
Enter fullscreen mode Exit fullscreen mode

The solver can simply try them.

             Search Space

             X = 1
            /     \
         Y=1      Y=2
         ❌        ✓

             X = 2
            /     \
         Y=1      Y=2
         ✓         ❌

             X = 3
            /     \
         Y=1      Y=2
         ✓         ✓
Enter fullscreen mode Exit fullscreen mode

This is brute force.

And brute force is actually an excellent place to start.

Why?

Because a constraint solver isn't magic.

It is fundamentally a search algorithm.

The intelligence comes from learning how to avoid searching parts of the space that cannot possibly produce a solution.

That is where things become interesting.


3. The Search Tree

Imagine three variables:

A
B
C
Enter fullscreen mode Exit fullscreen mode

Each has:

{1, 2, 3}
Enter fullscreen mode Exit fullscreen mode

The complete search space contains:

3 × 3 × 3 = 27
Enter fullscreen mode Exit fullscreen mode

possibilities.

The search tree looks like:

                         Start
                           |
          +----------------+----------------+
          |                |                |
        A = 1            A = 2            A = 3
       / | \             / | \             / | \
     B=1 B=2 B=3       B=1 B=2 B=3       B=1 B=2 B=3
      |    |    |        |    |    |        |    |    |
      C    C    C        C    C    C        C    C    C
Enter fullscreen mode Exit fullscreen mode

Even with only three variables, the tree grows quickly.

If there are:

10 variables
10 values each
Enter fullscreen mode Exit fullscreen mode

then the naive search space is:

10^10
Enter fullscreen mode Exit fullscreen mode

That's:

10,000,000,000
Enter fullscreen mode Exit fullscreen mode

possibilities.

And that's a tiny CSP.

This is why constraint solving is fundamentally an exercise in search-space reduction.


4. Backtracking

The first serious algorithm we need is backtracking.

The idea is simple.

Assign a value.

Check whether the partial assignment is still valid.

If it is, continue.

If it isn't, undo the assignment and try another value.

Choose value
     |
     v
Does it violate a constraint?
     |
   +---+---+
   |       |
  Yes      No
   |       |
Backtrack  Continue
Enter fullscreen mode Exit fullscreen mode

This is much smarter than blindly generating complete assignments.

Consider:

A != B
B != C
A != C
Enter fullscreen mode Exit fullscreen mode

We assign:

A = 1
Enter fullscreen mode Exit fullscreen mode

Then:

B = 1
Enter fullscreen mode Exit fullscreen mode

Immediately:

A == B
Enter fullscreen mode Exit fullscreen mode

which violates:

A != B
Enter fullscreen mode Exit fullscreen mode

There is no reason to assign C.

The entire subtree beneath:

A = 1
B = 1
Enter fullscreen mode Exit fullscreen mode

can be discarded.

This is called pruning.

And pruning is where constraint solvers become powerful.


5. Building the First Solver in Python

Let's make this concrete.

We'll build a small generic solver.

First, variables:

variables = ["A", "B", "C"]
Enter fullscreen mode Exit fullscreen mode

Domains:

domains = {
    "A": [1, 2, 3],
    "B": [1, 2, 3],
    "C": [1, 2, 3],
}
Enter fullscreen mode Exit fullscreen mode

Now our constraint:

def all_different(assignment):
    values = list(assignment.values())
    return len(values) == len(set(values))
Enter fullscreen mode Exit fullscreen mode

And our solver:

def solve(variables, domains, assignment=None):
    if assignment is None:
        assignment = {}

    if len(assignment) == len(variables):
        return assignment.copy()

    variable = next(
        v for v in variables
        if v not in assignment
    )

    for value in domains[variable]:
        assignment[variable] = value

        if all_different(assignment):
            result = solve(
                variables,
                domains,
                assignment
            )

            if result is not None:
                return result

        del assignment[variable]

    return None
Enter fullscreen mode Exit fullscreen mode

We can run:

solution = solve(
    variables,
    domains
)

print(solution)
Enter fullscreen mode Exit fullscreen mode

And get something like:

{'A': 1, 'B': 2, 'C': 3}
Enter fullscreen mode Exit fullscreen mode

We have built a constraint solver.

It's not sophisticated.

But conceptually, this is the foundation of many much more advanced systems.


6. The Recursive Mental Model

The most important part of that implementation isn't the Python syntax.

It's this:

for value in domains[variable]:
    assignment[variable] = value

    if constraints_satisfied():
        result = solve(...)

        if result:
            return result

    del assignment[variable]
Enter fullscreen mode Exit fullscreen mode

This is the heart of backtracking.

Think of recursion as moving deeper into the search tree.

solve()
   |
   +--- A=1
   |      |
   |      +--- B=1 ❌
   |      |
   |      +--- B=2
   |             |
   |             +--- C=1 ❌
   |             +--- C=2 ❌
   |             +--- C=3 ✓
   |
   +--- A=2
   |
   +--- A=3
Enter fullscreen mode Exit fullscreen mode

When we hit a violation, recursion returns.

That is the "backtrack."

We go back up the tree and try something else.


7. Why Naive Backtracking Isn't Enough

Our solver works.

But it can still become ridiculously slow.

Imagine:

100 variables
10 values each
Enter fullscreen mode Exit fullscreen mode

The theoretical search space is:

10^100
Enter fullscreen mode Exit fullscreen mode

No computer is casually iterating through that.

The entire field of constraint solving is essentially about avoiding that explosion.

We need to make the search smarter.

The first major improvement is:

Choose the variable intelligently.


8. Minimum Remaining Values

Suppose we have:

A = {1, 2, 3, 4, 5}
B = {2}
C = {1, 2, 3}
D = {1, 2, 3, 4}
Enter fullscreen mode Exit fullscreen mode

Which variable should we assign first?

A naive solver might choose:

A
Enter fullscreen mode Exit fullscreen mode

But that's a bad choice.

B has only one possible value.

So choose:

B
Enter fullscreen mode Exit fullscreen mode

This strategy is called Minimum Remaining Values, or MRV.

The idea:

Choose the variable with the smallest remaining domain.

A → 5 possibilities
B → 1 possibility
C → 3 possibilities
D → 4 possibilities

Choose B.
Enter fullscreen mode Exit fullscreen mode

Why?

Because variables with tiny domains are the most constrained.

If B cannot be assigned, we'd rather discover that immediately.

This is an example of the fail-first principle:

Try to discover failure as early as possible.


9. Implementing MRV

We can replace:

variable = next(
    v for v in variables
    if v not in assignment
)
Enter fullscreen mode Exit fullscreen mode

with:

unassigned = [
    v for v in variables
    if v not in assignment
]

variable = min(
    unassigned,
    key=lambda v: len(domains[v])
)
Enter fullscreen mode Exit fullscreen mode

Now our solver makes a more intelligent decision.

But we can go further.


10. Forward Checking

Suppose:

A != B
Enter fullscreen mode Exit fullscreen mode

and:

A ∈ {1, 2, 3}
B ∈ {1, 2, 3}
Enter fullscreen mode Exit fullscreen mode

We assign:

A = 1
Enter fullscreen mode Exit fullscreen mode

We already know:

B != 1
Enter fullscreen mode Exit fullscreen mode

So why leave:

B = {1, 2, 3}
Enter fullscreen mode Exit fullscreen mode

?

We can immediately reduce it:

B = {2, 3}
Enter fullscreen mode Exit fullscreen mode

This is called forward checking.

The assignment pushes information forward through the problem.

A = 1
 |
 +----> B cannot be 1
 |
 +----> C cannot be 1
 |
 +----> D cannot be 1
Enter fullscreen mode Exit fullscreen mode

Now the future search has fewer possibilities.

This is one of the fundamental patterns of constraint programming:

Every decision should eliminate impossible futures.


11. Constraint Propagation

Forward checking is one form of constraint propagation.

Imagine:

A < B
B < C
Enter fullscreen mode Exit fullscreen mode

with:

A = {1, 2, 3}
B = {1, 2, 3}
C = {1, 2, 3}
Enter fullscreen mode Exit fullscreen mode

The constraints imply relationships between the domains.

For example, if:

A = 3
Enter fullscreen mode Exit fullscreen mode

then:

B > 3
Enter fullscreen mode Exit fullscreen mode

which is impossible.

Therefore:

A = 3
Enter fullscreen mode Exit fullscreen mode

can be eliminated before we ever assign it.

Propagation tries to derive these consequences automatically.

Conceptually:

Initial domains
      |
      v
Apply constraints
      |
      v
Reduce domains
      |
      v
Apply constraints again
      |
      v
Reduce domains
      |
      v
Fixed point
Enter fullscreen mode Exit fullscreen mode

This process can dramatically shrink the search space.


12. Arc Consistency

One famous idea in CSPs is arc consistency.

Consider:

X < Y
Enter fullscreen mode Exit fullscreen mode

with:

X = {1, 2, 3}
Y = {1, 2}
Enter fullscreen mode Exit fullscreen mode

Can:

X = 3
Enter fullscreen mode Exit fullscreen mode

possibly work?

No.

There is no value in Y greater than 3.

So remove:

3
Enter fullscreen mode Exit fullscreen mode

from X.

Now:

X = {1, 2}
Y = {1, 2}
Enter fullscreen mode Exit fullscreen mode

This seems small.

But imagine hundreds of variables with interconnected constraints.

One domain reduction can trigger another.

X shrinks
   |
   v
Y shrinks
   |
   v
Z shrinks
   |
   v
W becomes impossible
Enter fullscreen mode Exit fullscreen mode

A solver can propagate these consequences before committing to a deep search.


13. The Constraint Graph

A useful way to visualize a CSP is as a graph.

Suppose:

A != B
B != C
C != D
A != D
Enter fullscreen mode Exit fullscreen mode

We can represent it as:

       A -------- B
       |           |
       |           |
       D -------- C
Enter fullscreen mode Exit fullscreen mode

Each node is a variable.

Each edge represents a relationship.

This graph gives us another powerful heuristic.

When choosing the next variable, we can consider not only how constrained its domain is, but also how many other variables depend on it.

That's the degree heuristic.

A highly connected variable can influence a large portion of the search.

So we may prefer it.


14. Variable Ordering Is a Superpower

Consider two strategies.

Strategy A

A
B
C
D
E
F
Enter fullscreen mode Exit fullscreen mode

Strategy B

Most constrained
        ↓
Most connected
        ↓
Most likely to fail
Enter fullscreen mode Exit fullscreen mode

Both are correct.

But their performance can be radically different.

This is an important lesson in algorithms:

The order in which you explore a search space can matter more than the search algorithm itself.

Two solvers can use the same backtracking algorithm and have dramatically different runtimes simply because they make different decisions about what to explore first.


15. Value Ordering

Choosing the variable is only half the problem.

We also need to decide which value to try first.

Suppose:

X = {1, 2, 3}
Enter fullscreen mode Exit fullscreen mode

Which should we try?

One strategy is the least-constraining value.

Choose the value that eliminates the fewest possibilities from neighboring variables.

Imagine:

X = 1
Enter fullscreen mode Exit fullscreen mode

eliminates 5 possibilities.

While:

X = 2
Enter fullscreen mode Exit fullscreen mode

eliminates only 1.

Try:

X = 2
Enter fullscreen mode Exit fullscreen mode

first.

The intuition is:

Keep the future as flexible as possible.

Now we have two complementary ideas:

MRV:
Choose the most constrained variable.

LCV:
Choose the least constraining value.
Enter fullscreen mode Exit fullscreen mode

Together they can make backtracking dramatically smarter.


16. A Better Solver Architecture

Our solver is starting to become a real engine.

Conceptually:

                  Solver
                     |
          +----------+----------+
          |                     |
       Choose Variable      Choose Value
          |                     |
         MRV                    LCV
          |                     |
          +----------+----------+
                     |
                     v
                 Assignment
                     |
                     v
              Propagate Rules
                     |
              +------+------+
              |             |
            Valid         Conflict
              |             |
              v             v
           Continue      Backtrack
Enter fullscreen mode Exit fullscreen mode

This is the architecture behind the basic idea.


17. Let's Build a More Generic Constraint API

Hardcoding all_different() isn't enough.

We want a reusable solver.

Let's create a constraint interface:

class Constraint:
    def __init__(self, variables):
        self.variables = variables

    def satisfied(self, assignment):
        raise NotImplementedError
Enter fullscreen mode Exit fullscreen mode

Then:

class NotEqualConstraint(Constraint):
    def __init__(self, a, b):
        super().__init__([a, b])
        self.a = a
        self.b = b

    def satisfied(self, assignment):
        if self.a not in assignment:
            return True

        if self.b not in assignment:
            return True

        return assignment[self.a] != assignment[self.b]
Enter fullscreen mode Exit fullscreen mode

Now we can define:

constraints = [
    NotEqualConstraint("A", "B"),
    NotEqualConstraint("B", "C"),
]
Enter fullscreen mode Exit fullscreen mode

The solver doesn't need to know what the constraints mean.

That's important.

The solver handles:

search
assignment
backtracking
Enter fullscreen mode Exit fullscreen mode

while constraints handle:

domain-specific logic
Enter fullscreen mode Exit fullscreen mode

This separation makes the system extensible.


18. The Solver Core

We can now write something like:

def consistent(variable, assignment, constraints):
    for constraint in constraints:
        if variable in constraint.variables:
            if not constraint.satisfied(assignment):
                return False

    return True
Enter fullscreen mode Exit fullscreen mode

And:

def solve(
    variables,
    domains,
    constraints,
    assignment=None
):
    if assignment is None:
        assignment = {}

    if len(assignment) == len(variables):
        return assignment.copy()

    unassigned = [
        v for v in variables
        if v not in assignment
    ]

    variable = min(
        unassigned,
        key=lambda v: len(domains[v])
    )

    for value in domains[variable]:
        assignment[variable] = value

        if consistent(
            variable,
            assignment,
            constraints
        ):
            result = solve(
                variables,
                domains,
                constraints,
                assignment
            )

            if result is not None:
                return result

        del assignment[variable]

    return None
Enter fullscreen mode Exit fullscreen mode

This is still a small solver.

But notice what happened.

We separated:

Problem definition
Enter fullscreen mode Exit fullscreen mode

from:

Search engine
Enter fullscreen mode Exit fullscreen mode

That is the beginning of a real constraint programming system.


19. Sudoku Is a Constraint Problem

Now let's apply the idea to something more interesting.

Sudoku.

A Sudoku board has:

81 variables
Enter fullscreen mode Exit fullscreen mode

Each cell is a variable.

Each cell has a domain:

{1,2,3,4,5,6,7,8,9}
Enter fullscreen mode Exit fullscreen mode

Then we have constraints.

Every row:

All different
Enter fullscreen mode Exit fullscreen mode

Every column:

All different
Enter fullscreen mode Exit fullscreen mode

Every 3×3 region:

All different
Enter fullscreen mode Exit fullscreen mode

The problem becomes:

81 variables
+
81 domains
+
row constraints
+
column constraints
+
region constraints
Enter fullscreen mode Exit fullscreen mode

The solver doesn't need to know that this is "Sudoku."

It only needs to know the constraints.

That's a beautiful abstraction.


20. Sudoku as a Graph

Imagine every cell as a node.

Two cells are connected if they cannot contain the same number.

       Cell A -------- Cell B
         |               |
         |               |
         |               |
       Cell C -------- Cell D
Enter fullscreen mode Exit fullscreen mode

Each cell has roughly 20 peers in a standard Sudoku board.

When:

A = 5
Enter fullscreen mode Exit fullscreen mode

the solver can remove:

5
Enter fullscreen mode Exit fullscreen mode

from every neighboring cell.

If one neighboring cell becomes:

{5}
Enter fullscreen mode Exit fullscreen mode

we've discovered another assignment.

That assignment propagates.

Sudoku is therefore an excellent demonstration of constraint propagation.


21. The Search Explosion

Why can't we just brute-force Sudoku?

Because each empty cell could initially contain nine values.

For 81 cells:

9^81
Enter fullscreen mode Exit fullscreen mode

possibilities.

That's an absurdly large number.

But Sudoku constraints eliminate enormous portions of that space.

The solver isn't really exploring:

9^81
Enter fullscreen mode Exit fullscreen mode

because propagation continually reduces the possibilities.

This is the essence of constraint solving:

Huge theoretical space
        |
        v
Constraint propagation
        |
        v
Smaller search space
        |
        v
Heuristics
        |
        v
Even smaller search
        |
        v
Backtracking
        |
        v
Solution
Enter fullscreen mode Exit fullscreen mode

22. Hard Constraints vs Soft Constraints

So far we've assumed that every constraint must be satisfied.

These are hard constraints.

For example:

room_capacity >= students
Enter fullscreen mode Exit fullscreen mode

must always be true.

But real-world scheduling often contains preferences.

For example:

Professor prefers morning classes.
Enter fullscreen mode Exit fullscreen mode

That's not necessarily mandatory.

This introduces soft constraints.

Now we're no longer simply asking:

"Is there a solution?"

We're asking:

"What is the best solution?"

This changes the problem.

We can define a score:

score(solution)
Enter fullscreen mode Exit fullscreen mode

and search for:

max(score)
Enter fullscreen mode Exit fullscreen mode

or:

min(cost)
Enter fullscreen mode Exit fullscreen mode

Now the solver starts looking like an optimization engine.


23. Constraint Solving and Optimization

Suppose we're scheduling meetings.

Hard constraints:

No overlapping meetings.
Room capacity must be sufficient.
Employees cannot be in two places simultaneously.
Enter fullscreen mode Exit fullscreen mode

Soft constraints:

Prefer morning.
Prefer certain rooms.
Minimize walking distance.
Prefer employee availability.
Enter fullscreen mode Exit fullscreen mode

Now we can represent the objective:

Cost =
    scheduling_penalty
  + room_penalty
  + travel_penalty
  + availability_penalty
Enter fullscreen mode Exit fullscreen mode

The solver attempts to minimize:

Cost
Enter fullscreen mode Exit fullscreen mode

Suddenly our little backtracking algorithm has become a primitive optimization engine.

This is one reason constraint programming is so useful.


24. Constraint Solvers Are Everywhere

Once you understand the pattern, you'll start seeing CSPs everywhere.

Scheduling

People
Rooms
Time slots
Resources
Enter fullscreen mode Exit fullscreen mode

Routing

Vehicles
Locations
Capacity
Time windows
Enter fullscreen mode Exit fullscreen mode

Configuration management

Package A requires B >= 2
Package C conflicts with D
Package E requires F
Enter fullscreen mode Exit fullscreen mode

Cloud infrastructure

CPU
Memory
Availability zones
Affinity
Anti-affinity
Enter fullscreen mode Exit fullscreen mode

Compilers

Registers
Instructions
Dependencies
Scheduling constraints
Enter fullscreen mode Exit fullscreen mode

Network design

Capacity
Latency
Topology
Routing rules
Enter fullscreen mode Exit fullscreen mode

Games

Legal moves
Board state
Resource limits
Victory conditions
Enter fullscreen mode Exit fullscreen mode

Artificial intelligence

Variables
Possible states
Relationships
Objectives
Enter fullscreen mode Exit fullscreen mode

A surprising amount of software can be reframed as:

Find an assignment satisfying these rules.
Enter fullscreen mode Exit fullscreen mode

25. Constraint Propagation as Information Flow

One of the deepest ways to think about a solver is not as a brute-force machine, but as an information propagation system.

Imagine:

A = 5
Enter fullscreen mode Exit fullscreen mode

and:

A != B
Enter fullscreen mode Exit fullscreen mode

Then:

B != 5
Enter fullscreen mode Exit fullscreen mode

Now suppose:

B ∈ {5, 6}
Enter fullscreen mode Exit fullscreen mode

After propagation:

B = {6}
Enter fullscreen mode Exit fullscreen mode

Therefore:

B = 6
Enter fullscreen mode Exit fullscreen mode

Then perhaps:

B != C
Enter fullscreen mode Exit fullscreen mode

which tells us:

C != 6
Enter fullscreen mode Exit fullscreen mode

One fact creates another.

A = 5
   |
   v
B != 5
   |
   v
B = 6
   |
   v
C != 6
   |
   v
C = 7
   |
   v
...
Enter fullscreen mode Exit fullscreen mode

This is almost like a chain reaction.

The solver keeps asking:

"What else becomes impossible because of what I just learned?"

That's constraint propagation.


26. Detecting Failure Early

Suppose a domain becomes empty:

X = {}
Enter fullscreen mode Exit fullscreen mode

This means:

There is no possible value for X under the current partial assignment.

We don't need to search further.

We immediately backtrack.

X domain
{1,2,3}
   |
constraint
   v
{2,3}
   |
constraint
   v
{3}
   |
constraint
   v
{}
   |
   v
CONFLICT
   |
   v
BACKTRACK
Enter fullscreen mode Exit fullscreen mode

An empty domain is one of the cleanest signals a solver can receive.

It means:

This branch of the search tree is mathematically impossible.


27. Conflict-Directed Thinking

As solvers become more sophisticated, they don't merely say:

"Something failed."

They try to understand:

"Why did it fail?"

Suppose:

A = 1
B = 1
Enter fullscreen mode Exit fullscreen mode

and:

A != B
Enter fullscreen mode Exit fullscreen mode

The conflict involves:

A
B
Enter fullscreen mode Exit fullscreen mode

There is no reason to reconsider unrelated variable:

Z
Enter fullscreen mode Exit fullscreen mode

This idea leads toward more advanced techniques such as conflict-directed backtracking and conflict-driven search.

Instead of blindly undoing the most recent assignment, the solver can jump back to the decision responsible for the conflict.

This can dramatically improve performance on difficult problems.


28. Memoization

Sometimes different paths lead to the same state.

Imagine the solver reaches:

A = 1
B = 2
Enter fullscreen mode Exit fullscreen mode

through one path.

Later, another path reaches the exact same relevant state.

If we've already proven:

A = 1, B = 2
Enter fullscreen mode Exit fullscreen mode

cannot produce a solution, why solve it again?

We can cache failed states.

failed_states = set()
Enter fullscreen mode Exit fullscreen mode

Then:

state = tuple(sorted(assignment.items()))

if state in failed_states:
    return None
Enter fullscreen mode Exit fullscreen mode

After failure:

failed_states.add(state)
Enter fullscreen mode Exit fullscreen mode

This is essentially memoization applied to search.

But there is a subtlety.

For large problems, storing entire states can consume enormous memory.

Again, engineering becomes a trade-off.


29. Bitsets Make Constraint Solvers Fast

If domains contain small integers, we can represent them using bits.

For Sudoku:

1 2 3 4 5 6 7 8 9
Enter fullscreen mode Exit fullscreen mode

can be represented by a 9-bit integer.

For example:

000001111
Enter fullscreen mode Exit fullscreen mode

could represent:

{1,2,3,4}
Enter fullscreen mode Exit fullscreen mode

Removing a value becomes a bit operation.

Intersection becomes:

domain_a & domain_b
Enter fullscreen mode Exit fullscreen mode

Union:

domain_a | domain_b
Enter fullscreen mode Exit fullscreen mode

Difference:

domain_a & ~domain_b
Enter fullscreen mode Exit fullscreen mode

Now domain operations become extremely cheap.

This is one reason highly optimized constraint solvers often look very different from their educational Python implementations.

The underlying mathematics stays the same.

The representation changes.


30. SAT: Constraint Solving Taken to Another Level

There is another major family of solvers:

SAT solvers.

SAT asks:

Is there an assignment of Boolean variables that makes a logical formula true?

For example:

A OR B
Enter fullscreen mode Exit fullscreen mode

and:

NOT A OR C
Enter fullscreen mode Exit fullscreen mode

A solution might be:

A = false
B = true
C = true
Enter fullscreen mode Exit fullscreen mode

SAT solvers are extraordinarily powerful.

Modern SAT solving has developed sophisticated techniques around:

  • conflict analysis
  • clause learning
  • watched literals
  • decision heuristics
  • non-chronological backtracking
  • restarts

A general constraint solver and a SAT solver are different tools, but conceptually they share the same DNA:

Decide
  |
Propagate
  |
Conflict?
  |
Learn
  |
Backtrack
  |
Decide again
Enter fullscreen mode Exit fullscreen mode

That pattern is incredibly powerful.


31. Constraint Programming vs Brute Force

It is tempting to describe a constraint solver as:

"A smarter brute-force algorithm."

That's not completely wrong.

But it's incomplete.

The deeper idea is:

Search plus inference.

Brute force says:

Try everything.
Enter fullscreen mode Exit fullscreen mode

Constraint solving says:

Infer what cannot work.
Don't search it.
Enter fullscreen mode Exit fullscreen mode

That distinction is enormous.

Suppose a search space contains:

1,000,000,000 possibilities
Enter fullscreen mode Exit fullscreen mode

and propagation proves that:

999,999,000
Enter fullscreen mode Exit fullscreen mode

are impossible without exploring them individually.

Now the problem isn't really a billion possibilities anymore.

The solver has compressed the search space using knowledge.

That's the magic.


32. A Production-Grade Architecture

If I were designing a serious constraint-solving library, I'd separate it into layers.

+------------------------------------------------+
|                 User API                       |
+------------------------------------------------+
|           Problem Definition                   |
+------------------------------------------------+
|       Variables / Domains / Constraints        |
+------------------------------------------------+
|          Constraint Propagation                |
+------------------------------------------------+
|            Search Heuristics                   |
+------------------------------------------------+
|         Backtracking Engine                    |
+------------------------------------------------+
|        Conflict / Learning Layer               |
+------------------------------------------------+
|          Domain Representation                 |
+------------------------------------------------+
|        Low-Level Data Structures               |
+------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

The user should be able to say something like:

solver = Solver()

x = solver.variable(
    "x",
    domain=range(1, 10)
)

y = solver.variable(
    "y",
    domain=range(1, 10)
)

solver.add(x != y)

solution = solver.solve()
Enter fullscreen mode Exit fullscreen mode

The user doesn't need to understand recursion.

They shouldn't need to.

The engine handles it.


33. Instrumentation Is Essential

A solver can return the correct answer and still be terrible.

You need to know what it did.

Track metrics such as:

nodes explored
backtracks
constraint checks
propagations
domain reductions
conflicts
maximum depth
solutions found
runtime
Enter fullscreen mode Exit fullscreen mode

For example:

Solver Statistics

Nodes explored:      42,182
Backtracks:          8,391
Propagations:        190,442
Conflicts:           8,391
Solution time:       0.81s
Enter fullscreen mode Exit fullscreen mode

Now you can compare algorithms.

Maybe MRV reduces:

nodes:
42,182 → 8,200
Enter fullscreen mode Exit fullscreen mode

That's evidence.

Performance engineering should always have evidence.


34. Visualizing the Search

One of my favorite ways to understand a solver is to visualize its search tree.

                        START
                          |
                +---------+---------+
                |                   |
              X = 1               X = 2
                |                   |
          +-----+-----+             |
          |           |             |
        Y = 1       Y = 2         Y = 1
          |           |             |
          X           ✓             X
       CONFLICT                  CONFLICT
Enter fullscreen mode Exit fullscreen mode

You can color nodes conceptually as:

✓ = solution
✗ = conflict
○ = unexplored
Enter fullscreen mode Exit fullscreen mode

Then you can watch the solver prune branches.

A good solver is essentially trying to make this tree look less like:

████████████████████████████
Enter fullscreen mode Exit fullscreen mode

and more like:

███
 █
 ██
  █
   ✓
Enter fullscreen mode Exit fullscreen mode

It wants to reach a solution without exploring unnecessary territory.


35. The Strange Connection to AI

Constraint solving has an interesting relationship with artificial intelligence.

Machine learning often works by learning patterns from data.

Constraint solving works by reasoning from explicit rules.

Machine Learning:

Data
 |
 v
Learn Model
 |
 v
Prediction


Constraint Solver:

Rules
 |
 v
Search + Propagation
 |
 v
Valid Assignment
Enter fullscreen mode Exit fullscreen mode

But modern AI systems increasingly combine both.

Imagine an optimization problem where a machine-learning model predicts:

"This variable is probably the best one to assign next."

The ML model provides a heuristic.

The constraint solver guarantees correctness.

That combination is powerful:

Machine Learning
       |
       v
Search Heuristic
       |
       v
Constraint Solver
       |
       v
Guaranteed-valid solution
Enter fullscreen mode Exit fullscreen mode

This is one direction where classical symbolic reasoning and modern AI can complement each other.


36. The Most Important Optimization: Don't Search What You Can Prove

This is the philosophy I'd keep in mind when building a solver.

Suppose you have:

1,000 possible values.
Enter fullscreen mode Exit fullscreen mode

If you can prove:

700 are impossible
Enter fullscreen mode Exit fullscreen mode

don't search them.

If you can prove another:

200 are impossible
Enter fullscreen mode Exit fullscreen mode

don't search those either.

Now you only have:

100
Enter fullscreen mode Exit fullscreen mode

possibilities.

Constraint propagation is therefore a form of computational compression.

You're compressing the search space by reasoning about it.

That is why the best constraint solvers don't feel like brute force.

They feel like deduction.


37. Building the Solver Incrementally

If I were actually implementing this project, I wouldn't start with SAT solving or advanced propagation.

I'd build it in stages.

Version 1

Variables
Domains
Constraints
Backtracking
Enter fullscreen mode Exit fullscreen mode

Version 2

MRV
Enter fullscreen mode Exit fullscreen mode

Version 3

Forward checking
Enter fullscreen mode Exit fullscreen mode

Version 4

Constraint propagation
Enter fullscreen mode Exit fullscreen mode

Version 5

Degree heuristic
LCV
Enter fullscreen mode Exit fullscreen mode

Version 6

Conflict detection
Enter fullscreen mode Exit fullscreen mode

Version 7

Memoization
Enter fullscreen mode Exit fullscreen mode

Version 8

Bitset domains
Enter fullscreen mode Exit fullscreen mode

Version 9

Conflict-directed backtracking
Enter fullscreen mode Exit fullscreen mode

Version 10

Optimization objectives
Enter fullscreen mode Exit fullscreen mode

This progression is valuable because every version teaches you something.

You aren't just building software.

You're discovering why the sophisticated algorithms exist.


38. Testing a Constraint Solver

Testing a solver is different from testing ordinary business logic.

You need to test:

Satisfiable problems

A != B
A = {1,2}
B = {1,2}
Enter fullscreen mode Exit fullscreen mode

Expected:

Solution exists.
Enter fullscreen mode Exit fullscreen mode

Unsatisfiable problems

A = 1
A = 2
Enter fullscreen mode Exit fullscreen mode

Expected:

No solution.
Enter fullscreen mode Exit fullscreen mode

Multiple solutions

Verify that returned assignments satisfy every constraint.

Empty domains

The solver should fail gracefully.

Large domains

Test performance.

Deep recursion

Ensure the implementation doesn't collapse under realistic problem sizes.

And most importantly:

for constraint in constraints:
    assert constraint.satisfied(solution)
Enter fullscreen mode Exit fullscreen mode

A solver's answer is worthless if it violates the rules.

Correctness comes before speed.


39. The Beautiful Thing About Constraints

There is something almost philosophical about constraint programming.

You don't tell the machine:

"Do this."

You tell it:

"This is allowed."

And:

"This is forbidden."

Then you let it reason.

That changes the programming model.

Traditional imperative programming often looks like:

Step 1
Step 2
Step 3
Step 4
Step 5
Enter fullscreen mode Exit fullscreen mode

Constraint programming looks more like:

Here are the variables.

Here are their possible values.

Here are the rules.

Find something that satisfies them.
Enter fullscreen mode Exit fullscreen mode

The computer determines the path.

You're describing the destination indirectly.

That's a powerful abstraction.


40. Final Architecture

At the end of our journey, the solver looks something like this:

                         PROBLEM
                            |
                            v
              +--------------------------+
              | Variables + Domains      |
              +--------------------------+
                            |
                            v
              +--------------------------+
              |      Constraints         |
              +--------------------------+
                            |
                            v
              +--------------------------+
              | Constraint Propagation   |
              +--------------------------+
                            |
                            v
              +--------------------------+
              | Variable Selection       |
              | MRV / Degree             |
              +--------------------------+
                            |
                            v
              +--------------------------+
              | Value Selection           |
              | LCV / Heuristics         |
              +--------------------------+
                            |
                            v
                     Assignment
                            |
                            v
                     Propagation
                            |
                 +----------+----------+
                 |                     |
              Conflict              Valid
                 |                     |
                 v                     v
             Backtrack             Continue
                 |                     |
                 +----------<----------+
                            |
                            v
                       Solution
Enter fullscreen mode Exit fullscreen mode

This architecture is deceptively small.

But it contains the core ideas behind some very sophisticated optimization and reasoning systems.


Conclusion: Build Machines That Know What Not to Try

The first constraint solver I would build is intentionally stupid.

It should brute-force.

It should make bad decisions.

It should backtrack constantly.

Because then you can watch it suffer.

And that suffering teaches you something.

You add MRV.

The search shrinks.

You add forward checking.

It shrinks again.

You add propagation.

It gets faster.

You add better value ordering.

Faster again.

You add conflict learning.

The solver starts jumping over entire regions of the search space.

Eventually, you realize something profound:

The fastest search isn't necessarily the one that searches faster.

It's the one that searches less.

That is the central idea behind constraint solving.

A naive program asks:

"What should I try next?"
Enter fullscreen mode Exit fullscreen mode

A constraint solver asks:

"What can I prove I don't need to try?"
Enter fullscreen mode Exit fullscreen mode

That difference is enormous.

And it appears everywhere.

In scheduling.

In databases.

In compilers.

In logistics.

In cloud infrastructure.

In configuration management.

In games.

In optimization.

In artificial intelligence.

In mathematics.

The world is full of systems where the answer isn't explicitly known, but the rules are.

And whenever you have:

Variables
+
Possible values
+
Rules
Enter fullscreen mode Exit fullscreen mode

you may have the beginning of a constraint satisfaction problem.

The computer doesn't need you to hand it the answer.

You can give it something more interesting.

Give it the rules.

Then teach it how to reason.

And suddenly, a collection of variables becomes a search space.

A collection of rules becomes a mathematical structure.

And a recursive function becomes something much more interesting:

a machine that systematically explores possibility while learning what is impossible.

That's a constraint solver.

And once you understand how to build one, you start seeing programming differently.

Because a surprising amount of software engineering isn't really about telling computers what to do.

It's about defining the boundaries of what they are allowed to do—and giving them enough intelligence to find the path through the space that remains.

Top comments (0)