homework.wenqian.dev
← Course materials

Tutorial 12 · Constraint Solving with Backtracking

Try a choice · Check conflicts · Undo · Find a solution

Goal: independently write a solver that tries candidates, rejects conflicts, restores state, and returns valid assignments. The teacher controls the demonstrations; students implement the Python functions below. 12 distinct coding tasks, including 2 optional challenges.

Copy full template

Python 3. No additional packages. Save as constraints.py; run python3 constraints.py 1 to check Q1. On Windows, python3 can be replaced with py.

A legal choice now may fail later

Build a program that tries choices, checks conflicts, and goes back to choose again. Watch the seating example before Q1 and Q2.

Class demonstration

Seat A, B and C

A and B cannot be adjacent; C cannot sit at an end; use each person once.

1/25

Seat A, B and C

A and B cannot be adjacent; C cannot sit at an end; use each person once.

·0
·1
·2

Blue: trial position · red: conflict source · gray: fixed clue. The change panel shows actual writes.

What changed in this step

The assignment is unchanged.

Candidates and usage
ABC
Valid solutions saved:0

No result yet

Manual control by default. Auto play pauses at the first conflict, undo and save.

Backtracking structure (pseudocode): decide one position per call. Check, choose, recurse, then undo this level’s changes.

python
def search(position):    if all_positions_filled():        save_a_copy()        return    for candidate in candidates_for(position):        if not allowed(position, candidate):            continue        old_state = make_choice(position, candidate)        search(position + 1)        undo_choice(position, candidate, old_state)

Record a choice → search the next position → restore this level’s changes. used describes the current assignment, not the whole search history.

1. Seats with adjacency rules

Coding

Seat everyone once in a row. Pairs in forbidden cannot sit next to each other; people in middle_only cannot sit at either end. Return all arrangements, in any order. Reversals are distinct.

Input
Distinct names people, forbidden name pairs, and a set middle_only. At least one person.
Output
A list of seating lists; [] if impossible.

Example

plain
seat_people(['A','B','C'], [], {'A'})→ [['B','A','C'], ['C','A','B']]

Template

python
def seat_people(people, forbidden, middle_only):    # Decide who sits at position len(path).    path = []    used = set()    answers = []    # TODO: check the end-seat and adjacency rules before each choice.    raise NotImplementedError("Q01")
Check: python3 constraints.py 1
python
def check_q01():    people = ['A','B','C','D']    forbidden = [('A','B')]    original = people[:]    result = seat_people(people, forbidden, {'C'})    check_unique(result, 8)    for row in result:        assert set(row) == set(people) and len(row) == 4        assert row[0] != 'C' and row[-1] != 'C'        assert all(set(row[i:i+2]) != {'A','B'} for i in range(3))    assert people == original    assert seat_people(['A'], [], {'A'}) == []    check_unique(seat_people(['A','B','C'], [], {'A'}), 2)

2. Allowed locker assignments

Coding

Each person has a list of allowed lockers. Assign one locker to each person without sharing. Return every assignment in person order. Backtrack when an early choice leaves a later person with no option.

Input
allowed: a list of distinct allowed locker IDs for each person.
Output
All assignment lists; [[]] for no people.

Example

plain
assign_lockers([[1,2], [1], [2,3]])→ [[2,1,3]]

Template

python
def assign_lockers(allowed):    assignment = []    used = set()    answers = []    # TODO: candidates for person i come from allowed[i], not all lockers.    raise NotImplementedError("Q02")
Check: python3 constraints.py 2
python
def check_q02():    allowed = [[1,2], [1], [2,3]]    original = copy.deepcopy(allowed)    assert assign_lockers(allowed) == [[2,1,3]]    assert allowed == original    assert assign_lockers([[1], [1]]) == []    assert assign_lockers([]) == [[]]    result = assign_lockers([[1,2,3], [1,2,3]])    check_unique(result, 6)    assert all(len(r)==2 and len(set(r))==2 and set(r)<={1,2,3} for r in result)

Whether a candidate can be reused depends on the rules

Seats cannot reuse a person, but regions can reuse colors. Try a triangle with two colors, watch the failed search, then switch to three colors.

