Constraint Satisfaction Problems (CSP) — Practice Tasks¶
All tasks must be solved in Go, Java, and Python. Each task ships with a precise spec and starter code in all three languages. Implement the CSP modeling and backtracking/propagation logic and validate against the evaluation criteria. Always test against a brute-force generate-and-test oracle on small inputs before trusting your solver, and independently re-validate every returned assignment against all constraints.
Beginner Tasks (4)¶
Task 1 — Consistency check¶
Problem. Implement consistent(var, value, assignment, neighbors) that returns whether assigning value to var violates any ≠ constraint with already-assigned neighbors.
Input / Output spec. - Inputs: a variable id, a candidate value, a map of assigned variables → values, and a neighbor list for var. - Output: true if no assigned neighbor already holds value, else false.
Constraints. - Up to 1000 variables; neighbor lists may be large. - Only assigned neighbors constrain the check.
Hint. Loop the neighbors; return false on the first assigned neighbor whose value equals value.
Starter — Go.
package main
import "fmt"
func consistent(v, val int, assign map[int]int, neighbors []int) bool {
// TODO: return false if any assigned neighbor holds val
return true
}
func main() {
assign := map[int]int{1: 0, 2: 1}
fmt.Println(consistent(0, 0, assign, []int{1, 2})) // false
fmt.Println(consistent(0, 2, assign, []int{1, 2})) // true
}
Starter — Java.
import java.util.*;
public class Task1 {
static boolean consistent(int v, int val, Map<Integer, Integer> assign, int[] neighbors) {
// TODO
return true;
}
public static void main(String[] args) {
Map<Integer, Integer> assign = Map.of(1, 0, 2, 1);
System.out.println(consistent(0, 0, assign, new int[]{1, 2})); // false
System.out.println(consistent(0, 2, assign, new int[]{1, 2})); // true
}
}
Starter — Python.
def consistent(v, val, assign, neighbors):
# TODO: return False if any assigned neighbor holds val
return True
if __name__ == "__main__":
assign = {1: 0, 2: 1}
print(consistent(0, 0, assign, [1, 2])) # False
print(consistent(0, 2, assign, [1, 2])) # True
Task 2 — MRV variable selection¶
Problem. Given current domains (a map from variable to list of legal values) and a set of assigned variables, return the unassigned variable with the fewest remaining values (break ties by smallest id).
Input / Output spec. - Inputs: domains, assigned set. - Output: the chosen variable id, or -1 if none unassigned.
Constraints. 1 ≤ vars ≤ 10⁴. Deterministic tie-break required.
Hint. Track best count and best id; iterate variables in increasing id order.
Starter — Go.
package main
import "fmt"
func selectMRV(domains map[int][]int, assigned map[int]bool, vars []int) int {
// TODO: smallest domain among unassigned; tie -> smallest id
return -1
}
func main() {
domains := map[int][]int{0: {1, 2, 3}, 1: {2}, 2: {1, 2}}
fmt.Println(selectMRV(domains, map[int]bool{}, []int{0, 1, 2})) // 1
}
Starter — Java.
import java.util.*;
public class Task2 {
static int selectMRV(Map<Integer, List<Integer>> domains, Set<Integer> assigned, int[] vars) {
// TODO
return -1;
}
public static void main(String[] args) {
Map<Integer, List<Integer>> domains = new HashMap<>();
domains.put(0, List.of(1, 2, 3));
domains.put(1, List.of(2));
domains.put(2, List.of(1, 2));
System.out.println(selectMRV(domains, new HashSet<>(), new int[]{0, 1, 2})); // 1
}
}
Starter — Python.
def select_mrv(domains, assigned, variables):
# TODO: smallest domain among unassigned; tie -> smallest id
return -1
if __name__ == "__main__":
domains = {0: [1, 2, 3], 1: [2], 2: [1, 2]}
print(select_mrv(domains, set(), [0, 1, 2])) # 1
Task 3 — Forward checking step¶
Problem. After assigning var = value, prune value from each unassigned neighbor's domain. Return the list of neighbors whose domain became empty (wipeouts). Do not mutate domains of assigned neighbors.
Input / Output spec. - Inputs: var, value, domains, neighbors, assigned set. - Output: list of wiped-out neighbor ids (empty list if none).
Constraints. Domains are lists/sets of ints. Mutation in place is allowed.
Hint. For each unassigned neighbor, remove value; if its domain is now empty, record it.
Starter — Go.
package main
import "fmt"
func forwardCheck(v, val int, domains map[int][]int, neighbors []int, assigned map[int]bool) []int {
// TODO: prune val from unassigned neighbors; collect wipeouts
return nil
}
func main() {
domains := map[int][]int{1: {0}, 2: {0, 1}}
fmt.Println(forwardCheck(0, 0, domains, []int{1, 2}, map[int]bool{})) // [1]
}
Starter — Java.
import java.util.*;
public class Task3 {
static List<Integer> forwardCheck(int v, int val, Map<Integer, List<Integer>> domains,
int[] neighbors, Set<Integer> assigned) {
// TODO
return new ArrayList<>();
}
public static void main(String[] args) {
Map<Integer, List<Integer>> domains = new HashMap<>();
domains.put(1, new ArrayList<>(List.of(0)));
domains.put(2, new ArrayList<>(List.of(0, 1)));
System.out.println(forwardCheck(0, 0, domains, new int[]{1, 2}, new HashSet<>())); // [1]
}
}
Starter — Python.
def forward_check(v, val, domains, neighbors, assigned):
# TODO: prune val from unassigned neighbors; collect wipeouts
return []
if __name__ == "__main__":
domains = {1: [0], 2: [0, 1]}
print(forward_check(0, 0, domains, [1, 2], set())) # [1]
Task 4 — Map coloring decision (2 colors)¶
Problem. Given an undirected graph, decide whether it is 2-colorable (i.e. bipartite) using a CSP backtracking solver. Return true/false.
Input / Output spec. - Inputs: n (vertices), edges (list of pairs). - Output: boolean.
Constraints. 1 ≤ n ≤ 1000. Graph may be disconnected.
Hint. Domain {0, 1}; backtrack with ≠ constraints, or recognize this is just bipartiteness.
Starter — Go.
package main
import "fmt"
func twoColorable(n int, edges [][2]int) bool {
// TODO: backtracking CSP with 2 colors
return false
}
func main() {
fmt.Println(twoColorable(3, [][2]int{{0, 1}, {1, 2}})) // true
fmt.Println(twoColorable(3, [][2]int{{0, 1}, {1, 2}, {2, 0}})) // false
}
Starter — Java.
import java.util.*;
public class Task4 {
static boolean twoColorable(int n, int[][] edges) {
// TODO
return false;
}
public static void main(String[] args) {
System.out.println(twoColorable(3, new int[][]{{0, 1}, {1, 2}})); // true
System.out.println(twoColorable(3, new int[][]{{0, 1}, {1, 2}, {2, 0}})); // false
}
}
Starter — Python.
def two_colorable(n, edges):
# TODO: backtracking CSP with 2 colors
return False
if __name__ == "__main__":
print(two_colorable(3, [(0, 1), (1, 2)])) # True
print(two_colorable(3, [(0, 1), (1, 2), (2, 0)])) # False
Intermediate Tasks (3)¶
Task 5 — Graph coloring with MRV + forward checking¶
Problem. Return a proper k-coloring of an undirected graph using backtracking with MRV variable selection and forward checking, or report that none exists.
Input / Output spec. - Inputs: n, k, edges. - Output: a length-n array of colors 0..k-1, or null/None/nil if infeasible.
Constraints. 1 ≤ n ≤ 200, 1 ≤ k ≤ 8. Must use MRV and restore domains on backtrack.
Hint. Use a bitset/boolean array per vertex for the domain; wipeout = no bit set.
Starter — Go.
package main
import "fmt"
func kColor(n, k int, edges [][2]int) ([]int, bool) {
// TODO: MRV + forward checking backtracking
return nil, false
}
func main() {
col, ok := kColor(4, 3, [][2]int{{0, 1}, {1, 2}, {2, 0}, {2, 3}})
fmt.Println(col, ok)
}
Starter — Java.
import java.util.*;
public class Task5 {
static int[] kColor(int n, int k, int[][] edges) {
// TODO: MRV + forward checking; return null if infeasible
return null;
}
public static void main(String[] args) {
System.out.println(Arrays.toString(
kColor(4, 3, new int[][]{{0, 1}, {1, 2}, {2, 0}, {2, 3}})));
}
}
Starter — Python.
def k_color(n, k, edges):
# TODO: MRV + forward checking backtracking
return None
if __name__ == "__main__":
print(k_color(4, 3, [(0, 1), (1, 2), (2, 0), (2, 3)]))
Task 6 — AC-3 with a custom constraint¶
Problem. Implement AC-3 for a binary CSP defined by a predicate allowed(x, vx, y, vy). Return reduced domains, or signal inconsistency on a wipeout. Test on the constraint vx < vy over a small chain.
Input / Output spec. - Inputs: domains (map var → values), neighbors (map var → list), allowed predicate. - Output: reduced domains and a boolean consistency flag.
Constraints. 1 ≤ vars ≤ 500, |domain| ≤ 50. Must re-enqueue arcs correctly after pruning.
Hint. Queue all arcs; on a successful revise, re-enqueue (z, x) for neighbors z ≠ y.
Starter — Go.
package main
import "fmt"
func ac3(domains map[int][]int, neighbors map[int][]int,
allowed func(x, vx, y, vy int) bool) bool {
// TODO: arc-consistency fixpoint; false on wipeout
return true
}
func main() {
domains := map[int][]int{0: {1, 2, 3}, 1: {1, 2, 3}}
neighbors := map[int][]int{0: {1}, 1: {0}}
allowed := func(x, vx, y, vy int) bool {
if x < y {
return vx < vy
}
return vy < vx
}
fmt.Println(ac3(domains, neighbors, allowed), domains)
}
Starter — Java.
import java.util.*;
public class Task6 {
interface Quad { boolean ok(int x, int vx, int y, int vy); }
static boolean ac3(Map<Integer, List<Integer>> domains,
Map<Integer, List<Integer>> neighbors, Quad allowed) {
// TODO
return true;
}
public static void main(String[] args) {
Map<Integer, List<Integer>> domains = new HashMap<>();
domains.put(0, new ArrayList<>(List.of(1, 2, 3)));
domains.put(1, new ArrayList<>(List.of(1, 2, 3)));
Map<Integer, List<Integer>> neighbors = Map.of(0, List.of(1), 1, List.of(0));
Quad allowed = (x, vx, y, vy) -> x < y ? vx < vy : vy < vx;
System.out.println(ac3(domains, neighbors, allowed) + " " + domains);
}
}
Starter — Python.
from collections import deque
def ac3(domains, neighbors, allowed):
# TODO: arc-consistency fixpoint; False on wipeout
return True
if __name__ == "__main__":
domains = {0: [1, 2, 3], 1: [1, 2, 3]}
neighbors = {0: [1], 1: [0]}
allowed = lambda x, vx, y, vy: (vx < vy) if x < y else (vy < vx)
print(ac3(domains, neighbors, allowed), domains)
Task 7 — Cryptarithmetic verifier and solver¶
Problem. Given three words a + b = c, find a digit assignment to distinct letters (no leading zeros) making the equation hold, or report none. Return the mapping.
Input / Output spec. - Inputs: three uppercase words. - Output: map letter → digit, or none.
Constraints. Total distinct letters ≤ 10. Words length ≤ 8.
Hint. Collect distinct letters and leading letters; backtrack over digit assignment; verify the sum at a complete assignment.
Starter — Go.
package main
import "fmt"
func solveCrypto(a, b, c string) (map[byte]int, bool) {
// TODO: assign distinct digits, no leading zeros, check a+b==c
return nil, false
}
func main() {
sol, ok := solveCrypto("SEND", "MORE", "MONEY")
fmt.Println(sol, ok)
}
Starter — Java.
import java.util.*;
public class Task7 {
static Map<Character, Integer> solveCrypto(String a, String b, String c) {
// TODO
return null;
}
public static void main(String[] args) {
System.out.println(solveCrypto("SEND", "MORE", "MONEY"));
}
}
Starter — Python.
def solve_crypto(a, b, c):
# TODO: distinct digits, no leading zeros, a + b == c
return None
if __name__ == "__main__":
print(solve_crypto("SEND", "MORE", "MONEY"))
Advanced Tasks (3)¶
Task 8 — Generic CSP solver with MAC¶
Problem. Build a reusable solver taking variables, domains, neighbors, and a binary constraint predicate; solve with backtracking + MRV + MAC (AC-3 at each node). Demonstrate on an n=8 N-Queens CSP.
Input / Output spec. - Inputs: model components and a constraint callback. - Output: a satisfying assignment, or none.
Constraints. Must snapshot/restore domains per node; must not corrupt state across branches.
Hint. After assigning X=v, set domain[X]={v}, seed AC-3 with arcs (Y, X) for neighbors, and recurse on a domain copy.
Starter — Go.
package main
import "fmt"
func solveMAC(vars []int, domains map[int][]int, neighbors map[int][]int,
ok func(a, va, b, vb int) bool) (map[int]int, bool) {
// TODO: MRV + MAC backtracking
return nil, false
}
func main() {
n := 8
vars := make([]int, n)
domains := map[int][]int{}
neighbors := map[int][]int{}
for r := 0; r < n; r++ {
vars[r] = r
d := make([]int, n)
for c := 0; c < n; c++ {
d[c] = c
}
domains[r] = d
for r2 := 0; r2 < n; r2++ {
if r2 != r {
neighbors[r] = append(neighbors[r], r2)
}
}
}
ok := func(a, ca, b, cb int) bool {
dr := a - b
if dr < 0 {
dr = -dr
}
dc := ca - cb
if dc < 0 {
dc = -dc
}
return ca != cb && dr != dc
}
sol, found := solveMAC(vars, domains, neighbors, ok)
fmt.Println(sol, found)
}
Starter — Java.
import java.util.*;
public class Task8 {
interface Quad { boolean ok(int a, int va, int b, int vb); }
static Map<Integer, Integer> solveMAC(int[] vars, Map<Integer, List<Integer>> domains,
Map<Integer, List<Integer>> neighbors, Quad c) {
// TODO: MRV + MAC
return null;
}
public static void main(String[] args) {
int n = 8;
int[] vars = new int[n];
Map<Integer, List<Integer>> domains = new HashMap<>(), neighbors = new HashMap<>();
for (int r = 0; r < n; r++) {
vars[r] = r;
List<Integer> d = new ArrayList<>();
for (int cc = 0; cc < n; cc++) d.add(cc);
domains.put(r, d);
List<Integer> nb = new ArrayList<>();
for (int r2 = 0; r2 < n; r2++) if (r2 != r) nb.add(r2);
neighbors.put(r, nb);
}
Quad c = (a, ca, b, cb) -> ca != cb && Math.abs(a - b) != Math.abs(ca - cb);
System.out.println(solveMAC(vars, domains, neighbors, c));
}
}
Starter — Python.
def solve_mac(variables, domains, neighbors, ok):
# TODO: MRV + MAC (AC-3 at each node)
return None
if __name__ == "__main__":
n = 8
variables = list(range(n))
domains = {r: list(range(n)) for r in range(n)}
neighbors = {r: [r2 for r2 in range(n) if r2 != r] for r in range(n)}
ok = lambda a, ca, b, cb: ca != cb and abs(a - b) != abs(ca - cb)
print(solve_mac(variables, domains, neighbors, ok))
Task 9 — Sudoku as a CSP¶
Problem. Solve a 9×9 Sudoku modeled as a CSP: 81 cell variables, domains 1..9, AllDifferent over each row, column, and 3×3 box. Use MRV + forward checking (or MAC). Return the completed grid.
Input / Output spec. - Input: 9×9 grid with 0 for blanks. - Output: solved grid, or report unsolvable.
Constraints. Standard Sudoku rules. Should solve typical boards quickly via propagation.
Hint. Precompute each cell's "peers" (the up-to-20 cells sharing a unit) as its neighbors; the constraint is ≠ among peers.
Starter — Go.
package main
import "fmt"
func solveSudoku(grid [9][9]int) ([9][9]int, bool) {
// TODO: CSP with AllDifferent (as != among peers), MRV + forward checking
return grid, false
}
func main() {
var g [9][9]int
// 0 = blank; fill a puzzle here
g[0] = [9]int{5, 3, 0, 0, 7, 0, 0, 0, 0}
out, ok := solveSudoku(g)
fmt.Println(out, ok)
}
Starter — Java.
public class Task9 {
static boolean solveSudoku(int[][] grid) {
// TODO: CSP MRV + forward checking; mutate grid in place
return false;
}
public static void main(String[] args) {
int[][] g = new int[9][9];
g[0] = new int[]{5, 3, 0, 0, 7, 0, 0, 0, 0};
System.out.println(solveSudoku(g));
}
}
Starter — Python.
def solve_sudoku(grid):
# TODO: CSP with AllDifferent peers, MRV + forward checking
return False
if __name__ == "__main__":
g = [[0] * 9 for _ in range(9)]
g[0] = [5, 3, 0, 0, 7, 0, 0, 0, 0]
print(solve_sudoku(g))
Task 10 — Exam scheduling CSP¶
Problem. Schedule m exams into s time slots so that no student has two exams in the same slot. Input: a list of student enrollments (each student lists the exams they take). Two exams sharing any student conflict (a ≠ constraint). Return a slot assignment per exam, or report infeasible.
Input / Output spec. - Inputs: m (exams), s (slots), enrollments (list of exam-id lists per student). - Output: array of slot per exam, or none.
Constraints. 1 ≤ m ≤ 100, 1 ≤ s ≤ 12. This is graph coloring with s colors on the conflict graph.
Hint. Build the conflict graph (edge between exams sharing a student), then run your k-coloring solver with k = s. Use MRV + forward checking.
Starter — Go.
package main
import "fmt"
func scheduleExams(m, s int, enrollments [][]int) ([]int, bool) {
// TODO: build conflict graph, color with s colors
return nil, false
}
func main() {
enroll := [][]int{{0, 1}, {1, 2}, {0, 2}}
sol, ok := scheduleExams(3, 3, enroll)
fmt.Println(sol, ok)
}
Starter — Java.
import java.util.*;
public class Task10 {
static int[] scheduleExams(int m, int s, List<List<Integer>> enrollments) {
// TODO: conflict graph + s-coloring
return null;
}
public static void main(String[] args) {
List<List<Integer>> enroll = List.of(
List.of(0, 1), List.of(1, 2), List.of(0, 2));
System.out.println(Arrays.toString(scheduleExams(3, 3, enroll)));
}
}
Starter — Python.
def schedule_exams(m, s, enrollments):
# TODO: build conflict graph, color with s colors (MRV + forward checking)
return None
if __name__ == "__main__":
enroll = [[0, 1], [1, 2], [0, 2]]
print(schedule_exams(3, 3, enroll))
Evaluation Criteria¶
| Criterion | What to check |
|---|---|
| Correctness | Returned assignment satisfies all constraints (re-validate independently). |
| Completeness | Returns a solution when one exists; returns failure cleanly when none does. |
| Heuristics | MRV (and degree tie-break) actually used where required. |
| Propagation | Forward checking / AC-3 prunes domains and detects wipeouts. |
| State hygiene | Domains and assignments fully restored on backtrack — no cross-branch corruption. |
| Edge cases | Empty domains, isolated variables, unsolvable instances, multiple solutions. |
| Testing | Compared against a brute-force oracle on small inputs. |
Test every solution in Go, Java, and Python. For each task, write at least one solvable case, one unsolvable case, and (where relevant) one case with multiple valid outputs.