diff --git a/parsers/src/test/resources/flatzinc/instances.csv b/parsers/src/test/resources/flatzinc/instances.csv index a2ce021225..537dd542a5 100644 --- a/parsers/src/test/resources/flatzinc/instances.csv +++ b/parsers/src/test/resources/flatzinc/instances.csv @@ -20,7 +20,7 @@ 2019,fm3_3.fzn,89,165922,575372,575195 2019,group+u6g1pref1.fzn,14,120,284,257 2019,median_string_dp+p2_10_8-0.fzn,4,34,6207,6200 -2019,mknapsack_global+mknap2-1.fzn,1,7772,5808,5807 +2019,mknapsack_global+mknap2-1.fzn,1,7772,5790,5789 2019,vrp-s4-v2-c3_svrp-v2-c3_det.fzn,8,117,926,911 2019,zephyrus+12_6_6_3.fzn,1,780,62559,62558 2018,oocsp_racks+050_r1.fzn,1,_,9,4 diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/PropKnapsack.java b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/PropKnapsack.java index 6def92cd51..1b022230e4 100644 --- a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/PropKnapsack.java +++ b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/PropKnapsack.java @@ -18,8 +18,29 @@ import org.chocosolver.util.tools.ArrayUtils; /** - * Propagator for the Knapsack constraint - * based on Dantzig-Wolfe relaxation + * Propagator for the 0/1-Knapsack constraint using Dantzig-Wolfe relaxation. + *

+ * This propagator enforces the knapsack constraint by: + *

    + *
  1. Computing the minimum possible weight from the lower bounds of item occurrences.
  2. + *
  3. Updating the total profit lower bound based on the minimum weight configuration.
  4. + *
  5. Using efficiency-based filtering (profit/weight ratio) to prune the search space: + *
  6. If adding all remaining items (sorted by decreasing efficiency) exceeds capacity, + * it computes the maximum achievable profit and updates the total profit upper bound.
  7. + *
  8. If capacity is exhausted, it fails if the profit constraint cannot be satisfied.
  9. + * + * + *
+ *

+ * Note: This propagator assumes that linear constraints maintaining the consistency between + * item occurrences, total weight, and total profit are also posted. Specifically, the following must hold: + *

+ * Without these constraints, the propagator may produce incorrect filtering results. + *

+ * This propagator has linear time complexity per propagation call. * * @author Jean-Guillaume Fages */ @@ -68,8 +89,39 @@ public int getPropagationConditions(int vIdx) { return IntEventType.boundAndInst(); } + /** + * Propagates the knapsack constraint using Dantzig-Wolfe relaxation-based filtering. + *

+ * This method performs the following steps: + *

    + *
  1. Initial computation: Computes the remaining capacity after accounting for + * items at their lower bounds, and calculates the minimum achievable power (profit).
  2. + *
  3. Lower bound update: Updates the total profit lower bound if the computed + * minimum power from lower bounds exceeds the current lower bound.
  4. + *
  5. Feasibility check: Fails if the remaining capacity is negative (i.e., the + * minimum weight configuration exceeds the capacity).
  6. + *
  7. Efficiency-based filtering: Iterates items by decreasing efficiency ratio + * (profit/weight) to compute the maximum achievable profit: + * + *
  8. + *
+ *

+ * The filtering is based on the observation that items sorted by decreasing efficiency + * ratio provide an optimal way to maximize profit within a given capacity. + * + * @param evtmask the event mask that triggered the propagation + * @throws ContradictionException if a contradiction is detected (capacity exceeded or profit bounds inconsistent) + */ @Override public void propagate(int evtmask) throws ContradictionException { + // Step 1: Initial computation + // Compute remaining capacity after placing all items at their lower bounds + // and calculate the minimum power (profit) achievable from the lower bound configuration int remainingCapacity = capacity.getUB(); int maxPower = 0; for (int i = 0; i < n; i++) { @@ -77,22 +129,39 @@ public void propagate(int evtmask) throws ContradictionException { remainingCapacity -= weight[i] * lb; maxPower += energy[i] * lb; } + + // Step 2: Lower bound update + // Update the total profit lower bound if the minimum power from lower bounds is greater if (power.getLB() < maxPower) { power.updateLowerBound(maxPower, this, lcg() ? this.lbounds(power, vars) : Reason.undef()); } + + // Step 3: Feasibility check + // Fail if the remaining capacity is negative (minimum weight exceeds capacity) if (remainingCapacity < 0) { this.fails(lcg() ? this.lbounds(power, vars) : Reason.undef()); } else { + // Step 4: Efficiency-based filtering + // Iterate items by decreasing efficiency ratio (profit/weight) to maximize profit int idx; for (int i = 0; i < n; i++) { assert remainingCapacity >= 0; idx = order[i]; + + // Get the range of possible additional occurrences for this item int range = vars[idx].getUB() - vars[idx].getLB(); if (range > 0) { + // Compute the weight delta if we add all remaining occurrences of this item int delta = weight[idx] * (range); + + // Case 1: All of this item can fit in the remaining capacity if (delta <= remainingCapacity) { + // Add full profit contribution from this item maxPower += energy[idx] * (range); remainingCapacity -= delta; + + // Special case: capacity is now exhausted + // Update upper bound since no more items can be added if (weight[idx] > 0 && remainingCapacity == 0) { if (power.getUB() > maxPower) { power.updateUpperBound(maxPower, this, explain(i)); @@ -100,6 +169,9 @@ public void propagate(int evtmask) throws ContradictionException { return; } } else { + // Case 2: Only part of this item can fit + // Compute maximum profit achievable with remaining capacity + // using the efficiency ratio (profit per unit weight) int deltaPow = (int) Math.ceil((double) remainingCapacity * ratio[idx]); if (power.getUB() > maxPower + deltaPow) { power.updateUpperBound(maxPower + deltaPow, this, explain(i)); @@ -111,6 +183,22 @@ public void propagate(int evtmask) throws ContradictionException { } } + /** + * Generates an explanation for the reason of a bound update or failure. + *

+ * This method constructs a reason object that explains why the profit upper bound was updated + * or why the propagator failed. The explanation is based on the literals representing: + *

+ * This captures the constraint that the current filtering decision is valid given the + * lower bounds of all items and the upper bounds of the items considered so far. + * + * @param i the index up to which items have been considered in the efficiency-based filtering + * @return a Reason object explaining the bound update or failure, or Reason.undef() if + * learning clause generation (lcg) is not enabled + */ private Reason explain(int i) { Reason r = Reason.undef(); if (lcg()) { diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/PropKnapsackKatriel01.java b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/PropKnapsackKatriel01.java index aaa1fc8b85..491251164d 100644 --- a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/PropKnapsackKatriel01.java +++ b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/PropKnapsackKatriel01.java @@ -25,64 +25,206 @@ import java.util.Arrays; /** - * Propagator for the 0/1-Knapsack constraint - * based on Dantzig-Wolfe relaxation trying - * to find forbidden and mandatory items + * Propagator for the 0/1-Knapsack constraint using cost-based filtering. + *

+ * Note: This propagator requires that linear constraints maintaining the consistency between + * item occurrences, total weight, and total profit are also posted. Specifically, the following must hold: + *

+ * Without these constraints, the propagator may produce incorrect filtering results. + *

+ * This propagator implements the algorithm described in: + *

+ *

+ * The algorithm performs cost-based filtering by: + *

    + *
  1. Sorting items by decreasing efficiency (profit/weight ratio), breaking ties in favor of larger weights.
  2. + *
  3. Computing the Dantzig relaxation to find the critical item (first item that would exceed capacity).
  4. + *
  5. Using two specialized search trees: + * + *
  6. + *
  7. Identifying mandatory items (items that must be included in any solution) and + * forbidden items (items that cannot be included in any solution).
  8. + *
+ *

+ * The propagator maintains the following invariant: + * For a given lower bound B on the objective (profit), an item x_i is: + *

+ *

+ * This implementation uses an incremental approach with amortized linear time complexity + * per call after an O(n log n) preprocessing step. * * @author Nicolas PIERRE + * @author Charles Prud'homme */ public class PropKnapsackKatriel01 extends Propagator { + + /** + * State constant indicating that an item has been added to the solution. + * An item in this state is included in every valid solution and its value 0 + * has been removed from the variable domain. + */ static final int ADDED = 1; + + /** + * State constant indicating that an item has been removed from the problem. + * An item in this state cannot be included in any valid solution and its value 1 + * has been removed from the variable domain. + */ static final int REMOVED = -1; + + /** + * State constant indicating that an item's status is not yet determined. + * Items in this state may or may not be included in the optimal solution. + */ static final int NOT_DEFINED = 0; // *********************************************************************************** // VARIABLES // *********************************************************************************** + /** + * Array mapping original item indices to their position in the efficiency-sorted order. + * Items are sorted by decreasing efficiency (profit/weight), with ties broken in favor of larger weights. + */ private final int[] order; + + /** + * Array mapping efficiency-sorted indices back to original item indices. + * This is the inverse of {@link #order}. + */ private final int[] reverseOrder; + + /** + * Number of items in the knapsack problem. + */ private final int n; + + /** + * The capacity variable of the knapsack (maximum allowed weight). + * The upper bound of this variable represents the available capacity. + */ private final IntVar capacity; - private final IntVar power; + + /** + * The total profit variable of the knapsack (profit to maximize). + * The lower bound of this variable represents the minimum required profit. + */ + private final IntVar totalProfit; + + /** + * State of each item: {@link #NOT_DEFINED}, {@link #ADDED}, or {@link #REMOVED}. + * This array tracks which items have been forced into or out of the solution. + */ private final int[] itemState; + + /** + * Search tree for efficiently finding the next item to check based on weight. + * Used to implement the monotonicity property: when processing items in weight order, + * the critical item position increases monotonically. + */ private final ItemFindingSearchTree findingTree; + + /** + * Search tree for computing the maximum weight that can be replaced without violating + * the profit bound. Contains all items sorted by efficiency and stores cumulative weight + * and profit sums in internal nodes. + */ private final ComputingLossWeightTree computingTree; + + /** + * Information about the critical item from the Dantzig relaxation: + * index (position in the tree), profit (relaxed solution profit), + * weight (total weight used), and weightWithoutCriticalItem (weight without the critical item). + */ private Info criticalItemInfos; - // variables to keep track of the change from the original constraint expression - private int usedCapacity; - private int powerCreated; + + // *********************************************************************************** + // STATE VARIABLES + // *********************************************************************************** + + /** + * Total weight of items currently included in the solution. + * This is updated when items are added to the solution via {@link #addItemToSolution}. + */ + private int totalWeight; + + /** + * Total profit of items currently included in the solution. + * This is updated when items are added to the solution via {@link #addItemToSolution}. + */ + private int accumulatedProfit; + + /** + * Flag indicating whether the critical item information needs to be recomputed. + * This is set to true when the world changes (backtrack/restart) or when items are added/removed. + */ private boolean mustRecomputeCriticalInfos; + + /** + * Last known world index, used to detect backtracks. + */ private int lastWorld = -1; + + /** + * Last known backtrack count, used to detect backtracks. + */ private long lastNbOfBacktracks = -1; + + /** + * Last known restart count, used to detect restarts. + */ private long lastNbOfRestarts = -1; // *********************************************************************************** // CONSTRUCTORS // *********************************************************************************** - public PropKnapsackKatriel01(BoolVar[] itemOccurence, IntVar capacity, IntVar power, - int[] weight, int[] energy) { - super(ArrayUtils.append(itemOccurence, new IntVar[]{capacity, power}), PropagatorPriority.QUADRATIC, true); + /** + * Constructs a knapsack propagator using the Katriel et al. algorithm. + * + * @param itemOccurence array of boolean variables indicating whether each item is included (1) or not (0) + * @param capacity the integer variable representing the knapsack capacity (maximum weight) + * @param totalProfit the integer variable representing the knapsack total profit + * @param weights array of item weights + * @param profits array of item profits + */ + public PropKnapsackKatriel01(BoolVar[] itemOccurence, IntVar capacity, IntVar totalProfit, + int[] weights, int[] profits) { + super(ArrayUtils.append(itemOccurence, new IntVar[]{capacity, totalProfit}), PropagatorPriority.QUADRATIC, true); this.n = itemOccurence.length; this.itemState = new int[n]; this.reverseOrder = new int[n]; this.capacity = capacity; - this.power = power; - this.usedCapacity = 0; - this.powerCreated = 0; + this.totalProfit = totalProfit; + this.totalWeight = 0; + this.accumulatedProfit = 0; Arrays.fill(this.itemState, 0); // we find the decreasing order of efficiency this.order = ArrayUtils.array(0, n - 1); ArraySort sorter = new ArraySort<>(n, false, true); sorter.sort(order, n, (i1, i2) -> { // Compares efficiencies decreasingly - long comparaison = (long) energy[i2] * weight[i1] - (long) energy[i1] * weight[i2]; + long comparaison = (long) profits[i2] * weights[i1] - (long) profits[i1] * weights[i2]; if (comparaison == 0) { - if (weight[i1] * weight[i2] == 0) { - comparaison = (long) energy[i2] - energy[i1]; + if (weights[i1] * weights[i2] == 0) { + comparaison = (long) profits[i2] - profits[i1]; } else { // breaking ties in favor of larger weights - comparaison = (long) weight[i2] - weight[i1]; + comparaison = (long) weights[i2] - weights[i1]; } } return Long.signum(comparaison); @@ -90,7 +232,7 @@ public PropKnapsackKatriel01(BoolVar[] itemOccurence, IntVar capacity, IntVar po ArrayList orderedItems = new ArrayList<>(); orderedItems.ensureCapacity(n); for (int i = 0; i < n; ++i) { - orderedItems.add(new KPItem(energy[order[i]], weight[order[i]])); + orderedItems.add(new KPItem(profits[order[i]], weights[order[i]])); reverseOrder[order[i]] = i; } this.findingTree = new ItemFindingSearchTree(orderedItems); @@ -111,29 +253,48 @@ public int getPropagationConditions(int vIdx) { // updates on the max weight return IntEventType.upperBoundAndInst(); } else /* vIdx == n + 1 */ { - // updates on the energy variable + // updates on the total profit variable return IntEventType.lowerBoundAndInst(); } } @Override public void propagate(int evtmask) throws ContradictionException { + if (PropagatorEventType.isFullPropagation(evtmask)) { + // Synchronize trees with current variable states BEFORE computing critical item. + // This is critical because other propagators may have instantiated variables between + // propagator construction and first propagation call. If trees are not synchronized, + // findCriticalItem would compute based on stale data, leading to incorrect filtering. + for (int i = 0; i < n; i++) { + if (this.vars[i].isInstantiatedTo(0) && this.itemState[i] == NOT_DEFINED) { + this.removeItemFromProblem(i, false); + mustRecomputeCriticalInfos = true; + } else if (this.vars[i].isInstantiatedTo(1) && this.itemState[i] == NOT_DEFINED) { + this.addItemToSolution(i, false); + mustRecomputeCriticalInfos = true; + } + } + } + assert checkItemTreeConsistency(); + // Recompute critical item information if needed (after backtrack, restart, or capacity change) if (mustRecomputeCriticalInfos()) { // compute the Dantzig solution and set members variables - this.criticalItemInfos = this.computingTree.findCriticalItem(this.capacity.getUB() - usedCapacity); + this.criticalItemInfos = this.computingTree.findCriticalItem(this.capacity.getUB() - totalWeight); mustRecomputeCriticalInfos = false; } - // if power LB is not reachable so we can't filter + // if total profit LB is not reachable so we can't filter // (every item would be mandatory and forbidden) - if (criticalItemInfos.profit + powerCreated >= power.getLB()) { + if (criticalItemInfos.profit() + accumulatedProfit >= totalProfit.getLB()) { TIntList mandatoryList = findMandatoryItems(); TIntList forbiddenList = findForbiddenItems(); + // Process mandatory items: add them to the solution for (int i = 0; i < mandatoryList.size(); i++) { int unorderedLeafIdx = mandatoryList.get(i); addItemToSolution(unorderedLeafIdx, true); // case 3. from mustRecomputeCriticalInfos() mustRecomputeCriticalInfos = true; } + // Process forbidden items: remove them from the problem for (int i = 0; i < forbiddenList.size(); i++) { int unorderedLeafIdx = forbiddenList.get(i); removeItemFromProblem(unorderedLeafIdx, true); @@ -156,21 +317,31 @@ public void propagate(int varIdx, int mask) throws ContradictionException { } // case 2. from mustRecomputeCriticalInfos() mustRecomputeCriticalInfos |= varIdx < n + 1; - forcePropagate(PropagatorEventType.FULL_PROPAGATION); + forcePropagate(PropagatorEventType.CUSTOM_PROPAGATION); } /** - * we recompute the Dantzig solution if : - * - a backtrack occured - * - capacity UB changed - * - an item has been determined + * Checks if the critical item information needs to be recomputed. + * This happens when: + *

+ * + * @return true if critical item information must be recomputed, false otherwise */ private boolean mustRecomputeCriticalInfos() { checkWorld(); return mustRecomputeCriticalInfos; } + /** + * Checks if the solver's world has changed (due to backtrack or restart). + * If a change is detected, sets the flag to recompute critical item information. + */ private void checkWorld() { int currentworld = model.getEnvironment().getWorldIndex(); long currentbt = model.getSolver().getBackTrackCount(); @@ -204,8 +375,8 @@ private void addItemToSolution(int i, boolean removeVarValue) throws Contradicti computingTree.removeLeaf(sortedGlobalIndex); findingTree.removeLeaf(sortedGlobalIndex); // we update intern values - this.usedCapacity += computingTree.getLeaf(sortedGlobalIndex).getActivatedWeight(); - this.powerCreated += computingTree.getLeaf(sortedGlobalIndex).getActivatedProfit(); + this.totalWeight += computingTree.getLeaf(sortedGlobalIndex).getActivatedWeight(); + this.accumulatedProfit += computingTree.getLeaf(sortedGlobalIndex).getActivatedProfit(); getEnvironment().save(() -> activateItemToProblem(i, ADDED)); if (removeVarValue) { vars[i].removeValue(0, this); @@ -246,8 +417,8 @@ private void activateItemToProblem(int i, int expectedState) { findingTree.activateLeaf(sortedGlobalIndex); if (this.itemState[i] == ADDED) { // we update intern values as the item was added to every solutions - this.usedCapacity -= computingTree.getLeaf(sortedGlobalIndex).getActivatedWeight(); - this.powerCreated -= computingTree.getLeaf(sortedGlobalIndex).getActivatedProfit(); + this.totalWeight -= computingTree.getLeaf(sortedGlobalIndex).getActivatedWeight(); + this.accumulatedProfit -= computingTree.getLeaf(sortedGlobalIndex).getActivatedProfit(); } this.itemState[i] = NOT_DEFINED; } else if (this.itemState[i] != NOT_DEFINED) { @@ -256,75 +427,89 @@ private void activateItemToProblem(int i, int expectedState) { } /** - * exploits {@code computeLimitWeightMandatory} to find all mandatory items in a - * linear scan + * Finds all mandatory items by scanning items to the left of the critical item. + *

+ * An item x_i (i < s, where s is the critical item) is mandatory if removing it + * would make it impossible to reach the lower bound on profit. + * This is determined by checking if the fractional optimum of (X \ {x_i}, C) < B, + * where B is the current lower bound on profit. + *

+ * The method uses the monotonicity property: when processing items in weight order, + * the critical item position increases monotonically, allowing a linear scan. * - * @return indexes (given by the constructor) of mandatory items + * @return list of indices (from the constructor's item list) of mandatory items */ private TIntList findMandatoryItems() { TIntList mandatoryList = new TIntArrayList(); - double allowedProfitLoss = criticalItemInfos.profit + powerCreated - power.getLB(); + double allowedProfitLoss = criticalItemInfos.profit() + accumulatedProfit - totalProfit.getLB(); // finding first active item int index = computingTree.leafToGlobalIndex(0); if (index != -1) { if (!computingTree.getLeaf(index).isActive()) { - index = findingTree.findNextRightItem(index, criticalItemInfos.index, 0); + index = findingTree.findNextRightItem(index, criticalItemInfos.index(), 0); } // not a trivial KP int maxWeight = 0; - double criticalItemWeightNotInDantzig = 0; - if (computingTree.isLeaf(criticalItemInfos.index)) { - criticalItemWeightNotInDantzig = computingTree.getNodeWeight(criticalItemInfos.index) - - (criticalItemInfos.weight - criticalItemInfos.weightWithoutCriticalItem); + double criticalItemRemainingWeight = 0; + if (computingTree.isLeaf(criticalItemInfos.index())) { + criticalItemRemainingWeight = computingTree.getNodeWeight(criticalItemInfos.index()) + - (criticalItemInfos.weight() - criticalItemInfos.weightWithoutCriticalItem()); } - SearchInfos infos = new SearchInfos(false, criticalItemInfos.index, - 0, 0, criticalItemWeightNotInDantzig); + SearchInfos infos = new SearchInfos(false, criticalItemInfos.index(), + 0, 0, criticalItemRemainingWeight); while (index != -1) { - infos = computingTree.computeLimitWeightMandatory(criticalItemInfos, index, infos.endItem, - infos.profitAccumulated, infos.weightAccumulated, allowedProfitLoss, - infos.remainingWeightEndItem); - if (infos.decision) { + infos = computingTree.computeLimitWeightMandatory(criticalItemInfos, index, infos.lastItemIndex(), + infos.accumulatedProfit(), infos.accumulatedWeight(), allowedProfitLoss, + infos.remainingWeight()); + if (infos.decision()) { mandatoryList.add(order[computingTree.globalToLeaf(index)]); } else { maxWeight = Math.max(maxWeight, computingTree.getNodeWeight(index)); - maxWeight = Math.max(maxWeight, (int) infos.weightAccumulated); + maxWeight = Math.max(maxWeight, (int) infos.accumulatedWeight()); } - index = findingTree.findNextRightItem(index, criticalItemInfos.index, maxWeight); + index = findingTree.findNextRightItem(index, criticalItemInfos.index(), maxWeight); } } return mandatoryList; } /** - * exploits {@code computeLimitWeightForbidden} to find all forbidden items in a - * linear scan + * Finds all forbidden items by scanning items to the right of the critical item. + *

+ * An item x_i (i > s, where s is the critical item) is forbidden if including it + * would make it impossible to reach the lower bound on profit. + * This is determined by checking if the fractional optimum of (X \ {x_i}, C - w_i) + p_i < B, + * where B is the current lower bound on profit. + *

+ * The method uses the monotonicity property: when processing items in weight order, + * the critical item position decreases monotonically, allowing a linear scan. * - * @return indexes (given by the constructor) of forbidden items + * @return list of indices (from the constructor's item list) of forbidden items */ private TIntList findForbiddenItems() { TIntList forbiddenList = new TIntArrayList(); - double allowedProfitLoss = criticalItemInfos.profit + powerCreated - power.getLB(); + double allowedProfitLoss = criticalItemInfos.profit() + accumulatedProfit - totalProfit.getLB(); // finding first active item - int index = criticalItemInfos.index; - if (index != -1 && criticalItemInfos.index != computingTree.getNumberNodes()) { + int index = criticalItemInfos.index(); + if (index != -1 && criticalItemInfos.index() != computingTree.getNumberNodes()) { int maxWeight = 0; - double criticalItemWeightInDantzig = criticalItemInfos.weight - criticalItemInfos.weightWithoutCriticalItem; + double criticalItemIncludedWeight = criticalItemInfos.weight() - criticalItemInfos.weightWithoutCriticalItem(); if (!computingTree.getLeaf(index).isActive()) { index = findingTree.findNextRightItem(index, computingTree.getNumberNodes() - 1, maxWeight); } - SearchInfos infos = new SearchInfos(false, criticalItemInfos.index, - 0, 0, criticalItemWeightInDantzig); + SearchInfos infos = new SearchInfos(false, criticalItemInfos.index(), + 0, 0, criticalItemIncludedWeight); while (index != -1) { - infos = computingTree.computeLimitWeightForbidden(criticalItemInfos, index, infos.endItem, - infos.profitAccumulated, infos.weightAccumulated, allowedProfitLoss, - infos.remainingWeightEndItem); - if (infos.decision) { + infos = computingTree.computeLimitWeightForbidden(criticalItemInfos, index, infos.lastItemIndex(), + infos.accumulatedProfit(), infos.accumulatedWeight(), allowedProfitLoss, + infos.remainingWeight()); + if (infos.decision()) { forbiddenList.add(order[computingTree.globalToLeaf(index)]); } else { - maxWeight = Math.max(maxWeight, (int) infos.weightAccumulated); + maxWeight = Math.max(maxWeight, (int) infos.accumulatedWeight()); maxWeight = Math.max(maxWeight, computingTree.getNodeWeight(index)); } index = findingTree.findNextRightItem(index, computingTree.getNumberNodes() - 1, maxWeight); @@ -333,7 +518,109 @@ private TIntList findForbiddenItems() { return forbiddenList; } + /** + * Returns the environment associated with this propagator's model. + * The environment is used for saving restoration callbacks during backtracking. + * + * @return the environment of the model + */ private IEnvironment getEnvironment() { return this.getModel().getEnvironment(); } + + /** + * Checks consistency between item occurrence variables (index < n), their internal state, + * and their presence/absence in the finger trees. + *

+ * This method verifies that: + *

+ *

+ * This is useful for debugging and ensuring the propagator's internal state is consistent + * with the variable domains and tree structures. + * + * @return true if all consistency checks pass, false otherwise + */ + public boolean checkItemTreeConsistency() { + int expectedTotalWeight = 0; + int expectedAccumulatedProfit = 0; + + for (int i = 0; i < n; i++) { + int sortedIndex = reverseOrder[i]; + int computingGlobalIndex = computingTree.leafToGlobalIndex(sortedIndex); + int findingGlobalIndex = findingTree.leafToGlobalIndex(sortedIndex); + + if (computingGlobalIndex == -1 || findingGlobalIndex == -1) { + return false; + } + + boolean computingActive = computingTree.getLeaf(computingGlobalIndex).isActive(); + boolean findingActive = findingTree.getLeaf(findingGlobalIndex).isActive(); + + // Both trees must agree on activation state + if (computingActive != findingActive) { + return false; + } + + if (vars[i].isInstantiatedTo(0)) { + // Variable fixed to 0 + if (itemState[i] != REMOVED) { + return false; + } + if (computingActive) { + return false; + } + } else if (vars[i].isInstantiatedTo(1)) { + // Variable fixed to 1 + if (itemState[i] != ADDED) { + return false; + } + if (computingActive) { + return false; + } + // Accumulate weight and profit for ADDED items + expectedTotalWeight += computingTree.getLeaf(computingGlobalIndex).getActivatedWeight(); + expectedAccumulatedProfit += computingTree.getLeaf(computingGlobalIndex).getActivatedProfit(); + } else { + // Variable is free (contains both 0 and 1) + if (itemState[i] != NOT_DEFINED) { + return false; + } + if (!computingActive) { + return false; + } + } + } + + // Verify totalWeight and accumulatedProfit match the sum of ADDED items + if (totalWeight != expectedTotalWeight) { + return false; + } + if (accumulatedProfit != expectedAccumulatedProfit) { + return false; + } + + return true; + } } diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/BinarySearchFingerTree.java b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/BinarySearchFingerTree.java index 8a73424e26..f95e1f8c8c 100644 --- a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/BinarySearchFingerTree.java +++ b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/BinarySearchFingerTree.java @@ -10,13 +10,38 @@ import java.util.function.Predicate; import java.util.function.Supplier; +/** + * Finger tree with binary search capabilities for knapsack filtering. + *

+ * This class extends {@link FingerTree} with the ability to perform efficient binary searches + * on the tree structure. It maintains aggregate information (weight) at inner nodes to enable + * efficient traversal and querying. + *

+ * The tree supports activation and deactivation of leaf items, with automatic propagation + * of changes up the tree hierarchy. + * + * @author Nicolas PIERRE + */ public class BinarySearchFingerTree extends FingerTree { + /** + * Constructs a binary search finger tree from a list of sorted knapsack items. + * + * @param sortedItems the knapsack items sorted by decreasing efficiency + * @param supplier a supplier function that creates inner nodes of the appropriate type + */ public BinarySearchFingerTree(List sortedItems, Supplier supplier) { super(sortedItems); setupTree(supplier); } + /** + * Returns the weight interface for a node at the given index. + * + * @param index the global index of the node + * @return the weight interface for the node + * @throws IndexOutOfBoundsException if the index is invalid + */ private WeightInterface getNodeWeightInterface(int index) { if (isLeaf(index)) { return getLeaf(index); @@ -28,6 +53,12 @@ private WeightInterface getNodeWeightInterface(int index) { } } + /** + * Returns the weight of a node at the given index. + * + * @param index the global index of the node + * @return the weight of the node, or -1 if the index is invalid + */ public int getNodeWeight(int index) { if (isInnerNode(index) || isLeaf(index)) { return getNodeWeightInterface(index).getWeight(); @@ -51,7 +82,9 @@ private void setupTree(Supplier supplier) { } /** - * deactivate leaf and updates inner nodes + * Deactivates a leaf and updates inner nodes. + * When a leaf is deactivated, it is treated as having zero weight and profit, + * and all ancestor nodes are updated to reflect this change. * * @param leafIndex global leaf index */ @@ -61,7 +94,9 @@ public void removeLeaf(int leafIndex) { } /** - * activate leaf and updates inner nodes + * Activates a leaf and updates inner nodes. + * When a leaf is activated, its weight and profit values are restored, + * and all ancestor nodes are updated to reflect this change. * * @param leafIndex global leaf index */ @@ -71,7 +106,9 @@ public void activateLeaf(int leafIndex) { } /** - * updates inner nodes, when we activate/deactivate a leaf + * Updates inner nodes when a leaf is activated or deactivated. + * This method propagates the change up the tree hierarchy, updating all + * affected inner nodes. * * @param leafIndex global leaf index */ @@ -95,11 +132,11 @@ private void updateTree(int leafIndex) { } /** - * Compute the minimum leaf global index starting from a given node. - * If indexNode has a valid leaf, it will return a valid leaf index - * - * @param indexNode starting node - * @return a global leaf index + * Computes the minimum leaf global index starting from a given node. + * If the starting node is an inner node, traverses to the leftmost leaf. + * + * @param indexNode starting node (global index) + * @return a global leaf index (the leftmost leaf in the subtree) */ private int minLeafIndexFromInnerNode(int indexNode) { int minIdx = indexNode; @@ -110,11 +147,12 @@ private int minLeafIndexFromInnerNode(int indexNode) { } /** - * Compute the maximum leaf global index starting from a given node. - * If indexNode has a valid leaf, it will return a valid leaf index - * - * @param indexNode starting node - * @return a global leaf index + * Computes the maximum leaf global index starting from a given node. + * If the starting node is an inner node, traverses to the rightmost valid leaf. + * Skips subtrees that are marked as inactive. + * + * @param indexNode starting node (global index) + * @return a global leaf index (the rightmost valid leaf in the subtree) */ private int maxLeafIndexFromInnerNode(int indexNode) { int maxIdx = indexNode; @@ -129,16 +167,19 @@ private int maxLeafIndexFromInnerNode(int indexNode) { } /** - * Binary search in the leaves, does not consider "removed" leaves - * all information in () are valid when right=false + * Performs a binary search in the leaves, skipping removed/inactive leaves. + *

+ * When searching right (true), finds the minimum index greater than startIndex where + * predicate is true and index is less than or equal to boundIndex. + * When searching left (false), finds the maximum index less than startIndex where + * predicate is true and index is greater than or equal to boundIndex. * * @param startIndex global starting leaf index * @param boundIndex global index that bounds the search - * @param predicate int -> boolean - * @param right true iff the search is left to right - * @return minimum(maximum) index bigger(smaller) than startingIndex for which - * predicate(index) is true and index is - * smaller(bigger) than or equal to boundIndex. + * @param predicate predicate to test on node indices + * @param right true if searching left to right, false if searching right to left + * @return minimum (maximum) index bigger (smaller) than startingIndex for which + * predicate(index) is true and index is smaller (bigger) than or equal to boundIndex. * -1 if no such index exists. */ public int binarySearch(int startIndex, int boundIndex, Predicate predicate, @@ -193,6 +234,12 @@ public int binarySearch(int startIndex, int boundIndex, Predicate predi } } + /** + * Returns a DOT format string representation of the tree. + * This can be used to visualize the tree structure using Graphviz. + * + * @return a DOT format string + */ public String toString() { StringBuilder str = new StringBuilder("digraph FingerTree{\n"); for (int i = 0; i < getInnerNodeTreeList().size(); ++i) { @@ -215,3 +262,4 @@ public String toString() { return str.toString(); } } + diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/ComputingLossWeightTree.java b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/ComputingLossWeightTree.java index 6833b787d3..0e57c892ad 100644 --- a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/ComputingLossWeightTree.java +++ b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/ComputingLossWeightTree.java @@ -8,17 +8,44 @@ import java.util.List; +/** + * Finger tree specialized for computing weight loss in knapsack filtering. + *

+ * This class extends {@link BinarySearchFingerTree} to support the computation of + * mandatory and forbidden items in the Katriel knapsack filtering algorithm. + * It uses {@link InnerNodeSum} nodes which store the sum of weights and profits + * in each subtree, enabling efficient computation of whether items can be + * determined as mandatory or forbidden based on the Dantzig relaxation solution. + *

+ * The tree provides methods to: + *

+ * + * @author Nicolas PIERRE + */ public class ComputingLossWeightTree extends BinarySearchFingerTree { // used to make mandatory and forbidden test return false on equality public static final double OFFSET = 1e-4; /** - * @param sortedItems sorted items by efficiency ! + * Constructs a computing loss weight tree from a list of sorted knapsack items. + * + * @param sortedItems knapsack items sorted by decreasing efficiency */ public ComputingLossWeightTree(List sortedItems) { super(sortedItems, InnerNodeSum::new); } + /** + * Returns the profit interface for a node at the given index. + * + * @param index the global index of the node + * @return the profit interface for the node + * @throws IndexOutOfBoundsException if the index is invalid + */ private ProfitInterface getNodeProfitInterface(int index) { if (isLeaf(index)) { return getLeaf(index); @@ -32,20 +59,42 @@ private ProfitInterface getNodeProfitInterface(int index) { } } + /** + * Returns the profit of a node at the given index. + * + * @param index the global index of the node + * @return the profit of the node + */ public int getNodeProfit(int index) { return getNodeProfitInterface(index).getProfit(); } + /** + * Checks if the knapsack problem is trivial for a given capacity. + * A problem is trivial if all items can fit in the knapsack. + * + * @param capacity the knapsack capacity + * @return true if the total weight of all items is less than or equal to the capacity + */ public boolean isTrivial(int capacity) { // detect the trivial case where we can put every item in the KP return getNodeWeight(0) <= capacity; } /** - * Computes the index of the critical item and the solutions informations + * Computes the index of the critical item and the Dantzig relaxation solution information. + *

+ * The Dantzig relaxation is computed by greedily selecting items in order of decreasing + * efficiency (profit/weight ratio) until the capacity is reached. The critical item is the + * first item that cannot be fully included without exceeding the capacity. + *

+ * The method traverses the tree, at each inner node deciding whether to go left (if the + * left subtree's weight exceeds remaining capacity) or right (adding the left subtree's + * weight and profit and continuing with the remaining capacity). * - * @param capacity capacity of the KP to consider - * @return Info object with informations of the solution + * @param capacity capacity of the knapsack to consider + * @return Info object containing the critical item index, total profit, weight without + * critical item, and total weight */ public Info findCriticalItem(int capacity) { int remainingCapacity = capacity; @@ -77,47 +126,54 @@ public Info findCriticalItem(int capacity) { } /** - * @param criticalInfos infos about the Dantzig solution - * @param itemIndex global item index to decide on - * @param startingIndex global item index to start the search - * @param profitAccumulated since the beggining of the whole search - * @param weightAccumulated since the beggining of the whole search - * @param allowedProfitLoss delta between optimal solution and profit lower - * bound - * @param startItemWeight weight to consider for the startingIndex item - * @return + * Computes whether an item can be determined as mandatory (must be in any optimal solution). + *

+ * An item is mandatory if excluding it would reduce the profit below the lower bound + * (critical profit minus allowed profit loss). This method performs a search through the tree + * to find the maximum weight of items that can replace the item under consideration, and + * determines if the profit loss would be acceptable. + * + * @param criticalInfos information about the Dantzig relaxation solution + * @param itemIndex global index of the item to check for mandatoriness + * @param startingIndex global index to start the search from + * @param accumulatedProfit profit accumulated during the search so far + * @param accumulatedWeight weight accumulated during the search so far + * @param allowedProfitLoss maximum allowed profit loss from the optimal solution + * @param startItemWeight weight to consider for the starting index item + * @return SearchInfos object containing the decision (true if mandatory), last item index, + * accumulated profit and weight, and remaining weight of the last item */ public SearchInfos computeLimitWeightMandatory(Info criticalInfos, - int itemIndex, int startingIndex, double profitAccumulated, - double weightAccumulated, double allowedProfitLoss, + int itemIndex, int startingIndex, double accumulatedProfit, + double accumulatedWeight, double allowedProfitLoss, double startItemWeight) { assert !isInnerNode(startingIndex); boolean decision = false; - if (criticalInfos.index == getNumberNodes()) { + if (criticalInfos.index() == getNumberNodes()) { // if the KP optimal solution is trivial // we can have every item in the solution decision = getNodeProfit(itemIndex) > allowedProfitLoss + ComputingLossWeightTree.OFFSET; - return new SearchInfos(decision, startingIndex, profitAccumulated, weightAccumulated, startItemWeight); + return new SearchInfos(decision, startingIndex, accumulatedProfit, accumulatedWeight, startItemWeight); } if (getNodeWeight(itemIndex) == 0) { // we just see if removing the profit of the item is allowed, // then it is NOT mandatory decision = getNodeProfit(itemIndex) > allowedProfitLoss + ComputingLossWeightTree.OFFSET; - return new SearchInfos(decision, startingIndex, profitAccumulated, weightAccumulated, startItemWeight); + return new SearchInfos(decision, startingIndex, accumulatedProfit, accumulatedWeight, startItemWeight); } double itemWeight = getNodeWeight(itemIndex); if (!isLeaf(startingIndex)) { // no node left to add - decision = itemWeight * getLeaf(itemIndex).getEfficiency() - profitAccumulated > allowedProfitLoss + decision = itemWeight * getLeaf(itemIndex).getEfficiency() - accumulatedProfit > allowedProfitLoss + ComputingLossWeightTree.OFFSET; - return new SearchInfos(decision, startingIndex, profitAccumulated, weightAccumulated, startItemWeight); + return new SearchInfos(decision, startingIndex, accumulatedProfit, accumulatedWeight, startItemWeight); } int index = startingIndex; double itemEfficiency = getLeaf(itemIndex).getEfficiency(); - double profit = profitAccumulated; - double weight = weightAccumulated; + double profit = accumulatedProfit; + double weight = accumulatedWeight; double nextWeight = startItemWeight; double nextProfit = startItemWeight * getLeaf(startingIndex).getEfficiency(); // we are looking for the node that contains the "exceeding" item @@ -148,7 +204,7 @@ public SearchInfos computeLimitWeightMandatory(Info criticalInfos, index = getRightChild(index); } } - double remainingWeightEndItem = 0; + double remainingWeight = 0; // Special case where we went to the end of the tree and the leaf does not // exists, thus we must give up the rest without additionnal profit if (!isLeaf(index)) { @@ -161,47 +217,54 @@ public SearchInfos computeLimitWeightMandatory(Info criticalInfos, weight += portionWeight; profit += portionWeight * getLeaf(index).getEfficiency(); if (index == startingIndex) { - remainingWeightEndItem = startItemWeight - portionWeight; + remainingWeight = startItemWeight - portionWeight; } else { - remainingWeightEndItem = getNodeWeight(index) - portionWeight; + remainingWeight = getNodeWeight(index) - portionWeight; } decision = weight + ComputingLossWeightTree.OFFSET < itemWeight; if (Math.abs(weight * itemEfficiency - profit - allowedProfitLoss) > 0.01) { throw new RuntimeException("Limit Weight found is not correct"); } } - return new SearchInfos(decision, index, profit, weight, remainingWeightEndItem); + return new SearchInfos(decision, index, profit, weight, remainingWeight); } /** - * @param criticalInfos infos about the Dantzig solution - * @param itemIndex global item index to decide on - * @param startingIndex global item index to start the search - * @param profitAccumulated since the beggining of the whole search - * @param weightAccumulated since the beggining of the whole search - * @param allowedProfitLoss delta between optimal solution and profit lower - * bound - * @param startItemWeight weight to consider for the startingIndex item - * @return + * Computes whether an item can be determined as forbidden (cannot be in any optimal solution). + *

+ * An item is forbidden if including it would require excluding items whose combined profit + * exceeds the allowed profit loss from the optimal solution. This method performs a search + * through the tree to find items that can be excluded to make room for the item under + * consideration, and determines if the profit loss would be too great. + * + * @param criticalInfos information about the Dantzig relaxation solution + * @param itemIndex global index of the item to check for being forbidden + * @param startingIndex global index to start the search from + * @param accumulatedProfit profit accumulated during the search so far + * @param accumulatedWeight weight accumulated during the search so far + * @param allowedProfitLoss maximum allowed profit loss from the optimal solution + * @param startItemWeight weight to consider for the starting index item + * @return SearchInfos object containing the decision (true if forbidden), last item index, + * accumulated profit and weight, and remaining weight of the last item */ public SearchInfos computeLimitWeightForbidden(Info criticalInfos, - int itemIndex, int startingIndex, double profitAccumulated, - double weightAccumulated, double allowedProfitLoss, + int itemIndex, int startingIndex, double accumulatedProfit, + double accumulatedWeight, double allowedProfitLoss, double startItemWeight) { assert !isInnerNode(startingIndex); boolean decision = false; double itemWeight = getNodeWeight(itemIndex); if (!isLeaf(startingIndex)) { // no node left to add - decision = profitAccumulated - itemWeight * getLeaf(itemIndex).getEfficiency() > allowedProfitLoss + decision = accumulatedProfit - itemWeight * getLeaf(itemIndex).getEfficiency() > allowedProfitLoss + ComputingLossWeightTree.OFFSET; - return new SearchInfos(decision, startingIndex, profitAccumulated, weightAccumulated, startItemWeight); + return new SearchInfos(decision, startingIndex, accumulatedProfit, accumulatedWeight, startItemWeight); } int index = startingIndex; double itemEfficiency = getLeaf(itemIndex).getEfficiency(); - double profit = profitAccumulated; - double weight = weightAccumulated; + double profit = accumulatedProfit; + double weight = accumulatedWeight; double nextWeight = startItemWeight; double nextProfit = startItemWeight * getLeaf(startingIndex).getEfficiency(); // we are looking for the node that contains the "exceeding" item @@ -231,7 +294,7 @@ public SearchInfos computeLimitWeightForbidden(Info criticalInfos, index = getLeftChild(index); } } - double remainingWeightEndItem = 0; + double remainingWeight = 0; // Special case where we went to the end of the tree and the leaf does not // exists, thus we must give up the rest without additionnal profit if (!isLeaf(index)) { @@ -244,16 +307,16 @@ public SearchInfos computeLimitWeightForbidden(Info criticalInfos, weight += portionWeight; profit += portionWeight * getLeaf(index).getEfficiency(); if (index == startingIndex) { - remainingWeightEndItem = startItemWeight - portionWeight; + remainingWeight = startItemWeight - portionWeight; } else { - remainingWeightEndItem = getNodeWeight(index) - portionWeight; + remainingWeight = getNodeWeight(index) - portionWeight; } decision = weight + ComputingLossWeightTree.OFFSET < itemWeight; if (Math.abs(profit - weight * itemEfficiency - allowedProfitLoss) > 0.01) { throw new RuntimeException("Limit Weight found is not correct"); } } - return new SearchInfos(decision, index, profit, weight, remainingWeightEndItem); + return new SearchInfos(decision, index, profit, weight, remainingWeight); } } diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/FingerTree.java b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/FingerTree.java index 8ee06f01c8..fa6f2abbc4 100644 --- a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/FingerTree.java +++ b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/FingerTree.java @@ -10,34 +10,88 @@ import java.util.List; /** - * FingerTree + * Generic finger search tree implementation for efficient tree traversal. + *

+ * This class provides a binary tree structure with two key features: + *

    + *
  1. Complete binary tree: All levels are fully filled except possibly the last level.
  2. + *
  3. Finger search: Allows efficient traversal to adjacent nodes using precomputed "finger" links.
  4. + *
+ *

+ * The tree consists of: + *

+ *

+ * The finger search capability allows moving to the next/previous node at the same depth + * in amortized constant time, which is crucial for the efficiency of the Katriel algorithm. + * + * @param the type of inner nodes (e.g., {@link InnerNodeSum}, {@link InnerNodeMaxWeight}) + * @param the type of leaf nodes (e.g., {@link KPItem}) + * @author Nicolas PIERRE */ public class FingerTree { + + /** + * List of inner nodes in the tree, stored in level-order traversal. + * Index 0 is the root, index 1-2 are its children, etc. + */ private ArrayList innerNodeTreeList; + + /** + * List of leaf nodes in the tree, stored in order. + * Leaf indices start after all inner node indices. + */ private ArrayList leafTreeList; + /** + * Returns the list of inner nodes in this tree. + * + * @return the list of inner nodes + */ public ArrayList getInnerNodeTreeList() { return innerNodeTreeList; } + /** + * Returns the list of leaf nodes in this tree. + * + * @return the list of leaf nodes + */ public ArrayList getLeafTreeList() { return leafTreeList; } /** - * @param sortedItems Knapsack items sorted by decreasing efficiency, if - * equality higher weight first + * Constructs a finger tree from a list of sorted items. + * The items should be sorted by decreasing efficiency (profit/weight ratio), + * with ties broken in favor of larger weights. + * + * @param sortedItems knapsack items sorted by decreasing efficiency, with ties broken by larger weight */ public FingerTree(List sortedItems) { init(sortedItems); } + /** + * Constructs an empty finger tree. + * This is used by subclasses that need to customize the initialization. + */ protected FingerTree() { } + /** + * Initializes the tree structure with the given sorted items. + * Creates a complete binary tree with enough inner nodes to hold all leaves. + * The tree is built as a perfect binary tree where all leaves are at the same depth. + * + * @param sortedItems the items to store in the tree leaves + */ protected void init(List sortedItems) { leafTreeList = new ArrayList<>(sortedItems); innerNodeTreeList = new ArrayList<>(); + // Calculate size for a complete binary tree: 2^ceil(log2(n)) - 1 inner nodes for n leaves int innerNodeSize = power2(1 + (int) (Math.log(sortedItems.size()) / Math.log(2))) - 1; innerNodeTreeList.ensureCapacity(innerNodeSize); for (int i = 0; i < innerNodeSize; i++) { @@ -45,14 +99,36 @@ protected void init(List sortedItems) { } } + /** + * Returns the parent index of a leaf node. + * + * @param leafIndex the index of the leaf node + * @return the index of the parent node + */ public int getLeafParentIndex(int leafIndex) { return getLeafParentIndex(leafIndex, true); } + /** + * Returns the parent index of a leaf node with an optional offset. + * The offset accounts for the inner node list size when computing the parent. + * + * @param leafIndex the index of the leaf node (global index) + * @param offset if true, applies an offset for the inner node list size + * @return the index of the parent node + */ public int getLeafParentIndex(int leafIndex, boolean offset) { return Math.floorDiv((offset ? this.innerNodeTreeList.size() : 0) + leafIndex - 1, 2); } + /** + * Returns the parent index of any node (leaf or inner). + * For the root node (index 0), this returns 0. + * + * @param nodeIndex the index of the node (global index) + * @return the index of the parent node + * @throws IllegalArgumentException if the node index is invalid (neither leaf nor inner node) + */ public int getParentIndex(int nodeIndex) { if (nodeIndex != 0 && (isLeaf(nodeIndex) || isInnerNode(nodeIndex))) { return Math.floorDiv(nodeIndex - 1, 2); @@ -62,12 +138,13 @@ public int getParentIndex(int nodeIndex) { } /** - * Finger tree specific search + * Finds the finger neighbor of a node (adjacent node at the same depth). + * This implements the finger search capability that gives the tree its name. + * The neighbor must be at the same depth and adjacent in the tree structure. * - * @param nodeIndex - * @param right true iff going right - * @return the node to the right/left at the same depth of nodeIndex, -1 if it - * is not a valid node + * @param nodeIndex the index of the starting node (global index) + * @param right true to find the right neighbor, false to find the left neighbor + * @return the index of the neighbor node, or -1 if it doesn't exist */ public int getFingerNeighboor(int nodeIndex, boolean right) { // checks if we are the last element to the border @@ -83,14 +160,24 @@ public int getFingerNeighboor(int nodeIndex, boolean right) { } } + /** + * Returns the left child of a node. + * + * @param nodeIndex the index of the parent node + * @return the index of the left child + */ public int getLeftChild(int nodeIndex) { return 2 * nodeIndex + 1; } /** - * @param nodeIndex - * @return index of the node with the same parent as nodeIndex, if it does not - * exists returns nodeIndex + * Returns the brother node (sibling with the same parent). + * For the root node (index 0), returns 0 since it has no sibling. + * For even indices, returns the left sibling (index-1). + * For odd indices, returns the right sibling (index+1) if it exists. + * + * @param nodeIndex the index of a node (global index) + * @return the index of the brother node, or nodeIndex itself if no brother exists */ public int getBrother(int nodeIndex) { // return nodeIndex if there is no brother @@ -109,30 +196,64 @@ public int getBrother(int nodeIndex) { } } + /** + * Returns the right child of a node. + * + * @param nodeIndex the index of the parent node + * @return the index of the right child + */ public int getRightChild(int nodeIndex) { return 2 * nodeIndex + 2; } + /** + * Checks if a given index corresponds to an inner node. + * + * @param index the index to check + * @return true if the index is a valid inner node, false otherwise + */ public boolean isInnerNode(int index) { return index < innerNodeTreeList.size() && index >= 0; } + /** + * Returns the inner node at a given index. + * + * @param index the index of the inner node + * @return the inner node at that index + */ public NodeType getInnerNode(int index) { return innerNodeTreeList.get(index); } + /** + * Checks if a given index corresponds to a leaf node. + * + * @param index the index to check + * @return true if the index is a valid leaf node, false otherwise + */ public boolean isLeaf(int index) { return index >= innerNodeTreeList.size() && index < innerNodeTreeList.size() + leafTreeList.size(); } + /** + * Returns the leaf node at a given global index. + * + * @param index the global index of the leaf (must be a leaf index) + * @return the leaf node at that index + */ public LeafType getLeaf(int index) { return leafTreeList.get(index - innerNodeTreeList.size()); } /** - * @param startingIndex starting node index - * @param right true will search to the right, false to the left - * @return index of the next node to explore, -1 if there is no node found + * Finds the next node to explore in the specified direction. + * This is the core of the finger search: it moves up the tree until it can move + * right/left, then returns the finger neighbor. + * + * @param startingIndex the index of the starting node + * @param right true to search to the right, false to search to the left + * @return the index of the next node to explore, or -1 if none exists */ public int getNextNode(int startingIndex, boolean right) { int index = startingIndex; @@ -146,26 +267,60 @@ public int getNextNode(int startingIndex, boolean right) { } } + /** + * Converts a leaf index (0-based in the leaf list) to a global index. + * + * @param index the leaf index (0 to numLeaves-1) + * @return the global index of the leaf + */ public int leafToGlobalIndex(int index) { return index + getInnerNodeTreeList().size(); } + /** + * Converts a global leaf index to a leaf index (0-based in the leaf list). + * + * @param index the global index of the leaf + * @return the leaf index (0 to numLeaves-1) + */ public int globalToLeaf(int index) { return index - getInnerNodeTreeList().size(); } + /** + * Returns the total number of nodes in the tree (inner nodes + leaves). + * + * @return the total number of nodes + */ public int getNumberNodes() { return getLeafTreeList().size() + getInnerNodeTreeList().size(); } + /** + * Returns the number of leaf nodes in the tree. + * + * @return the number of leaves + */ public int getNumberLeaves() { return getLeafTreeList().size(); } + /** + * Checks if a number is a power of two. + * + * @param x the number to check + * @return true if x is a power of two, false otherwise + */ public static boolean isPowerOfTwo(int x) { return x != 0 && ((x & (x - 1)) == 0); } + /** + * Computes 2 raised to the power of the given exponent. + * + * @param exponant the exponent + * @return 2^exponent + */ public static int power2(int exponant) { return 1 << exponant; } diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/Info.java b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/Info.java index 4aad8d1a3d..5d7711e8a7 100644 --- a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/Info.java +++ b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/Info.java @@ -6,16 +6,31 @@ */ package org.chocosolver.solver.constraints.nary.knapsack.structure; -public class Info { - public final int index; - public final double profit; - public final int weight; - public final int weightWithoutCriticalItem; - - public Info(int index, double profit, int profitWithoutCriticalItem, int weight) { - this.index = index; - this.profit = profit; - this.weight = weight; - this.weightWithoutCriticalItem = profitWithoutCriticalItem; - } +/** + * Information container for the Dantzig relaxation solution of a knapsack problem. + *

+ * This class stores the results of computing the fractional knapsack solution (Dantzig relaxation), + * which includes: + *

+ *

+ * The Dantzig relaxation is computed by greedily selecting items in order of decreasing + * efficiency until the capacity is reached, then including a fraction of the next item. + * + * @param index The index of the critical item in the efficiency-sorted tree. + * The critical item is the first item that cannot be fully included without exceeding the capacity. + * @param profit The total profit of the Dantzig relaxation solution. + * This includes the profit from fully included items plus the fractional profit from the critical item. + * @param weight The total weight used in the Dantzig relaxation solution. + * This equals the knapsack capacity when the solution uses the full capacity. + * @param weightWithoutCriticalItem The weight without the critical item's fractional part. + * This represents the weight of all fully included items before the critical item. + * Note: Despite the parameter name in the constructor, this field stores a weight value, not a profit. + * @author Nicolas PIERRE + */ +public record Info(int index, double profit, int weightWithoutCriticalItem, int weight) { } diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/InnerNode.java b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/InnerNode.java index cfcdf62f69..c12dcdb1ef 100644 --- a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/InnerNode.java +++ b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/InnerNode.java @@ -6,26 +6,71 @@ */ package org.chocosolver.solver.constraints.nary.knapsack.structure; +/** + * Interface for inner nodes in the knapsack search trees. + *

+ * Inner nodes are used to build binary trees that store aggregate information + * (such as total weight and profit) for subsets of items. This allows efficient + * computation of knapsack bounds and critical items. + *

+ * This interface extends {@link WeightInterface} to provide weight information, + * and adds methods for setting up and updating node values from child nodes or items. + * + * @author Nicolas PIERRE + */ public interface InnerNode extends WeightInterface { + /** + * Initializes this node's value to zero (or appropriate initial state). + */ void setup(); + /** + * Updates this node's value by incorporating the values from a child item. + * + * @param item the child item whose values should be incorporated + */ void updateValue(KPItem item); + /** + * Updates this node's value by incorporating the values from a child node. + * + * @param item the child node whose values should be incorporated + */ void updateValue(InnerNode item); + /** + * Sets this node's value from two child items. + * This is a convenience method that calls setup() and then updates with both items. + * + * @param item1 the first child item + * @param item2 the second child item + */ default void setValue(KPItem item1, KPItem item2) { setup(); updateValue(item1); updateValue(item2); } + /** + * Sets this node's value from two child nodes. + * This is a convenience method that calls setup() and then updates with both nodes. + * + * @param item1 the first child node + * @param item2 the second child node + */ default void setValue(InnerNode item1, InnerNode item2) { setup(); updateValue(item1); updateValue(item2); } + /** + * Checks if this node is currently active in the tree. + * + * @return true if active, false otherwise + * @deprecated This method should be removed as it's not consistently used + */ // todo remove this method boolean isActive(); } diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/InnerNodeMaxWeight.java b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/InnerNodeMaxWeight.java index 114d1172e1..ee9d4d1969 100644 --- a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/InnerNodeMaxWeight.java +++ b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/InnerNodeMaxWeight.java @@ -6,27 +6,66 @@ */ package org.chocosolver.solver.constraints.nary.knapsack.structure; +/** + * Inner node implementation that stores the maximum weight in its subtree. + *

+ * This class is used in the {@link ItemFindingSearchTree} to efficiently find + * items with sufficient weight during the filtering process. + * The node maintains the maximum weight among all items in its subtree, + * allowing O(1) access to this information. + * + * @author Nicolas PIERRE + */ public class InnerNodeMaxWeight implements InnerNode { + + /** + * The maximum weight among all items in this subtree. + * Initialized to -1 to indicate an empty/inactive state. + */ private int maxWeight; + /** + * Constructs a new inner node with initial maximum weight set to -1. + */ public InnerNodeMaxWeight() { setup(); } + /** + * Resets this node's maximum weight to -1 (inactive state). + */ public void setup() { this.maxWeight = -1; } + /** + * Updates this node's maximum weight by considering an item's weight. + * Only active items are considered. + * + * @param item the item whose weight should be considered + */ public void updateValue(KPItem item) { if (item.isActive()) { this.maxWeight = Math.max(item.getWeight(), maxWeight); } } + /** + * Returns the maximum weight among all items in this subtree. + * + * @return the maximum weight, or -1 if no items are present + */ public int getWeight() { return maxWeight; } + /** + * Updates this node's maximum weight by considering another node's maximum weight. + * This method only works with other InnerNodeMaxWeight instances. + * + * @param node the node whose maximum weight should be considered + * @throws RuntimeException if the node is not an InnerNodeMaxWeight + */ public void updateValue(InnerNode node) { try { InnerNodeMaxWeight nodeMaxWeight = (InnerNodeMaxWeight) node; @@ -36,10 +75,20 @@ public void updateValue(InnerNode node) { } } + /** + * Checks if this node is currently active (has a valid maximum weight). + * + * @return true if this node has a non-negative maximum weight, false otherwise + */ public boolean isActive() { return maxWeight != -1; } + /** + * Returns a string representation of this node. + * + * @return a string in the format "w=maxWeight" + */ public String toString() { return "w=" + maxWeight; } diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/InnerNodeSum.java b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/InnerNodeSum.java index 6f65c322c2..8b0a9ff3cd 100644 --- a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/InnerNodeSum.java +++ b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/InnerNodeSum.java @@ -6,19 +6,51 @@ */ package org.chocosolver.solver.constraints.nary.knapsack.structure; +/** + * Inner node implementation that stores the sum of weights and profits of its subtree. + *

+ * This class is used in the {@link ComputingLossWeightTree} to maintain cumulative + * weight and profit information for subsets of items, enabling efficient computation + * of knapsack bounds. + *

+ * The node accumulates values from its children, allowing O(1) access to the total + * weight and profit of any subtree. + * + * @author Nicolas PIERRE + */ public class InnerNodeSum implements InnerNode, ProfitInterface { + + /** + * The sum of weights of all items in this subtree. + */ private int sumWeight; + + /** + * The sum of profits of all items in this subtree. + */ private int sumProfit; + /** + * Constructs a new inner node with zero initial values. + */ public InnerNodeSum() { setup(); } + /** + * Resets this node's values to zero. + */ public void setup() { sumWeight = 0; sumProfit = 0; } + /** + * Updates this node's values by adding the weight and profit of an item. + * Only active items contribute to the sums. + * + * @param item the item whose values should be added + */ public void updateValue(KPItem item) { if (item.isActive()) { sumWeight += item.getWeight(); @@ -26,14 +58,31 @@ public void updateValue(KPItem item) { } } + /** + * Returns the total weight of all items in this subtree. + * + * @return the sum of weights + */ public int getWeight() { return sumWeight; } + /** + * Returns the total profit of all items in this subtree. + * + * @return the sum of profits + */ public int getProfit() { return sumProfit; } + /** + * Updates this node's values by adding the weight and profit of another node. + * This method only works with other InnerNodeSum instances. + * + * @param node the node whose values should be added + * @throws RuntimeException if the node is not an InnerNodeSum + */ public void updateValue(InnerNode node) { try { InnerNodeSum nodeSum = (InnerNodeSum) node; @@ -44,10 +93,20 @@ public void updateValue(InnerNode node) { } } + /** + * Checks if this node is currently active (has non-zero weight and profit). + * + * @return true if this node has non-zero values, false otherwise + */ public boolean isActive() { return !(sumProfit == 0 && sumWeight == 0); } + /** + * Returns a string representation of this node. + * + * @return a string in the format "w=weight,p=profit" + */ public String toString() { return "w=" + sumWeight + ",p=" + sumProfit; } diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/ItemFindingSearchTree.java b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/ItemFindingSearchTree.java index 523379c608..58d0f5b5b0 100644 --- a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/ItemFindingSearchTree.java +++ b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/ItemFindingSearchTree.java @@ -8,14 +8,41 @@ import java.util.List; +/** + * Finger tree specialized for finding items with sufficient weight. + *

+ * This class extends {@link BinarySearchFingerTree} to provide efficient search + * for items that meet a minimum weight requirement. It uses {@link InnerNodeMaxWeight} + * nodes which store the maximum weight in each subtree, enabling efficient pruning + * of search branches that cannot contain items meeting the weight criterion. + *

+ * This tree is used in the Katriel algorithm to find the first leaf to the right of a + * starting position that has weight greater than a given value. + * + * @author Nicolas PIERRE + */ public class ItemFindingSearchTree extends BinarySearchFingerTree { + /** + * Constructs an item finding search tree from a list of sorted knapsack items. + * + * @param sortedItems the knapsack items sorted by decreasing efficiency + */ public ItemFindingSearchTree(List sortedItems) { super(sortedItems, InnerNodeMaxWeight::new); } + /** + * Finds the next leaf to the right of startingIndex (up to criticalIndex) with weight greater than the given value. + * + * @param startingIndex the global index to start searching from (exclusive) + * @param criticalIndex the global index that bounds the search (inclusive) + * @param weight the minimum weight that the item must have + * @return the global index of the first leaf to the right with weight > given weight, or -1 if none exists + */ public int findNextRightItem(int startingIndex, int criticalIndex, int weight) { return binarySearch(startingIndex, criticalIndex, i -> weight < getNodeWeight(i), true); } } + diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/KPItem.java b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/KPItem.java index 4b86038d32..faad420f01 100644 --- a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/KPItem.java +++ b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/KPItem.java @@ -7,52 +7,122 @@ package org.chocosolver.solver.constraints.nary.knapsack.structure; /** - * KPItem + * Represents an item in the Knapsack Problem. + *

+ * Each item has a weight and a profit value. Items can be activated or deactivated + * to reflect whether they are currently part of the problem being solved. + * When deactivated, an item's weight and profit are treated as zero in calculations. + *

+ * This class implements both {@link WeightInterface} and {@link ProfitInterface} to allow + * uniform treatment of items and tree nodes in the knapsack algorithms. + * + * @author Nicolas PIERRE */ public class KPItem implements WeightInterface, ProfitInterface { + /** + * The profit value of this item (immutable). + */ private final int profit; + + /** + * The weight value of this item (can be modified). + */ private int weight; + + /** + * Indicates whether this item is currently active in the problem. + * When inactive, the item is treated as having zero weight and profit. + */ private boolean active; + /** + * Constructs a new KPItem with the specified profit and weight. + * + * @param profit the profit value of this item + * @param weight the weight value of this item + */ public KPItem(int profit, int weight) { this.profit = profit; this.weight = weight; this.active = true; } + /** + * Deactivates this item, making it behave as having zero weight and profit. + */ public void deactivate() { active = false; } + /** + * Activates this item, restoring its original weight and profit values. + */ public void activate() { active = true; } + /** + * Returns the profit of this item if active, zero otherwise. + * + * @return the profit value if active, 0 otherwise + */ public int getProfit() { return active ? profit : 0; } + /** + * Returns the original weight of this item, regardless of its active state. + * This is used when the item is added to or removed from the solution. + * + * @return the weight value of this item + */ public int getActivatedWeight() { return weight; } + /** + * Returns the original profit of this item, regardless of its active state. + * This is used when the item is added to or removed from the solution. + * + * @return the profit value of this item + */ public int getActivatedProfit() { return profit; } + /** + * Returns the weight of this item if active, zero otherwise. + * + * @return the weight value if active, 0 otherwise + */ public int getWeight() { return active ? weight : 0; } + /** + * Checks if this item is currently active. + * + * @return true if active, false otherwise + */ public boolean isActive() { return active; } + /** + * Sets the weight of this item. + * + * @param weight the new weight value + */ public void setWeight(int weight) { this.weight = weight; } + /** + * Computes and returns the efficiency (profit/weight ratio) of this item. + * + * @return the efficiency ratio if active and weight > 0, 0 otherwise + */ public double getEfficiency() { return active ? (double) getProfit() / getWeight() : 0; } diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/ProfitInterface.java b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/ProfitInterface.java index c61aa726af..3346413c52 100644 --- a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/ProfitInterface.java +++ b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/ProfitInterface.java @@ -6,6 +6,21 @@ */ package org.chocosolver.solver.constraints.nary.knapsack.structure; +/** + * Interface for objects that have a profit value. + *

+ * This interface is implemented by both {@link KPItem} and tree node classes + * to allow uniform access to profit information in the knapsack algorithms. + * + * @author Nicolas PIERRE + */ public interface ProfitInterface { + + /** + * Returns the profit of this object. + * For inactive items, this should return 0. + * + * @return the profit value + */ int getProfit(); } diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/SearchInfos.java b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/SearchInfos.java index ac59459fc1..ee54e320df 100644 --- a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/SearchInfos.java +++ b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/SearchInfos.java @@ -7,23 +7,29 @@ package org.chocosolver.solver.constraints.nary.knapsack.structure; /** - * class to transmit informations about the last search, useful for - * {@code computeLimitWeightForbidden} and {@code computeLimitWeightMandatory} + * Information container for search results in the knapsack filtering algorithm. + *

+ * This class is used to transmit information between calls to + * {@link ComputingLossWeightTree#computeLimitWeightMandatory} and + * {@link ComputingLossWeightTree#computeLimitWeightForbidden} methods. + * It contains the results of determining whether an item is mandatory or forbidden, + * along with accumulated values used for subsequent computations. + *

+ * All fields are immutable to ensure consistency during the filtering process. + * + * @param decision The decision result: true if the item is mandatory/forbidden, false otherwise. + * @param lastItemIndex The index of the last item processed in the search. + * This is used as the starting point for the next search iteration. + * @param accumulatedProfit The total profit accumulated during the search. + * This represents the profit from items that can be used to replace or compensate + * for the item being checked. + * @param accumulatedWeight The total weight accumulated during the search. + * This represents the weight from items that can be used to replace or compensate + * for the item being checked. + * @param remainingWeight The remaining weight of the last item after partial inclusion. + * This is used for precise calculations when an item is only partially included. + * @author Nicolas PIERRE */ -public class SearchInfos { - public final boolean decision; - public final int endItem; - public final double profitAccumulated; - public final double weightAccumulated; - public final double remainingWeightEndItem; - - public SearchInfos(boolean decision, int endItem, double profitAccumulated, double weightAccumulated, - double remainingWeightEndItem) { - this.decision = decision; - this.endItem = endItem; - this.profitAccumulated = profitAccumulated; - this.weightAccumulated = weightAccumulated; - this.remainingWeightEndItem = remainingWeightEndItem; - } - +public record SearchInfos(boolean decision, int lastItemIndex, double accumulatedProfit, double accumulatedWeight, + double remainingWeight) { } diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/WeightInterface.java b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/WeightInterface.java index ace18d81ea..c016805541 100644 --- a/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/WeightInterface.java +++ b/solver/src/main/java/org/chocosolver/solver/constraints/nary/knapsack/structure/WeightInterface.java @@ -6,6 +6,21 @@ */ package org.chocosolver.solver.constraints.nary.knapsack.structure; +/** + * Interface for objects that have a weight value. + *

+ * This interface is implemented by both {@link KPItem} and tree node classes + * to allow uniform access to weight information in the knapsack algorithms. + * + * @author Nicolas PIERRE + */ public interface WeightInterface { + + /** + * Returns the weight of this object. + * For inactive items, this should return 0. + * + * @return the weight value + */ int getWeight(); } diff --git a/solver/src/main/java/org/chocosolver/solver/search/loop/lns/neighbors/PropagationGuidedNeighborhood.java b/solver/src/main/java/org/chocosolver/solver/search/loop/lns/neighbors/PropagationGuidedNeighborhood.java index 34a5d825a6..ce8b31a84f 100644 --- a/solver/src/main/java/org/chocosolver/solver/search/loop/lns/neighbors/PropagationGuidedNeighborhood.java +++ b/solver/src/main/java/org/chocosolver/solver/search/loop/lns/neighbors/PropagationGuidedNeighborhood.java @@ -17,10 +17,22 @@ import java.util.stream.IntStream; /** - * A Propagation Guided LNS + * A Propagation Guided Large Neighborhood Search (LNS) neighbor. *

* Based on "Propagation Guided Large Neighborhood Search", Perron et al. CP2004. - *
+ *

+ * This implementation selects variables to be part of the fragment (to be frozen) based on + * the impact of constraint propagation. The algorithm maintains a fragment of variables + * that are frozen to their current values. It iteratively selects variables to add to the + * fragment, prioritizing those that cause the most domain reduction when frozen. + *

+ * Variables that cause significant domain reduction in other variables through propagation + * are considered most influential and are prioritized for inclusion in the fragment. + * This creates a dynamic neighborhood that adapts based on constraint propagation effects. + *

+ * This strategy is particularly effective when the constraint propagation provides + * strong guidance on which variables are most influential in reducing the search space. + * For a reverse approach, see {@link ReversePropagationGuidedNeighborhood}. * * @author Charles Prud'homme * @since 08/04/13 @@ -29,61 +41,79 @@ public class PropagationGuidedNeighborhood extends IntNeighbor { /** - * Number of variables + * Number of variables in the neighborhood */ protected final int n; /** - * Domain size of each variable in {@link #variables} + * Current domain size of each variable in {@link #variables} */ protected int[] curDoms; /** - * Domain size of each variable in {@link #variables} before propagation + * Domain size of each variable in {@link #variables} before the current propagation step. + * Used to compute the domain reduction caused by freezing a variable. */ protected int[] befDoms; /** - * Store the modified variables + * Stores the domain reduction (in absolute values) for each variable, + * used to rank variables by their impact on propagation */ protected int[] all; /** - * For randomness + * Random number generator for random variable selection */ protected Random rd; /** - * Intial size of the fragment + * Desired size of the fragment (target logarithmic sum of domain sizes) */ final double desiredSize; /** - * Current size of the fragment + * Current size of the fragment. + * This is dynamically adjusted by {@link #restrictLess()} to increase the neighborhood size + * over time (size *= 1.01 each call). */ double size; /** - * Number of variables modified through propagation to consider while computing the neighbor + * Maximum number of candidate variables to store and consider. + * Only the top listSize variables with the highest domain reduction impact + * are kept as candidates for the next selection. */ int listSize; /** - * Logarithmic cardinality of domains + * Current logarithmic sum of domain sizes of variables in the fragment. + * Used to track progress toward the desired fragment size. + * The loop in {@link #update()} continues while logSum > size. */ double logSum = 0.; /** - * Store the variable elligible for propagation + * List of candidate variable indices eligible for selection. + * Contains variables from the fragment that caused domain reduction when frozen, + * sorted by their impact (highest first) and limited to {@link #listSize} entries. */ List candidates; /** - * Indicate which variables are selected in a fragment + * BitSet indicating which variables are currently in the fragment (to be frozen). + * A bit set to 1 means the variable is in the fragment and will be frozen. */ protected BitSet fragment; /** - * Reference to the model + * Reference to the model containing the variables and constraints */ protected Model mModel; /** - * Create a propagation-guided neighbor for LNS + * Constructs a Propagation Guided LNS neighbor. + *

+ * This neighbor selects variables to be part of the fragment (to be frozen) based on + * the impact of constraint propagation. Variables that cause the most domain reduction + * when frozen are prioritized. * - * @param vars set of variables to consider - * @param desiredSize desired size of the fragment - * @param listSize number of modified variable to store while propagating - * @param seed for randomness + * @param vars the integer variables to consider for the neighborhood + * @param desiredSize the desired size of the fragment (logarithmic sum of domain sizes). + * Note: this is a double value representing a target sum, not a count of variables. + * @param listSize the number of modified variables to store and consider while propagating. + * Variables are ranked by their impact (domain reduction caused) and only the + * top listSize are kept as candidates for the next selection. + * @param seed the seed for the random number generator used when no candidates are available */ public PropagationGuidedNeighborhood(IntVar[] vars, double desiredSize, int listSize, long seed) { super(vars); @@ -97,6 +127,14 @@ public PropagationGuidedNeighborhood(IntVar[] vars, double desiredSize, int list this.fragment = new BitSet(n); } + /** + * Creates a fragment by freezing variables based on propagation guidance. + * Initially computes the logarithmic sum of all variable domain sizes and copies + * current domain sizes to {@link #befDoms}. All variables start in the fragment. + * Then calls {@link #update()} to iteratively select and freeze variables. + * + * @throws ContradictionException if the fragment is trivially infeasible + */ @Override public void fixSomeVariables() throws ContradictionException { logSum = Arrays.stream(variables).mapToDouble(v -> MathUtils.log2(v.getDomainSize())).sum(); @@ -106,9 +144,19 @@ public void fixSomeVariables() throws ContradictionException { } /** - * Create the fragment + * Creates the fragment by iteratively selecting and freezing variables. + * For each selected variable, it freezes the variable to its solution value, + * propagates constraints, and measures the impact on other variables' domains. + * Variables that cause significant domain reduction in others are prioritized for + * inclusion in the fragment. + *

+ * The method stops when either: + *

* - * @throws ContradictionException if the fragment is trivially infeasible + * @throws ContradictionException if propagating the freezing of a variable leads to a contradiction */ protected void update() throws ContradictionException { while (logSum > size && fragment.cardinality() > 0) { @@ -148,7 +196,12 @@ protected void update() throws ContradictionException { } /** - * @return a variable id in {@link #variables} to be part of the fragment + * Selects the next variable to process from the fragment. + * If there are candidate variables (those that caused significant domain reduction when + * frozen), it prioritizes them (selecting from the head of the list). + * Otherwise, it selects a variable randomly from the remaining variables in the fragment. + * + * @return the index of the selected variable in {@link #variables} */ int selectVariable() { int id; @@ -163,23 +216,44 @@ int selectVariable() { return id; } + /** + * Loads the neighborhood state from a solution. + * Resets the current size to the desired size. + * + * @param solution the solution to load from + */ @Override public void loadFromSolution(Solution solution) { super.loadFromSolution(solution); size = desiredSize; } + /** + * Records the current solution. + * Resets the current size to the desired size after recording. + */ @Override public void recordSolution() { super.recordSolution(); size = desiredSize; } + /** + * Restricts the neighborhood less by increasing the fragment size. + * Multiplies the current size by 1.01, allowing the neighborhood to grow + * over time and explore larger fragments. + */ @Override public void restrictLess() { size *= 1.01; } + /** + * Initializes the neighborhood by recording the initial domain sizes of all variables. + * This is called once at the beginning of the search to establish baseline domain sizes + * in {@link #curDoms} and {@link #befDoms} that are used to measure the impact of + * freezing variables during the neighborhood exploration. + */ @Override public void init() { this.curDoms = new int[n]; diff --git a/solver/src/main/java/org/chocosolver/solver/search/loop/lns/neighbors/ReversePropagationGuidedNeighborhood.java b/solver/src/main/java/org/chocosolver/solver/search/loop/lns/neighbors/ReversePropagationGuidedNeighborhood.java index 440f1a9ee3..6f6661229f 100644 --- a/solver/src/main/java/org/chocosolver/solver/search/loop/lns/neighbors/ReversePropagationGuidedNeighborhood.java +++ b/solver/src/main/java/org/chocosolver/solver/search/loop/lns/neighbors/ReversePropagationGuidedNeighborhood.java @@ -17,71 +17,100 @@ import java.util.stream.IntStream; /** - * A Propagation Guided LNS - *

- * Based on "Propagation Guided Large Neighborhood Search", Perron et al. CP2004. - *
+ * A Reverse Propagation Guided Large Neighborhood Search (LNS) neighbor. + *

+ * This implementation works in reverse compared to {@link PropagationGuidedNeighborhood}: + * instead of selecting variables to be part of the fragment (to be frozen), it selects + * variables to NOT be part of the fragment (to be relaxed). The approach is based on + * "Propagation Guided Large Neighborhood Search", Perron et al. CP2004. + *

+ * The algorithm maintains a fragment of variables that are frozen to their current values. + * It iteratively selects variables to remove from the fragment (unfreeze) based on the + * impact of propagation. Variables that cause the most domain reduction when frozen are + * prioritized for removal, creating a dynamic neighborhood that adapts based on constraint + * propagation effects. + *

+ * This strategy can be particularly effective when the constraint propagation provides + * strong guidance on which variables are most influential in reducing the search space. * * @author Charles Prud'homme * @since 08/04/13 */ -public class ReversePropagationGuidedNeighborhood extends IntNeighbor{ +public class ReversePropagationGuidedNeighborhood extends IntNeighbor { /** - * Number of variables + * Number of variables in the neighborhood */ protected final int n; /** - * Domain size of each variable in {@link #variables} + * Initial domain size of each variable in {@link #variables}, + * recorded during initialization */ protected int[] domSiz; /** - * Store the modified variables + * Stores the domain reduction percentage for each variable, + * used to rank variables by their impact on propagation */ protected int[] all; /** - * For randomness + * Random number generator for random variable selection */ protected Random rd; /** - * Intial size of the fragment + * Desired size of the fragment (target logarithmic sum of domain sizes) */ final double desiredSize; /** - * Goal size of the fragment + * Current target size of the fragment (adjusted by epsilon) */ double size; /** - * Number of variables modified through propagation to consider while computing the neighbor + * Maximum number of candidate variables to store and consider. + * Only the top listSize variables with the highest domain reduction impact + * are kept as candidates for the next selection. */ int listSize; /** - * Logarithmic cardinality of domains + * Current logarithmic sum of domain sizes of frozen variables. + * Used to track progress toward the desired fragment size. */ double logSum = 0.; /** - * Restriction parameter + * Adaptive restriction parameter that adjusts the fragment size dynamically. + * It is updated after each call to {@link #fixSomeVariables()} based on the + * actual log-sum achieved, allowing the algorithm to adapt to the problem structure. + * A value greater than 1.0 increases the fragment size, while a value less than 1.0 decreases it. */ private double epsilon = 1.; /** - * Store the variable elligible for propagation + * List of candidate variable indices eligible for selection. + * Contains variables from the fragment that caused domain reduction when frozen, + * sorted by their impact (highest first) and limited to {@link #listSize} entries. */ List candidates; /** - * Indicate which variables are selected in a fragment + * BitSet indicating which variables are currently in the fragment (frozen). + * A bit set to 1 means the variable is frozen to its solution value. */ protected BitSet fragment; /** - * Reference to the model + * Reference to the model containing the variables and constraints */ protected Model mModel; /** - * Create a reverse adaptive neighbor for LNS based on PGLNS, which selects variables to not be part of a fragment - * @param vars variables to consider - * @param desiredSize desired size of the fragment - * @param listSize number of modified variable to store while propagating - * @param seed for randomness + * Constructs a Reverse Propagation Guided LNS neighbor. + *

+ * This neighbor selects variables to NOT be part of the fragment (i.e., to relax/freeze). + * The selection is guided by the impact of constraint propagation: variables that cause + * the most domain reduction when frozen are prioritized. + * + * @param vars the integer variables to consider for the neighborhood + * @param desiredSize the desired size of the fragment (number of variables to freeze) + * @param listSize the number of modified variables to store and consider while propagating. + * Variables are ranked by their impact (domain reduction caused) and only the + * top listSize are kept as candidates for the next selection. + * @param seed the seed for the random number generator used when no candidates are available */ public ReversePropagationGuidedNeighborhood(IntVar[] vars, int desiredSize, int listSize, long seed) { super(vars); @@ -96,6 +125,17 @@ public ReversePropagationGuidedNeighborhood(IntVar[] vars, int desiredSize, int this.fragment = new BitSet(n); } + /** + * Creates a fragment by freezing variables based on reverse propagation guidance. + * Initially, all variables are considered frozen (part of the fragment). + * The method iteratively removes variables from the fragment until the desired size + * is reached or a contradiction is detected. + *

+ * The epsilon parameter is adaptively adjusted based on the actual log-sum of domain + * sizes encountered during the process, allowing the neighborhood size to adapt over time. + * + * @throws ContradictionException if fixing variables leads to a contradiction + */ @Override public void fixSomeVariables() throws ContradictionException { logSum = 0; @@ -104,12 +144,27 @@ public void fixSomeVariables() throws ContradictionException { try { update(); epsilon = (.95 * epsilon) + (.05 * (logSum / size)); - }catch (ContradictionException ce){ + } catch (ContradictionException ce) { epsilon = (.95 * epsilon) + (.05 / size); throw ce; } } + /** + * Updates the fragment by iteratively selecting and removing variables. + * For each selected variable, it temporarily freezes the variable to its solution value, + * propagates constraints, and measures the impact on other variables' domains. + * Variables that cause significant domain reduction in others are prioritized for removal + * from the fragment in subsequent iterations. + *

+ * The method stops when either: + *

+ * + * @throws ContradictionException if propagating the freezing of a variable leads to a contradiction + */ protected void update() throws ContradictionException { while (logSum < size && fragment.cardinality() > 0) { // 1. pick a variable @@ -121,7 +176,10 @@ protected void update() throws ContradictionException { mModel.getSolver().pushTrail(); variables[id].instantiateTo(values[id], Cause.Null); - mModel.getSolver().propagate(); + try { + mModel.getSolver().propagate(); + } catch (ContradictionException ignored) { + } fragment.clear(id); for (int i = 0; i < n; i++) { @@ -156,7 +214,12 @@ protected void update() throws ContradictionException { } /** - * @return a variable id in {@link #variables} to be part of the fragment + * Selects the next variable to process from the fragment. + * If there are candidate variables (those that caused significant domain reduction when + * frozen), it prioritizes them. Otherwise, it selects a variable randomly from the + * remaining variables in the fragment. + * + * @return the index of the selected variable in {@link #variables} */ int selectVariable() { int id; @@ -171,6 +234,11 @@ int selectVariable() { return id; } + /** + * Initializes the neighborhood by recording the initial domain sizes of all variables. + * This is called once at the beginning of the search to establish baseline domain sizes + * that are used to measure the impact of freezing variables during the neighborhood exploration. + */ @Override public void init() { this.domSiz = new int[n]; diff --git a/solver/src/test/java/org/chocosolver/solver/constraints/nary/KnapsackTest.java b/solver/src/test/java/org/chocosolver/solver/constraints/nary/KnapsackTest.java index 04767ce1b1..84addbdd15 100644 --- a/solver/src/test/java/org/chocosolver/solver/constraints/nary/KnapsackTest.java +++ b/solver/src/test/java/org/chocosolver/solver/constraints/nary/KnapsackTest.java @@ -7,17 +7,32 @@ package org.chocosolver.solver.constraints.nary; import org.chocosolver.solver.Model; +import org.chocosolver.solver.Providers; import org.chocosolver.solver.Solver; +import org.chocosolver.solver.constraints.Constraint; +import org.chocosolver.solver.constraints.Propagator; +import org.chocosolver.solver.constraints.nary.knapsack.PropKnapsack; +import org.chocosolver.solver.exception.ContradictionException; import org.chocosolver.solver.search.strategy.Search; import org.chocosolver.solver.search.strategy.selectors.values.IntDomainBest; import org.chocosolver.solver.search.strategy.selectors.values.IntDomainMax; import org.chocosolver.solver.search.strategy.selectors.variables.Largest; +import org.chocosolver.solver.search.strategy.strategy.FullyRandom; import org.chocosolver.solver.variables.BoolVar; import org.chocosolver.solver.variables.IntVar; +import org.chocosolver.util.tools.ArrayUtils; import org.chocosolver.util.tools.MathUtils; import org.testng.Assert; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +import static java.lang.Math.ceil; + /** * @author Jean-Guillaume FAGES (cosling) * @since 05/04/2017. @@ -123,6 +138,527 @@ public void knapsackTest4() { } Assert.assertEquals(p, 28); } + } + + @Test(groups = "10s", timeOut = 600000, dataProvider = "random", dataProviderClass = Providers.class) + @Providers.Arguments(values = {"1", "20", "1"}) + public void testIssue1231Example1(int seed) { + // Example 1: a valid solution is removed + // Two assignments are feasible: [0,1,0] -> weight=1, profit=2 and [1,1,0] -> weight=2, profit=3 + Model model = new Model(); + IntVar[] occurrences = model.intVarArray("item", 3, 0, 1); + IntVar weight = model.intVar("weight", 0, 2); + IntVar profit = model.intVar("profit", 0, 6); + + model.knapsack( + occurrences, + weight, + profit, + new int[]{1, 1, 3}, + new int[]{1, 2, 3} + ).post(); + + model.arithm(occurrences[1], "=", 1).post(); + model.arithm(occurrences[2], "=", 0).post(); + model.arithm(profit, ">=", 2).post(); + Solver solver = model.getSolver(); + + try{ + solver.propagate(); + Assert.assertEquals(occurrences[0].getLB(), 0); + Assert.assertEquals(occurrences[0].getUB(), 1); + Assert.assertEquals(occurrences[1].getLB(), 1); + Assert.assertEquals(occurrences[2].getUB(), 0); + }catch (ContradictionException cex){ + Assert.fail(); + } + + solver.setSearch(new FullyRandom(model.retrieveIntVars(true), seed)); + List solutions = new ArrayList<>(); + while (solver.solve()) { + solutions.add(new int[]{occurrences[0].getValue(), occurrences[1].getValue(), occurrences[2].getValue()}); + } + + // Both solutions should be found + Assert.assertEquals(solutions.size(), 2, "Expected 2 solutions, but found: " + solutions.size()); + + // Check that both expected solutions are present + boolean found010 = false; + boolean found110 = false; + for (int[] sol : solutions) { + if (sol[0] == 0 && sol[1] == 1 && sol[2] == 0) { + found010 = true; + } + if (sol[0] == 1 && sol[1] == 1 && sol[2] == 0) { + found110 = true; + } + } + Assert.assertTrue(found010, "Solution [0,1,0] not found"); + Assert.assertTrue(found110, "Solution [1,1,0] not found"); + } + + @Test(groups = "10s", timeOut = 60000, dataProvider = "random", dataProviderClass = Providers.class) + @Providers.Arguments(values = {"1", "100", "1"}) + public void testIssue1231Example2(int seed) { + // Example 2: wrong optimum with a profit lower bound + // Using the same instance as Example 1, set profit as the objective + Model model = new Model(); + IntVar[] occurrences = model.intVarArray("item", 3, 0, 1); + IntVar weight = model.intVar("weight", 0, 2); + IntVar profit = model.intVar("profit", 0, 6); + + model.knapsack( + occurrences, + weight, + profit, + new int[]{1, 1, 3}, + new int[]{1, 2, 3} + ).post(); + + model.arithm(profit, ">=", 2).post(); + + model.setObjective(Model.MAXIMIZE, profit); + Solver solver = model.getSolver(); + solver.setSearch(new FullyRandom(model.retrieveIntVars(true), seed)); + int best = Integer.MIN_VALUE; + while (solver.solve()) { + best = profit.getValue(); + } + + // The exact optimum is 3 with [1,1,0] + Assert.assertEquals(best, 3, "Expected optimum 3, but got: " + best); + } + + @Test(groups = "10s", timeOut = 60000, dataProvider = "random", dataProviderClass = Providers.class) + @Providers.Arguments(values = {"1", "100", "1"}) + public void testIssue1231Example3(int seed) { + // Example 3: satisfiable model reported UNSAT + Model model = new Model(); + IntVar[] occurrences = model.intVarArray("item", 3, 0, 1); + IntVar weight = model.intVar("weight", 0, 17); + IntVar profit = model.intVar("profit", 0, 29); + + model.knapsack( + occurrences, + weight, + profit, + new int[]{16, 1, 25}, + new int[]{6, 3, 20} + ).post(); + + model.arithm(profit, ">=", 9).post(); + + Solver solver = model.getSolver(); + solver.setSearch(new FullyRandom(model.retrieveIntVars(true), seed)); + boolean solutionFound = false; + int[] foundSolution = null; + + while (solver.solve()) { + solutionFound = true; + foundSolution = new int[]{occurrences[0].getValue(), occurrences[1].getValue(), occurrences[2].getValue()}; + } + + Assert.assertTrue(solutionFound, "Expected to find solution [1,1,0] but no solution found"); + if (foundSolution != null) { + Assert.assertEquals(foundSolution[0], 1, "Expected occurrences[0]=1"); + Assert.assertEquals(foundSolution[1], 1, "Expected occurrences[1]=1"); + Assert.assertEquals(foundSolution[2], 0, "Expected occurrences[2]=0"); + } + } + + + @Test(groups = "10s", timeOut = 60000, dataProvider = "random", dataProviderClass = Providers.class) + @Providers.Arguments(values = {"1", "100", "1"}) + public void testIssue1231Example3ProfitThresholds(int seed) { + // Additional test: different profit thresholds for Example 3 instance + // Expected solution counts: 0->4, 1->3, 2->3, 3->3, 4->2, 5->2, 6->2, 7->1, 8->1, 9->1, 10->0 + int[] weights = {16, 1, 25}; + int[] profits = {6, 3, 20}; + int[] expectedCounts = {4, 3, 3, 3, 2, 2, 2, 1, 1, 1, 0}; + + for (int minProfit = 0; minProfit <= 10; minProfit++) { + Model model = new Model(); + IntVar[] occurrences = model.intVarArray("item", 3, 0, 1); + IntVar weight = model.intVar("weight", 0, 17); + IntVar profit = model.intVar("profit", 0, 29); + + model.knapsack(occurrences, weight, profit, weights, profits).post(); + model.arithm(profit, ">=", minProfit).post(); + + Solver solver = model.getSolver(); + solver.setSearch(new FullyRandom(model.retrieveIntVars(true), seed)); + int solutionCount = 0; + while (solver.solve()) { + solutionCount++; + } + + int expectedCount = expectedCounts[minProfit]; + Assert.assertEquals(solutionCount, expectedCount, + "For minProfit=" + minProfit + ", expected " + expectedCount + " solutions, but found " + solutionCount); + } + } + + @Test(groups = "10s", timeOut = 60000, dataProvider = "random", dataProviderClass = Providers.class) + @Providers.Arguments(values = {"1", "20", "1"}) + public void testIssue1231Example4(int seed) { + // Example 4: wrong optimum without an external profit bound + Model model = new Model(); + IntVar[] occurrences = model.intVarArray("item", 5, 0, 1); + IntVar weight = model.intVar("weight", 0, 14); + IntVar profit = model.intVar("profit", 0, 25); + + model.knapsack( + occurrences, + weight, + profit, + new int[]{1, 6, 7, 9, 5}, + new int[]{1, 6, 8, 9, 1} + ).post(); + + model.setObjective(Model.MAXIMIZE, profit); + model.getSolver().setSearch(Search.randomSearch(occurrences, seed)); + + int best = Integer.MIN_VALUE; + while (model.getSolver().solve()) { + best = profit.getValue(); + } + + // The exact optimum is 15 with [1,1,1,0,0] + Assert.assertEquals(best, 15, "Expected optimum 15, but got: " + best); + } + + // ==================================================================================== + // COMPARISON TESTS: With vs Without PropKnapsackKatriel01 + // ==================================================================================== + + /** + * Creates a knapsack constraint manually with only PropKnapsack (without PropKnapsackKatriel01) + */ + private Constraint createKnapsackWithoutKatriel(Model model, IntVar[] occurrences, IntVar weightSum, IntVar energySum, + int[] weights, int[] energies) { + Constraint scalar1 = model.scalar(occurrences, weights, "=", weightSum); + scalar1.ignore(); + Constraint scalar2 = model.scalar(occurrences, energies, "=", energySum); + scalar2.ignore(); + + return new Constraint( + "KNAPSACK", + ArrayUtils.append( + scalar1.getPropagators(), + scalar2.getPropagators(), + new Propagator[]{new PropKnapsack(occurrences, weightSum, energySum, weights, energies)} + ) + ); + } + + /** + * Generates a random knapsack instance + */ + private void generateKnapsackInstance(Random random, int n, int[] weights, int[] energies, int maxWeight, int maxEnergy) { + for (int i = 0; i < n; i++) { + weights[i] = random.nextInt(maxWeight) + 1; // Weight between 1 and maxWeight + energies[i] = random.nextInt(maxEnergy) + 1; // Profit between 1 and maxEnergy + } + } + + @DataProvider + public Object[][] instances() { + final int N_ITEMS = 10; + final int MAX_WEIGHT = 20; + final int MAX_ENERGY = 50; + final int MAX_CAPACITY = 50; + + int nbInstances = 20; + int seeds = 20; + Object[][] results = new Object[(nbInstances + 1) * seeds][]; + int k = 0; + for (int j = 0; j < seeds; j++) { + results[k++] = new Object[]{3, 2, 6, 2, new int[]{1, 1, 3}, new int[]{1, 2, 3}, j}; + } + + for (int i = 1; i < nbInstances + 1; i++) { + Random random = new Random(i); + int[] weights = new int[N_ITEMS]; + int[] energies = new int[N_ITEMS]; + generateKnapsackInstance(random, N_ITEMS, weights, energies, MAX_WEIGHT, MAX_ENERGY); + for (int j = 0; j < seeds; j++) { + results[k++] = new Object[]{N_ITEMS, MAX_ENERGY / 2, Arrays.stream(energies).sum(), MAX_CAPACITY / 3, weights, energies, j}; + } + } + return results; + } + + + @Test(groups = "10s", timeOut = 60000, dataProvider = "instances") + public void testKnapsackComparisonWithSeed(int N_ITEMS, int MIN_ENERGY, int MAX_ENERGY, int MAX_WEIGHT, int[] weights, int[] energies, int seed) { + // Test comparing solutions with and without PropKnapsackKatriel01 using a specific seed + + // ======================================================================== + // Model WITH PropKnapsackKatriel01 + // ======================================================================== + Model modelWithKatriel = new Model(); + IntVar[] occWithKatriel = modelWithKatriel.intVarArray("occ", N_ITEMS, 0, 1); + IntVar weightWithKatriel = modelWithKatriel.intVar("weight", 0, MAX_WEIGHT); + IntVar profitWithKatriel = modelWithKatriel.intVar("profit", 0, N_ITEMS * MAX_ENERGY); + + modelWithKatriel.knapsack(occWithKatriel, weightWithKatriel, profitWithKatriel, weights, energies).post(); + modelWithKatriel.arithm(profitWithKatriel, ">=", MIN_ENERGY).post(); + modelWithKatriel.setObjective(Model.MAXIMIZE, profitWithKatriel); + + // ======================================================================== + // Model WITHOUT PropKnapsackKatriel01 (only PropKnapsack) + // ======================================================================== + Model modelWithoutKatriel = new Model(); + IntVar[] occWithoutKatriel = modelWithoutKatriel.intVarArray("occ", N_ITEMS, 0, 1); + IntVar weightWithoutKatriel = modelWithoutKatriel.intVar("weight", 0, MAX_WEIGHT); + IntVar profitWithoutKatriel = modelWithoutKatriel.intVar("profit", 0, N_ITEMS * MAX_ENERGY); + + createKnapsackWithoutKatriel(modelWithoutKatriel, occWithoutKatriel, weightWithoutKatriel, + profitWithoutKatriel, weights, energies).post(); + modelWithoutKatriel.arithm(profitWithoutKatriel, ">=", MIN_ENERGY).post(); + modelWithoutKatriel.setObjective(Model.MAXIMIZE, profitWithoutKatriel); + + // ======================================================================== + // Solving and comparison + // ======================================================================== + Solver solverWithKatriel = modelWithKatriel.getSolver(); + Solver solverWithoutKatriel = modelWithoutKatriel.getSolver(); + + // Use the same search strategy for both + solverWithKatriel.setSearch(new FullyRandom(modelWithKatriel.retrieveIntVars(true), seed)); + solverWithoutKatriel.setSearch(new FullyRandom(modelWithoutKatriel.retrieveIntVars(true), seed)); + + List solutionsWithKatriel = new ArrayList<>(); + int bestProfitWithKatriel = Integer.MIN_VALUE; + while (solverWithKatriel.solve()) { + int[] sol = new int[occWithKatriel.length]; + for (int i = 0; i < occWithKatriel.length; i++) { + sol[i] = occWithKatriel[i].getValue(); + } + solutionsWithKatriel.add(sol); + bestProfitWithKatriel = Math.max(bestProfitWithKatriel, profitWithKatriel.getValue()); + } + + List solutionsWithoutKatriel = new ArrayList<>(); + int bestProfitWithoutKatriel = Integer.MIN_VALUE; + while (solverWithoutKatriel.solve()) { + int[] sol = new int[occWithoutKatriel.length]; + for (int i = 0; i < occWithoutKatriel.length; i++) { + sol[i] = occWithoutKatriel[i].getValue(); + } + solutionsWithoutKatriel.add(sol); + bestProfitWithoutKatriel = Math.max(bestProfitWithoutKatriel, profitWithoutKatriel.getValue()); + } + + // ======================================================================== + // Assertions + // ======================================================================== + String msg = String.format("Seed=%d, Items=%d, Weights=%s, Energies=%s\n", + seed, N_ITEMS, java.util.Arrays.toString(weights), java.util.Arrays.toString(energies)); + + // 1. Best profit must be identical + Assert.assertEquals(bestProfitWithKatriel, bestProfitWithoutKatriel, + msg + "Best profit differs: with Katriel=" + bestProfitWithKatriel + + ", without Katriel=" + bestProfitWithoutKatriel); + + // 2. Each solution found with Katriel must be valid (and vice versa) + for (int[] sol : solutionsWithKatriel) { + int totalWeight = 0; + int totalProfit = 0; + for (int i = 0; i < N_ITEMS; i++) { + totalWeight += sol[i] * weights[i]; + totalProfit += sol[i] * energies[i]; + } + Assert.assertTrue(totalWeight <= MAX_WEIGHT, + msg + "Invalid solution with Katriel: weight=" + totalWeight + " > " + MAX_WEIGHT); + } + + for (int[] sol : solutionsWithoutKatriel) { + int totalWeight = 0; + int totalProfit = 0; + for (int i = 0; i < N_ITEMS; i++) { + totalWeight += sol[i] * weights[i]; + totalProfit += sol[i] * energies[i]; + } + Assert.assertTrue(totalWeight <= MAX_WEIGHT, + msg + "Invalid solution without Katriel: weight=" + totalWeight + " > " + MAX_WEIGHT); + } + } + + @Test(groups = "10s", timeOut = 60000, dataProvider = "random", dataProviderClass = Providers.class) + @Providers.Arguments(values = {"10", "100", "1"}) + public void testKnapsackComparisonWithProfitConstraint(int seed) { + // Test comparing solutions with and without PropKnapsackKatriel01 using profit constraint + final int N_ITEMS = 8; + final int MAX_WEIGHT = 20; + final int MAX_ENERGY = 40; + final int MAX_CAPACITY = 50; + + Random random = new Random(seed); + int[] weights = new int[N_ITEMS]; + int[] energies = new int[N_ITEMS]; + generateKnapsackInstance(random, N_ITEMS, weights, energies, MAX_WEIGHT, MAX_ENERGY); + + // Compute maximum possible profit + int maxPossibleProfit = 0; + for (int e : energies) maxPossibleProfit += e; + // Minimum profit constraint: between 30% and 70% of max profit + int minProfit = (int) (0.3 * maxPossibleProfit); + + // Model WITH Katriel + Model modelWith = new Model(); + IntVar[] occWith = modelWith.intVarArray("occ", N_ITEMS, 0, 1); + IntVar weightWith = modelWith.intVar("weight", 0, MAX_CAPACITY); + IntVar profitWith = modelWith.intVar("profit", 0, maxPossibleProfit); + modelWith.knapsack(occWith, weightWith, profitWith, weights, energies).post(); + modelWith.arithm(weightWith, "<=", MAX_CAPACITY).post(); + modelWith.arithm(profitWith, ">=", minProfit).post(); + + // Model WITHOUT Katriel + Model modelWithout = new Model(); + IntVar[] occWithout = modelWithout.intVarArray("occ", N_ITEMS, 0, 1); + IntVar weightWithout = modelWithout.intVar("weight", 0, MAX_CAPACITY); + IntVar profitWithout = modelWithout.intVar("profit", 0, maxPossibleProfit); + createKnapsackWithoutKatriel(modelWithout, occWithout, weightWithout, profitWithout, weights, energies).post(); + modelWithout.arithm(weightWithout, "<=", MAX_CAPACITY).post(); + modelWithout.arithm(profitWithout, ">=", minProfit).post(); + + Solver solverWith = modelWith.getSolver(); + Solver solverWithout = modelWithout.getSolver(); + + List solutionsWith = new ArrayList<>(); + int bestWith = Integer.MIN_VALUE; + while (solverWith.solve()) { + int[] sol = new int[occWith.length]; + for (int i = 0; i < occWith.length; i++) sol[i] = occWith[i].getValue(); + solutionsWith.add(sol); + bestWith = Math.max(bestWith, profitWith.getValue()); + } + + List solutionsWithout = new ArrayList<>(); + int bestWithout = Integer.MIN_VALUE; + while (solverWithout.solve()) { + int[] sol = new int[occWithout.length]; + for (int i = 0; i < occWithout.length; i++) sol[i] = occWithout[i].getValue(); + solutionsWithout.add(sol); + bestWithout = Math.max(bestWithout, profitWithout.getValue()); + } + + String errorMsg = String.format("Seed=%d, MinProfit=%d, Weights=%s, Energies=%s\n", + seed, minProfit, java.util.Arrays.toString(weights), java.util.Arrays.toString(energies)); + + Assert.assertEquals(solutionsWith.size(), solutionsWithout.size(), + errorMsg + "Solution count mismatch: with=" + solutionsWith.size() + ", without=" + solutionsWithout.size()); + Assert.assertEquals(bestWith, bestWithout, + errorMsg + "Best profit mismatch: with=" + bestWith + ", without=" + bestWithout); + } + + @Test(groups = "10s", timeOut = 60000, dataProvider = "random", dataProviderClass = Providers.class) + @Providers.Arguments(values = {"50"}) + public void testKnapsackComparisonMultipleSeeds(int dummy) { + // Test with multiple seeds to cover more cases + final int N_SEEDS = 20; + final int N_ITEMS = 8; + final int MAX_WEIGHT = 15; + final int MAX_ENERGY = 30; + final int MAX_CAPACITY = 40; + + for (int seed = 0; seed < N_SEEDS; seed++) { + Random random = new Random(seed * 12345 + dummy); // Varier les seeds + int[] weights = new int[N_ITEMS]; + int[] energies = new int[N_ITEMS]; + generateKnapsackInstance(random, N_ITEMS, weights, energies, MAX_WEIGHT, MAX_ENERGY); + + // Modèle avec Katriel + Model modelWith = new Model(); + IntVar[] occWith = modelWith.intVarArray("occ", N_ITEMS, 0, 1); + IntVar weightWith = modelWith.intVar("weight", 0, MAX_CAPACITY); + IntVar profitWith = modelWith.intVar("profit", 0, N_ITEMS * MAX_ENERGY); + modelWith.knapsack(occWith, weightWith, profitWith, weights, energies).post(); + modelWith.arithm(weightWith, "<=", MAX_CAPACITY).post(); + + // Modèle sans Katriel + Model modelWithout = new Model(); + IntVar[] occWithout = modelWithout.intVarArray("occ", N_ITEMS, 0, 1); + IntVar weightWithout = modelWithout.intVar("weight", 0, MAX_CAPACITY); + IntVar profitWithout = modelWithout.intVar("profit", 0, N_ITEMS * MAX_ENERGY); + createKnapsackWithoutKatriel(modelWithout, occWithout, weightWithout, profitWithout, weights, energies).post(); + modelWithout.arithm(weightWithout, "<=", MAX_CAPACITY).post(); + + Solver solverWith = modelWith.getSolver(); + Solver solverWithout = modelWithout.getSolver(); + + List solutionsWith = new ArrayList<>(); + int bestWith = Integer.MIN_VALUE; + while (solverWith.solve()) { + int[] sol = new int[occWith.length]; + for (int i = 0; i < occWith.length; i++) { + sol[i] = occWith[i].getValue(); + } + solutionsWith.add(sol); + bestWith = Math.max(bestWith, profitWith.getValue()); + } + + List solutionsWithout = new ArrayList<>(); + int bestWithout = Integer.MIN_VALUE; + while (solverWithout.solve()) { + int[] sol = new int[occWithout.length]; + for (int i = 0; i < occWithout.length; i++) { + sol[i] = occWithout[i].getValue(); + } + solutionsWithout.add(sol); + bestWithout = Math.max(bestWithout, profitWithout.getValue()); + } + + String errorMsg = String.format("Seed=%d, Weights=%s, Energies=%s\n", + seed, java.util.Arrays.toString(weights), java.util.Arrays.toString(energies)); + + Assert.assertEquals(solutionsWith.size(), solutionsWithout.size(), + errorMsg + "Solution count mismatch: with=" + solutionsWith.size() + ", without=" + solutionsWithout.size()); + Assert.assertEquals(bestWith, bestWithout, + errorMsg + "Best profit mismatch: with=" + bestWith + ", without=" + bestWithout); + } + } + + @Test(groups = "10s", timeOut = 600000, dataProvider = "random", dataProviderClass = Providers.class) + @Providers.Arguments(values = {"1", "20", "1"}) + public void testIssue1231Example5(int seed) { + int[] capacities = {99, 1101}; + int[] volumes = {54, 12, 47, 33, 30, 65, 56, 57, 91, 88, 77, 99, 29, 23, 39, 86, 12, 85, 22, 64}; + int[] energies = {38, 57, 69, 90, 79, 89, 28, 70, 38, 71, 46, 41, 49, 43, 36, 68, 92, 33, 84, 90}; + + Model model = new Model(); + int nos = 20; + // occurrence of each item + IntVar[] objects = new IntVar[nos]; + for (int i = 0; i < nos; i++) { + objects[i] = model.intVar("o_" + (i + 1), 0, (int) ceil(capacities[1]*1. / volumes[i]), true); + } + final IntVar profit = model.intVar("power", 0, 8415, true); + IntVar capacity = model.intVar("weight", capacities[0], capacities[1], true); + model.scalar(objects, volumes, "=", capacity).post(); + model.scalar(objects, energies, "=", profit).post(); + model.knapsack(objects, capacity, profit, volumes, energies).post(); + + model.arithm(profit, ">=", 5293).post(); + // 19:0, 8:0, 1:88, 13:0, 16:3, 14:0, 4:0, 12:0, 18:0, + model.arithm(objects[19], "=", 0).post(); + model.arithm(objects[8], "=", 0).post(); + model.arithm(objects[1], "=", 88).post(); + Solver solver = model.getSolver(); + + try{ + solver.propagate(); + Assert.fail(); + }catch (ContradictionException cex){ + } + + solver.setSearch(new FullyRandom(model.retrieveIntVars(true), seed)); + solver.findAllSolutions(); + + // Both solutions should be found + Assert.assertEquals(solver.getSolutionCount(), 0, + "Expected 0 solution, but found: " + solver.getSolutionCount()); } } diff --git a/solver/src/test/java/org/chocosolver/solver/search/loop/LNSTest.java b/solver/src/test/java/org/chocosolver/solver/search/loop/LNSTest.java index e12a50668d..604eae8329 100644 --- a/solver/src/test/java/org/chocosolver/solver/search/loop/LNSTest.java +++ b/solver/src/test/java/org/chocosolver/solver/search/loop/LNSTest.java @@ -30,6 +30,8 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import java.util.Arrays; + import static java.lang.Math.ceil; import static org.chocosolver.solver.search.strategy.Search.domOverWDegSearch; import static org.chocosolver.solver.search.strategy.Search.lastConflict; @@ -62,7 +64,8 @@ private void knapsack20(final int lns) { Solver r = model.getSolver(); r.setSearch(lastConflict(domOverWDegSearch(objects))); - r.limitTime(900); +// r.limitTime(900); + r.limitNode(3000); switch (lns) { case 0: break; @@ -103,7 +106,7 @@ private void knapsack20(final int lns) { // So, the least we can do is to check that a solution is found Assert.assertTrue(r.getSolutionCount() > 0, "No solution found with LNS=" + lns); // We can also check that the weight is not that bad - Assert.assertTrue(bp >= 7900, "Power is too low with LNS=" + lns); + Assert.assertTrue(bp >= 7900, "Power is too low "+bp+" with LNS=" + lns); Assert.assertTrue(bw >= 1090, "Weight is too low with LNS=" + lns); } @@ -301,7 +304,7 @@ public void testPN1() { } int[] coeffs = new int[nodes]; - for (int i = 0; i < nodes; i++) coeffs[i] = 1; + Arrays.fill(coeffs, 1); model.scalar(costmaxs, coeffs, "=", optVar).post(); model.setObjective(Model.MINIMIZE, optVar);