Class demonstration

Triangle map: two colors

Endpoints of every edge must differ in color. Colors may be reused; check all colored neighbors.

1/16

Triangle map: two colors

Endpoints of every edge must differ in color. Colors may be reused; check all colored neighbors.

·Node 0·Node 1·Node 2

Blue: trial position · red: conflict source · gray: fixed clue. The change panel shows actual writes.

What changed in this step

The assignment is unchanged.

Candidates and usage
01

Colors can be reused; only equal colors across an edge conflict.

Valid solutions saved:0

No result yet

Manual control by default. Auto play pauses at the first conflict, undo and save.

Do not add the same used structure to every task. First identify the constraints, then choose the state to track.

3. Map coloring

Coding

Regions are 0..n-1. Adjacent regions must differ in color. Enumerate all assignments using colors 0..k-1. Nonadjacent regions may share a color.

Input
Undirected adjacency list graph without self-loops or duplicate edges; k >= 1.
Output
Lists of colors indexed by region. Different color labels are distinct assignments.

Example

plain
color_map([[1],[0,2],[1]], 2)→ [[0,1,0], [1,0,1]]

Template

python
def color_map(graph, k):    colors = [-1] * len(graph)    answers = []    # TODO: check every already-colored neighbor, not only the previous region.    raise NotImplementedError("Q03")
Check: python3 constraints.py 3
python
def check_q03():    triangle = [[1,2],[0,2],[0,1]]    assert color_map(triangle, 2) == []    result = color_map(triangle, 3)    check_unique(result, 6)    assert all(len(r)==3 and set(r)=={0,1,2} for r in result)    check_unique(color_map([[1],[0,2],[1]], 2), 2)    check_unique(color_map([[],[]], 2), 4)    assert color_map([], 2) == [[]]

4. Code-breaking clues

Coding

Build a code from distinct characters in digits, each used at most once. A clue (guess, hits) means exactly hits positions match the code; it says nothing about other character occurrences. Find every code. Leading zero is allowed.

Input
Distinct digit characters digits, length, and clues containing equal-length guesses and exact position-match counts.
Output
A list of code strings, in any order.

Example

plain
solve_lock('012', 3, [('012',1), ('021',0)])→ ['102', '210']

Template

python
def solve_lock(digits, length, clues):    path = []    used = set()    answers = []    # TODO: a partial code must not exceed a clue's hit count.    # It must also have enough remaining positions to reach that count.    raise NotImplementedError("Q04")
Check: python3 constraints.py 4
python
def check_q04():    assert set(solve_lock('012', 3, [('012',1),('021',0)])) == {'102','210'}    assert solve_lock('012', 3, [('012',3),('012',0)]) == []    result = solve_lock('0123', 2, [])    check_unique(result, 12)    assert all(len(r)==2 and len(set(r))==2 and set(r)<=set('0123') for r in result)    assert solve_lock('01', 3, []) == []

Sudoku: one choice must pass three checks

Locate conflicts on the board. Inspect why a candidate cannot be placed, then try another. Fixed clues stay unchanged.

Class demonstration

Sudoku: check row, column and box

Gray numbers are fixed clues. Fill empty cells with 1..4; rows, columns and 2×2 boxes cannot repeat a number.

1/23

Sudoku: check row, column and box

Gray numbers are fixed clues. Fill empty cells with 1..4; rows, columns and 2×2 boxes cannot repeat a number.

10,0
·0,1
·0,2
40,3
·1,0
41,1
11,2
·1,3
·2,0
12,1
42,2
·2,3
43,0
·3,1
·3,2
13,3

Boxes: top-left / top-right / bottom-left / bottom-right, each 2 × 2.

Blue: trial position · red: conflict source · gray: fixed clue. The change panel shows actual writes.

What changed in this step

The assignment is unchanged.

Candidates and usage
1234
Valid solutions saved:0

No result yet

Manual control by default. Auto play pauses at the first conflict, undo and save.

This task needs only one solution, so return immediately on success. Do not confuse finding one solution with enumerating them all.

5. 4 × 4 Sudoku

Coding

