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: + *
+ * 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: + *
+ * 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: + *
+ * 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: + *
+ * 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: + *
+ * This propagator implements the algorithm described in: + *
+ * The algorithm performs cost-based filtering by: + *
+ * 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
+ * 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
+ * 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
+ * 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:
+ *
+ * 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:
+ *
+ * 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
+ * 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
+ * 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
+ * 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
+ * The method stops when either:
+ *
+ * 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
+ * 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
+ * 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:
+ *
+ *
+ *
+ * @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.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * @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
+ *
+ *
+ *
+ *
+ *
+ *
+ * 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.
*/
ListlistSize 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.
+ *
+ *
*
- * @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.
+ * 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.
*/
ListlistSize 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.
+ *
+ *
+ *
+ * @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