.. _maxicp_search: ************** Search ************** MaxiCP provides a flexible, compositional search framework. The core engine is depth-first search (``DFSearch``) with branch-and-bound for optimization. Source of the search package: `org.maxicp.search `_ Depth-First Search with Closures ================================== Following the design of MiniCP :cite:`Michel2021MiniCP`, the search in MaxiCP is defined by a *branching function*: a ``Supplier`` of an array of ``Runnable`` closures, one per child branch. An empty array signals a solution. .. code-block:: java DFSearch search = makeDfs(cp, () -> { CPIntVar qs = selectMin(q, // first-fail: pick the variable qi -> qi.size() > 1, // that is not yet fixed qi -> qi.size()); // with the smallest domain if (qs == null) return EMPTY; // all fixed -> solution found int v = qs.min(); return branch( () -> cp.post(eq(qs, v)), // left branch: assign v () -> cp.post(neq(qs, v)) // right branch: remove v ); }); The ``branch`` method returns a ``Runnable[]``. Each closure calls ``cp.post()`` which registers the constraint *and* immediately runs a full fixed-point computation, propagating all consequences of the decision. An ``InconsistencyException`` causes the search to backtrack. Built-In Heuristics: Variable and Value Selectors =================================================== ``Searches`` offers a compositional alternative to writing branching closures by hand, separating two orthogonal concerns: 1. A **variable selector** (``Supplier``) — returns the next unfixed variable, or ``null`` when all are fixed. 2. A **value selector** (``Function``) — given the variable, returns the value to try first. **Binary branching** — ``heuristicBinary(varSel)`` or ``heuristicBinary(varSel, valSel)`` Left branch assigns ``x = v``; right branch removes it (``x ≠ v``). .. code-block:: java // (a) most concise — built-in first-fail DFSearch s1 = makeDfs(cp, firstFailBinary(q)); // (b) explicit variable selector, default value (min) DFSearch s2 = makeDfs(cp, heuristicBinary(minDomVariableSelector(q))); // (c) explicit variable AND value selectors DFSearch s3 = makeDfs(cp, heuristicBinary(minDomVariableSelector(q), x -> x.min())); **N-ary branching** — ``heuristicNary(varSel)`` creates one child branch per domain value. An overload accepts a scoring function ``h: int -> int`` and sorts values by increasing ``h(v)``. .. code-block:: java // values tried in increasing order DFSearch s4 = makeDfs(cp, heuristicNary(minDomVariableSelector(q))); // values closer to n/2 tried first DFSearch s5 = makeDfs(cp, heuristicNary(minDomVariableSelector(q), v -> Math.abs(v - n / 2))); **Built-in variable selectors** - ``minDomVariableSelector(x)`` — first-fail (smallest domain). - ``staticOrderVariableSelector(x)`` — first unfixed variable in array order. Custom variable selectors are trivial to write: .. code-block:: java Supplier maxDom = () -> selectMin(q, qi -> !qi.isFixed(), qi -> -qi.size()); DFSearch s6 = makeDfs(cp, heuristicBinary(maxDom)); Advanced Value Selection ========================= - ``boundImpactValueSelector(objective)`` — for each candidate value, temporarily assigns it, measures the resulting objective bound, then backtracks. Returns the value that tightens the bound the least — biasing the search towards high-quality subtrees :cite:`fages2017making`. Conflict-Aware Heuristics =========================== - ``lastConflict`` — after a failure, the variable that caused it is retried *before* consulting the fallback variable selector :cite:`lecoutre2009reasoning`. - ``conflictOrderingSearch`` — maintains a conflict counter per variable; the most conflicted variable is selected first :cite:`conflictOrderingSearch`. Scheduling and Sequencing Heuristics ====================================== - ``fds(tasks)`` — **Failure-Directed Search** :cite:`failureDirectedSearch` for interval variables. Very effective for proving optimality; supports optional tasks. Branches on status, start time, and duration. - ``setTimes(tasks)`` — branches on the start time of the task with the smallest slack :cite:`setTimes`. Assumes all tasks are mandatory. - ``rank(tasks)`` — imposes a total order on tasks (assumes mandatory tasks). For sequence variables: ``firstFailBinary(seqVars)`` selects the insertable node with fewest insertion points; ``branchesInsertingNode`` sorts branches by a user-supplied detour cost. Combinators ============ - ``and(b1, b2, ...)`` — composes strategies sequentially; each strategy is activated only when all preceding ones return ``EMPTY``. - ``limitedDiscrepancy(branching, maxDisc)`` — prunes paths with more than ``maxDisc`` right branches (Limited Discrepancy Search) :cite:`harvey1995limited`. Branch-and-Bound Optimization ================================ .. code-block:: java // Minimize with a 60-second time limit SearchStatistics stats = search.optimize( minimize(cost), stats -> stats.timeInMillis() > 60_000); Each time a solution is found, the objective bound is tightened to exclude worse solutions. Large Neighborhood Search (LNS) ================================= LNS iteratively improves an incumbent by fixing a large subset of variables to their current best values (the *freeze* phase) then re-solving the relaxed sub-problem :cite:`shaw1998using`. The example below applies LNS to the Quadratic Assignment Problem (QAP). **Model** .. code-block:: java CPSolver cp = makeSolver(); CPIntVar[] x = makeIntVarArray(cp, n, n); CPIntVar[] weightedDist = new CPIntVar[n * n]; int ind = 0; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) weightedDist[ind++] = mul(element(d, x[i], x[j]), w[i][j]); CPIntVar totCost = sum(weightedDist); Objective obj = cp.minimize(totCost); DFSearch dfs = makeDfs(cp, firstFailBinary(x)); **Tracking the best solution** .. code-block:: java int[] xBest = IntStream.range(0, n).toArray(); // identity permutation dfs.onSolution(() -> { for (int i = 0; i < n; i++) xBest[i] = x[i].min(); System.out.println("objective: " + totCost.min()); }); **LNS loop** .. code-block:: java int nRestarts = 100; int failureLimit = 100; Random rand = new Random(0); for (int i = 0; i < nRestarts; i++) { dfs.optimizeSubjectTo(obj, stats -> stats.numberOfFailures() >= failureLimit, () -> { // Fix 95 % of variables to their best-known value for (int j = 0; j < n; j++) { if (rand.nextInt(100) < 95) cp.post(eq(x[j], xBest[j])); } } ); } After each call to ``optimizeSubjectTo`` the solver automatically restores its state, retracting all temporary fixing constraints. ``xBest`` lives outside the reversible state and always reflects the globally best solution found so far. Full example: `QAPLNS.java (raw) `__ Variable Objective LNS (VOLNS) ================================ Standard LNS applies to well-constrained problems where a feasible solution always exists and the goal is purely to minimise an objective. Many real-world problems are **over-constrained**: a fully feasible solution may not exist, and the goal is to find an assignment that violates as few constraints as possible. **Variable Objective LNS** (VOLNS) :cite:`volns` handles this by reformulating each hard constraint that may be violated as a *soft constraint* with an individual violation variable, then minimising the sum of those violations. The key insight is that tightening the individual violations during the LNS loop drives the search towards globally feasible solutions more effectively than simply minimising the total violation sum alone. The ``MinimizeObjectiveSum`` class encapsulates this strategy. Source: `MinimizeObjectiveSum.java `__ Example: Magic Square ---------------------- The Magic Square problem requires placing the numbers 1 … n² in an n×n grid so that every row, column, and diagonal sums to the same target S = n(n²+1)/2. It is a pure feasibility problem, but for large *n* it is extremely hard to find a feasible solution directly. VOLNS turns it into a minimisation problem by treating each *row* sum constraint as a soft constraint; columns and diagonals remain hard. The violation of row *i* is ``|sum(row_i) − S|``; the total violation is their sum. The search succeeds when the total violation reaches 0. Full source: `MagicSquareVOLNS.java `__ **Step 1 — Model hard and soft constraints** .. code-block:: java int sumResult = n * (n * n + 1) / 2; // Hard: all-different, column sums, diagonal sums cp.post(allDifferent(xFlat)); for (int j = 0; j < n; j++) { CPIntVar[] col = new CPIntVar[n]; for (int i = 0; i < n; i++) col[i] = x[i][j]; cp.post(sum(col, sumResult)); } cp.post(sum(diagonalLeft, sumResult)); cp.post(sum(diagonalRight, sumResult)); // Soft: row sums — violation = |sum(row_i) - sumResult| CPIntVar[] rowViolation = new CPIntVar[n]; for (int i = 0; i < n; i++) rowViolation[i] = abs(minus(sum(x[i]), sumResult)); CPIntVar totalViolation = sum(rowViolation); **Step 2 — Create the VOLNS objective** .. code-block:: java MinimizeObjectiveSum globalObjective = new MinimizeObjectiveSum(rowViolation); ``MinimizeObjectiveSum`` internally wraps each individual ``rowViolation[i]`` and their sum in separate ``IntObjective`` objects. On construction, only the sum objective is *filtered* (i.e., actively used for pruning); the individual terms are tracked but not yet enforced. **Step 3 — Find an initial solution** .. code-block:: java int[] xFlatSol = new int[n * n]; int[] totalViolationSol = new int[]{0}; DFSearch dfs = makeDfs(cp, firstFailBinary(xFlat)); dfs.onSolution(() -> { totalViolationSol[0] = totalViolation.min(); for (int j = 0; j < n * n; j++) xFlatSol[j] = xFlat[j].min(); }); // stop as soon as one feasible solution (or best approximation) is found dfs.optimize(globalObjective, stats -> stats.numberOfSolutions() >= 1); **Step 4 — VOLNS loop** The loop applies three tightening strategies each iteration before re-running ``optimizeSubjectTo``: .. code-block:: java Random random = new Random(42); for (int iter = 0; iter < maxIter && globalObjective.getBound() > 0; iter++) { // (1) Weak-tighten all terms: next solution must ≥ each current bound globalObjective.weakTightenAll(); // (2) Strong-tighten the sum: next solution must strictly improve the total globalObjective.strongTightenSum(); // (3) Strong-tighten the worst individual term: force improvement of the // row that currently contributes the most violation globalObjective.strongTigthenWorseTerm(); double relaxValue = random.nextDouble(); dfs.optimizeSubjectTo(globalObjective, s -> s.numberOfFailures() >= 100, () -> { for (int j = 0; j < n * n; j++) if (random.nextDouble() > relaxValue) cp.post(eq(xFlat[j], xFlatSol[j])); }); } The three tightening modes create a progressive pressure: .. list-table:: :widths: 25 75 :header-rows: 1 * - Method - Effect * - ``weakTightenAll()`` - Every individual violation and the sum must be **at most equal** to their current best values — no term may get worse. * - ``strongTightenSum()`` - The total violation must **strictly decrease** (delta = 1). * - ``strongTigthenWorseTerm()`` - The individual violation that is currently largest must also **strictly decrease**, focusing search on the hardest-to-satisfy row. The combination forces the search to simultaneously avoid global regression, improve the aggregate quality, and attack the most violated constraint. As iterations proceed, more rows reach violation 0 and the surviving violated rows are progressively tightened until the total violation reaches 0 — a valid magic square.