Fill zeros with 1..4 so every row, column and 2×2 box contains 1..4 once. Preserve clues and the input. Return any solution, or None for conflicting clues or an unsatisfiable puzzle.

Input
A 4×4 integer matrix containing 0..4.
Output
A completed matrix or None.

Example

plain
solve_sudoku([[1,0,0,4],[0,4,1,0],[0,1,4,0],[4,0,0,1]])→ [[1,2,3,4],[3,4,1,2],[2,1,4,3],[4,3,2,1]]# Other valid solutions are accepted.

Template

python
def solve_sudoku(board):    grid = [row[:] for row in board]    # TODO: validate existing clues; locate an empty cell.    # Try digits that do not conflict with its row, column or 2x2 box.    # Restore a failed choice to 0. Stop after finding one full solution.    raise NotImplementedError("Q05")
Check: python3 constraints.py 5
python
def check_q05():    puzzle = [[1,0,0,4],[0,4,1,0],[0,1,4,0],[4,0,0,1]]    original = copy.deepcopy(puzzle)    solved = solve_sudoku(puzzle)    assert solved is not None    assert len(solved)==4 and all(len(row)==4 for row in solved)    assert all(set(row)=={1,2,3,4} for row in solved)    assert all({solved[r][c] for r in range(4)}=={1,2,3,4} for c in range(4))    assert all({solved[r+i][c+j] for i in range(2) for j in range(2)}=={1,2,3,4}               for r in (0,2) for c in (0,2))    assert all(not puzzle[r][c] or solved[r][c]==puzzle[r][c] for r in range(4) for c in range(4))    assert puzzle == original    bad = [[1,1,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]]    assert solve_sudoku(bad) is None    assert solve_sudoku(solved) == solved

Crossword: undo does not always mean clear

Place AB, then BC; the last slot has no match. Undo BC while keeping the shared B, then try BD and fill ED.

Class demonstration

Crossword: undo BC without erasing the existing B

Fill the top horizontal slot, right vertical slot, then bottom horizontal slot. Use each word once; crossing letters must match.

1/16

Crossword: undo BC without erasing the existing B

Fill the top horizontal slot, right vertical slot, then bottom horizontal slot. Use each word once; crossing letters must match.

·0,0
·0,1
·1,0
·1,1

Blue: trial position · red: conflict source · gray: fixed clue. The change panel shows actual writes.

What changed in this step

The assignment is unchanged.

Candidates and usage
ABBCBDED
Valid solutions saved:0

No result yet

Manual control by default. Auto play pauses at the first conflict, undo and save.

Save old values before writing and restore them on failure. For dominoes and jigsaws, likewise identify every position changed by a choice.

6. Crossword intersections

Coding

Fill the given slots with words, each used at most once. Lengths and crossing letters must match; preserve existing letters. Return any completed board or None, without changing inputs.

Input
String rows board (# wall, . empty), slots as ordered coordinate lists, and distinct words. Slots contain only non-wall cells and cover every empty cell.
Output
A list of string rows or None.

Example

plain
fill_crossword(['...','#.#','#.#'],  [[(0,0),(0,1),(0,2)],[(0,1),(1,1),(2,1)]],  ['DOG','CAT','APE'])→ ['CAT','#P#','#E#']

Template

python
def fill_crossword(board, slots, words):    grid = [list(row) for row in board]    used = set()    # TODO: before writing a word, save the OLD content of every slot cell.    # Undo must restore old letters, not replace the entire slot with dots.    raise NotImplementedError("Q06")
Check: python3 constraints.py 6
python
def check_q06():    board = ['...','#.#','#.#']    slots = [[(0,0),(0,1),(0,2)],[(0,1),(1,1),(2,1)]]    assert fill_crossword(board, slots, ['DOG','CAT','APE']) == ['CAT','#P#','#E#']    assert board == ['...','#.#','#.#']    assert fill_crossword(board, slots, ['ANT','DOG']) is None    assert fill_crossword(['.A.','#.#','#.#'], slots, ['CAT','APE']) == ['CAT','#P#','#E#']    # A shared cell must survive undoing a later word.    s = [[(0,0),(0,1)],[(0,1),(1,1)],[(1,0),(1,1)]]    assert fill_crossword(['..','..'], s, ['AB','BC','BD','ED']) == ['AB','ED']

7. Domino tiling

Coding

Cover every nonblocked cell with horizontal or vertical 1×2 dominoes, without overlap. List distinct tilings; placing the same dominoes in another order is not a new answer.

Input
rows and cols in 1..4; blocked is a set of blocked cell coordinates.
Output
List of tilings; each tiling is a list of ((r1,c1),(r2,c2)) dominoes. A fully blocked board has one empty tiling.

Example

plain
tile_dominoes(2, 2, set())→ [[((0,0),(0,1)),((1,0),(1,1))],   [((0,0),(1,0)),((0,1),(1,1))]]

Template

python
def tile_dominoes(rows, cols, blocked):    occupied = set(blocked)    placement = []    answers = []    # TODO: always cover the FIRST empty cell next.    # A single choice occupies two cells; undo must free both.    raise NotImplementedError("Q07")
Check: python3 constraints.py 7
python
def check_q07():    for rows,cols,blocked,count in [(2,2,set(),2),(2,3,set(),3),(3,3,set(),0),                                   (2,2,{(0,0)},0),(1,2,{(0,0),(0,1)},1)]:        original = set(blocked)        result = tile_dominoes(rows,cols,blocked)        canonical = []        for tiling in result:            cells = []            for a,b in tiling:                assert abs(a[0]-b[0])+abs(a[1]-b[1])==1                cells.extend((a,b))            assert len(cells)==len(set(cells))            assert set(cells)=={(r,c) for r in range(rows) for c in range(cols)}-blocked            canonical.append(tuple(sorted(tuple(sorted(d)) for d in tiling)))        assert len(canonical)==len(set(canonical))==count        assert blocked==original

8. Visit every place exactly once

Coding

Start at start and follow edges to visit every vertex exactly once, without returning to the start. List all routes. This is not a shortest-path task; other branches may reuse vertices.

Input
A nonempty undirected adjacency list without self-loops or duplicate edges, and a valid start vertex.
Output
Lists of vertex IDs, or [] if no route visits every vertex.

Example

plain
visit_all([[1,2],[0,2],[0,1]], 0)→ [[0,1,2],[0,2,1]]

Template

python
def visit_all(graph, start):    path = [start]    used = {start}    answers = []    # TODO: only extend along an edge; save only paths visiting EVERY vertex.    # Remove a vertex from used when returning to try another route.    raise NotImplementedError("Q08")
Check: python3 constraints.py 8
python
def check_q08():    graph = [[1,2],[0,2],[0,1]]    result = visit_all(graph,0)    check_unique(result,2)    for path in result:        assert path[0]==0 and set(path)=={0,1,2} and len(path)==3        assert all(b in graph[a] for a,b in zip(path,path[1:]))    assert visit_all([[1],[0,2],[1]],1)==[]    assert visit_all([[1],[0],[]],0)==[]    assert visit_all([[]],0)==[[0]]

9. Matching jigsaw edges

Coding

Place four numbered tiles in a 2×2 board. Touching edges must have equal colors; outer edges are unrestricted. No rotations; use every tile once. Enumerate all arrangements.

Input
Four tuples (top,right,bottom,left). Tile IDs remain distinct even when all edge colors match.
Output
All valid tile-ID lists [top-left,top-right,bottom-left,bottom-right].

Example

plain
assemble_tiles([('x','a','b','x'),('x','x','c','a'),                ('b','d','x','x'),('c','x','x','d')])# One valid result is [0,1,2,3].

Template

python
def assemble_tiles(tiles):    placement = []    used = set()    answers = []    # TODO: a new tile must match both its placed left and upper neighbors.    raise NotImplementedError("Q09")
Check: python3 constraints.py 9
python
def check_q09():    tiles=[('x','a','b','x'),('x','x','c','a'),('b','d','x','x'),('c','x','x','d')]    result=assemble_tiles(tiles)    expected=[]    for p in itertools.permutations(range(4)):        if (tiles[p[0]][1]==tiles[p[1]][3] and tiles[p[2]][1]==tiles[p[3]][3]            and tiles[p[0]][2]==tiles[p[2]][0] and tiles[p[1]][2]==tiles[p[3]][0]):            expected.append(list(p))    check_same_rows(result,expected)    check_unique(assemble_tiles([('a','a','a','a')]*4),24)    assert assemble_tiles([(str(i),)*4 for i in range(4)])==[]

10. Letter arithmetic

Coding

Assign distinct digits 0..9 to distinct letters so left + right = total. Multi-digit words cannot start with zero; single-digit words may. List every mapping. Inputs contain at most five distinct letters.

Input
Three nonempty uppercase letter strings.
Output
A list of letter-to-digit dictionaries, in any order.

Example

plain
solve_letters('A','A','B')→ [{'A':1,'B':2}, {'A':2,'B':4},   {'A':3,'B':6}, {'A':4,'B':8}]

Template

python
def solve_letters(left, right, total):    letters = sorted(set(left + right + total))    assignment = {}    used = set()    answers = []    # TODO: enforce distinct digits and leading-zero rules.    # Once all letters are assigned, evaluate the actual equation.    raise NotImplementedError("Q10")
Check: python3 constraints.py 10
python
def check_q10():    result=solve_letters('A','A','B')    assert {tuple(sorted(d.items())) for d in result}=={        (('A',1),('B',2)),(('A',2),('B',4)),(('A',3),('B',6)),(('A',4),('B',8))}    assert len(result)==4    result=solve_letters('AB','BA','CC')    expected={(a,b,a+b) for a in range(1,10) for b in range(1,10) if a!=b and a+b<=9}    assert len(result)==len(expected)    assert {(d['A'],d['B'],d['C']) for d in result}==expected    assert solve_letters('A','B','A')==[{'A':a,'B':0} for a in range(1,10)]

11. Optional: Nonogram

Coding · Optional

Fill a 0/1 grid, where 1 is black. Each row and column must have exactly the specified consecutive black-run lengths, with at least one zero between runs. Return any solution or None.

Input
row_clues and col_clues with 1..5 rows and columns; [] means an entirely blank line.
Output
A 0/1 matrix or None.

Example

plain
solve_nonogram([[1],[3],[1]], [[1],[3],[1]])→ [[0,1,0],[1,1,1],[0,1,0]]

Template

python
def solve_nonogram(row_clues, col_clues):    # TODO: generate row patterns matching each row clue.    # Choose one pattern per row, then verify all column runs.    # Extra: reject a column prefix as soon as it cannot match its clue.    raise NotImplementedError("Q11")
Check: python3 constraints.py 11
python
def check_q11():    assert solve_nonogram([[1],[3],[1]],[[1],[3],[1]])==[[0,1,0],[1,1,1],[0,1,0]]    assert solve_nonogram([[]],[[]])==[[0]]    assert solve_nonogram([[1]],[[]]) is None    result=solve_nonogram([[1],[1]],[[1],[1]])    assert result in ([[1,0],[0,1]],[[0,1],[1,0]])

Challenge: restore multiple constraints together

A queen occupies a column and two diagonals. Red lines show attacks; inspect why the search must return to an earlier row.

Class demonstration

4 queens: columns and both diagonals

Place one Q per row. Red queens share a column or diagonal with the candidate: try another column.

1/80

4 queens: columns and both diagonals

Place one Q per row. Red queens share a column or diagonal with the candidate: try another column.

·0,0
·0,1
·0,2
·0,3
·1,0
·1,1
·1,2
·1,3
·2,0
·2,1
·2,2
·2,3
·3,0
·3,1
·3,2
·3,3

Blue: trial position · red: conflict source · gray: fixed clue. The change panel shows actual writes.

What changed in this step

The assignment is unchanged.

Candidates and usage
column 0column 1column 2column 3
Valid solutions saved:0

No result yet

Manual control by default. Auto play pauses at the first conflict, undo and save.

Placement and undo must match: restore column, r-c and r+c state together.

12. Optional: N-queens

Coding · Optional

Place n queens on an n×n board with no shared row, column or diagonal. Place one queen per row and enumerate all arrangements. Rotations and reflections count separately.

Input
1 <= n <= 8.
Output
Each result gives the queen column for each row r.

Example

plain
n_queens(4) → [[1,3,0,2], [2,0,3,1]]

Template

python
def n_queens(n):    placement = []    columns, down_diagonals, up_diagonals = set(), set(), set()    answers = []    # TODO: (r,c) occupies column c and diagonals r-c and r+c.    # Undo all three sets when returning.    raise NotImplementedError("Q12")
Check: python3 constraints.py 12
python
def check_q12():    for n,count in [(1,1),(2,0),(3,0),(4,2),(5,10),(6,4)]:        result=n_queens(n)        check_unique(result,count)        for row in result:            assert len(row)==n and set(row)==set(range(n))            assert len({r-c for r,c in enumerate(row)})==n            assert len({r+c for r,c in enumerate(row)})==n

Full template

Includes all functions and checks. Replace each function as you complete it. Q1–10 are core exercises; Q11–12 are optional challenges.

python
# Tutorial 12 — Constraint Solving with Backtracking# Python 3; standard library only. Q1–10 core; Q11–12 optional.# Run one question: python3 constraints.py 1def seat_people(people, forbidden, middle_only):    # Decide who sits at position len(path).    path = []    used = set()    answers = []    # TODO: check the end-seat and adjacency rules before each choice.    raise NotImplementedError("Q01")def assign_lockers(allowed):    assignment = []    used = set()    answers = []    # TODO: candidates for person i come from allowed[i], not all lockers.    raise NotImplementedError("Q02")def color_map(graph, k):    colors = [-1] * len(graph)    answers = []    # TODO: check every already-colored neighbor, not only the previous region.    raise NotImplementedError("Q03")def solve_lock(digits, length, clues):    path = []    used = set()    answers = []    # TODO: a partial code must not exceed a clue's hit count.    # It must also have enough remaining positions to reach that count.    raise NotImplementedError("Q04")def solve_sudoku(board):    grid = [row[:] for row in board]    # TODO: validate existing clues; locate an empty cell.    # Try digits that do not conflict with its row, column or 2x2 box.    # Restore a failed choice to 0. Stop after finding one full solution.    raise NotImplementedError("Q05")def fill_crossword(board, slots, words):    grid = [list(row) for row in board]    used = set()    # TODO: before writing a word, save the OLD content of every slot cell.    # Undo must restore old letters, not replace the entire slot with dots.    raise NotImplementedError("Q06")def tile_dominoes(rows, cols, blocked):    occupied = set(blocked)    placement = []    answers = []    # TODO: always cover the FIRST empty cell next.    # A single choice occupies two cells; undo must free both.    raise NotImplementedError("Q07")def visit_all(graph, start):    path = [start]    used = {start}    answers = []    # TODO: only extend along an edge; save only paths visiting EVERY vertex.    # Remove a vertex from used when returning to try another route.    raise NotImplementedError("Q08")def assemble_tiles(tiles):    placement = []    used = set()    answers = []    # TODO: a new tile must match both its placed left and upper neighbors.    raise NotImplementedError("Q09")def solve_letters(left, right, total):    letters = sorted(set(left + right + total))    assignment = {}    used = set()    answers = []    # TODO: enforce distinct digits and leading-zero rules.    # Once all letters are assigned, evaluate the actual equation.    raise NotImplementedError("Q10")def solve_nonogram(row_clues, col_clues):    # TODO: generate row patterns matching each row clue.    # Choose one pattern per row, then verify all column runs.    # Extra: reject a column prefix as soon as it cannot match its clue.    raise NotImplementedError("Q11")def n_queens(n):    placement = []    columns, down_diagonals, up_diagonals = set(), set(), set()    answers = []    # TODO: (r,c) occupies column c and diagonals r-c and r+c.    # Undo all three sets when returning.    raise NotImplementedError("Q12")# Checks; itertools is used only by checks.import copyimport itertoolsdef freeze(value):    if isinstance(value, dict):        return tuple(sorted((k,freeze(v)) for k,v in value.items()))    if isinstance(value,(list,tuple)):        return tuple(freeze(v) for v in value)    return valuedef check_unique(result, count):    assert isinstance(result,list), "Return a list"    assert len(result)==count, f"Expected {count} solutions; got {len(result)}"    assert len({freeze(row) for row in result})==count, "Duplicate solutions"def check_same_rows(actual, expected):    check_unique(actual,len(expected))    assert {freeze(row) for row in actual}=={freeze(row) for row in expected}def check_q01():    people = ['A','B','C','D']    forbidden = [('A','B')]    original = people[:]    result = seat_people(people, forbidden, {'C'})    check_unique(result, 8)    for row in result:        assert set(row) == set(people) and len(row) == 4        assert row[0] != 'C' and row[-1] != 'C'        assert all(set(row[i:i+2]) != {'A','B'} for i in range(3))    assert people == original    assert seat_people(['A'], [], {'A'}) == []    check_unique(seat_people(['A','B','C'], [], {'A'}), 2)def check_q02():    allowed = [[1,2], [1], [2,3]]    original = copy.deepcopy(allowed)    assert assign_lockers(allowed) == [[2,1,3]]    assert allowed == original    assert assign_lockers([[1], [1]]) == []    assert assign_lockers([]) == [[]]    result = assign_lockers([[1,2,3], [1,2,3]])    check_unique(result, 6)    assert all(len(r)==2 and len(set(r))==2 and set(r)<={1,2,3} for r in result)def check_q03():    triangle = [[1,2],[0,2],[0,1]]    assert color_map(triangle, 2) == []    result = color_map(triangle, 3)    check_unique(result, 6)    assert all(len(r)==3 and set(r)=={0,1,2} for r in result)    check_unique(color_map([[1],[0,2],[1]], 2), 2)    check_unique(color_map([[],[]], 2), 4)    assert color_map([], 2) == [[]]def check_q04():    assert set(solve_lock('012', 3, [('012',1),('021',0)])) == {'102','210'}    assert solve_lock('012', 3, [('012',3),('012',0)]) == []    result = solve_lock('0123', 2, [])    check_unique(result, 12)    assert all(len(r)==2 and len(set(r))==2 and set(r)<=set('0123') for r in result)    assert solve_lock('01', 3, []) == []def check_q05():    puzzle = [[1,0,0,4],[0,4,1,0],[0,1,4,0],[4,0,0,1]]    original = copy.deepcopy(puzzle)    solved = solve_sudoku(puzzle)    assert solved is not None    assert len(solved)==4 and all(len(row)==4 for row in solved)    assert all(set(row)=={1,2,3,4} for row in solved)    assert all({solved[r][c] for r in range(4)}=={1,2,3,4} for c in range(4))    assert all({solved[r+i][c+j] for i in range(2) for j in range(2)}=={1,2,3,4}               for r in (0,2) for c in (0,2))    assert all(not puzzle[r][c] or solved[r][c]==puzzle[r][c] for r in range(4) for c in range(4))    assert puzzle == original    bad = [[1,1,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]]    assert solve_sudoku(bad) is None    assert solve_sudoku(solved) == solveddef check_q06():    board = ['...','#.#','#.#']    slots = [[(0,0),(0,1),(0,2)],[(0,1),(1,1),(2,1)]]    assert fill_crossword(board, slots, ['DOG','CAT','APE']) == ['CAT','#P#','#E#']    assert board == ['...','#.#','#.#']    assert fill_crossword(board, slots, ['ANT','DOG']) is None    assert fill_crossword(['.A.','#.#','#.#'], slots, ['CAT','APE']) == ['CAT','#P#','#E#']    # A shared cell must survive undoing a later word.    s = [[(0,0),(0,1)],[(0,1),(1,1)],[(1,0),(1,1)]]    assert fill_crossword(['..','..'], s, ['AB','BC','BD','ED']) == ['AB','ED']def check_q07():    for rows,cols,blocked,count in [(2,2,set(),2),(2,3,set(),3),(3,3,set(),0),                                   (2,2,{(0,0)},0),(1,2,{(0,0),(0,1)},1)]:        original = set(blocked)        result = tile_dominoes(rows,cols,blocked)        canonical = []        for tiling in result:            cells = []            for a,b in tiling:                assert abs(a[0]-b[0])+abs(a[1]-b[1])==1                cells.extend((a,b))            assert len(cells)==len(set(cells))            assert set(cells)=={(r,c) for r in range(rows) for c in range(cols)}-blocked            canonical.append(tuple(sorted(tuple(sorted(d)) for d in tiling)))        assert len(canonical)==len(set(canonical))==count        assert blocked==originaldef check_q08():    graph = [[1,2],[0,2],[0,1]]    result = visit_all(graph,0)    check_unique(result,2)    for path in result:        assert path[0]==0 and set(path)=={0,1,2} and len(path)==3        assert all(b in graph[a] for a,b in zip(path,path[1:]))    assert visit_all([[1],[0,2],[1]],1)==[]    assert visit_all([[1],[0],[]],0)==[]    assert visit_all([[]],0)==[[0]]def check_q09():    tiles=[('x','a','b','x'),('x','x','c','a'),('b','d','x','x'),('c','x','x','d')]    result=assemble_tiles(tiles)    expected=[]    for p in itertools.permutations(range(4)):        if (tiles[p[0]][1]==tiles[p[1]][3] and tiles[p[2]][1]==tiles[p[3]][3]            and tiles[p[0]][2]==tiles[p[2]][0] and tiles[p[1]][2]==tiles[p[3]][0]):            expected.append(list(p))    check_same_rows(result,expected)    check_unique(assemble_tiles([('a','a','a','a')]*4),24)    assert assemble_tiles([(str(i),)*4 for i in range(4)])==[]def check_q10():    result=solve_letters('A','A','B')    assert {tuple(sorted(d.items())) for d in result}=={        (('A',1),('B',2)),(('A',2),('B',4)),(('A',3),('B',6)),(('A',4),('B',8))}    assert len(result)==4    result=solve_letters('AB','BA','CC')    expected={(a,b,a+b) for a in range(1,10) for b in range(1,10) if a!=b and a+b<=9}    assert len(result)==len(expected)    assert {(d['A'],d['B'],d['C']) for d in result}==expected    assert solve_letters('A','B','A')==[{'A':a,'B':0} for a in range(1,10)]def check_q11():    assert solve_nonogram([[1],[3],[1]],[[1],[3],[1]])==[[0,1,0],[1,1,1],[0,1,0]]    assert solve_nonogram([[]],[[]])==[[0]]    assert solve_nonogram([[1]],[[]]) is None    result=solve_nonogram([[1],[1]],[[1],[1]])    assert result in ([[1,0],[0,1]],[[0,1],[1,0]])def check_q12():    for n,count in [(1,1),(2,0),(3,0),(4,2),(5,10),(6,4)]:        result=n_queens(n)        check_unique(result,count)        for row in result:            assert len(row)==n and set(row)==set(range(n))            assert len({r-c for r,c in enumerate(row)})==n            assert len({r+c for r,c in enumerate(row)})==nimport sysCHECKS = {1: check_q01, 2: check_q02, 3: check_q03, 4: check_q04, 5: check_q05, 6: check_q06, 7: check_q07, 8: check_q08, 9: check_q09, 10: check_q10, 11: check_q11, 12: check_q12}GROUPS = {"core": list(range(1,11)), "extra": [11,12], "all": list(range(1,13))}if __name__ == "__main__":    name = sys.argv[1] if len(sys.argv)>1 else "core"    if name.isdigit() and int(name) in CHECKS:        selected = [int(name)]    elif name in GROUPS:        selected = GROUPS[name]    else:        print("Choose core, extra, all, or a question number from 1 to 12.")        sys.exit(2)    for number in selected:        try:            CHECKS[number]()        except Exception as error:            print(f"Q{number:02d} failed: {type(error).__name__}: {error}")            sys.exit(1)        print(f"Q{number:02d} passed.")    print(f"{len(selected)}/{len(selected)} checks passed.")
bash
python3 constraints.py       # Q1–10python3 constraints.py extra # Q11–12python3 constraints.py all   # all coding questions