Recursive Problem Implementetion
We now present how to implement a recursive model of a discrete optimization problem. As our running example, we will use the knapsack problem provided in the Examples\KnapsackInstance\ folder.
1. Setting Up the Enviroment
Create a Python file for the new class (e.g., KnapsackProblem.py) in your project directory.
Create C++ files for the new class (e.g., KnapsackProblem.cpp and KnapsackProblem.h) in your project directory.
2. Import Dependencies
Make sure you import the required modules and classes. Here's an example import section:
3. Create problem class and implement functions
We first create a class that inherits from AbstractProblem and implement its constructor. The constructor receives an instance object (KnapsackStructure in Python, KnapsackInstance in C++) that holds the parsed problem data, plus a sort flag. All problem classes that inherit from AbstractProblem must forward at least the initial state and the list of variables to the base constructor; all other fields are problem-specific, such as the weights and capacity of the knapsack.
Note that the state of a knapsack node is a single scalar (the current load of the knapsack), so the template type of AbstractProblem is int in C++.
class KnapsackProblem(AbstractProblem):
def __init__(self, params: 'KnapsackStructure', sort: bool = True):
super().__init__(params.initial_state, params.variables, sort)
self.weights: list[int] = params.weights
self.capacity: int = params.right_side_of_restrictions
assert len(self.weights) == len(params.variables), \
"weights and variables must have the same length"
class KnapsackProblem : public AbstractProblem<int> {
public:
KnapsackProblem(const KnapsackInstance& params, bool sort = true)
: AbstractProblem(params.initial_state, params.variables),
weights(params.weights),
capacity(params.right_side_of_restrictions)
{
assert(params.variables.size() == weights.size()
&& "weights and variables must have the same length");
}
private:
vector<int> weights;
int capacity;
};
C++ only — variable ordering requires a call in the derived constructor
The example KnapsackProblem keeps the default identity variable order. If you want a custom ordering, you must place an apply_variable_order(sort_variables(variables)) call inside the derived class constructor (guarded by the sort flag). It cannot go in AbstractProblem's constructor for two reasons specific to C++:
- No virtual dispatch during base construction. When
AbstractProblem's constructor runs, the derived-class vtable is not yet active. Callingsort_variables()there would always invoke the base-class version, ignoring any override you define. - Template instantiation.
AbstractProblemis a template whose implementation lives in a.tppfile. The compiler instantiates template bodies at compile time; without the call in the derived constructor, the overriddensort_variablesis never compiled for that concrete type.
The bool sort parameter is conventionally passed through the derived constructor so callers can opt in or out of reordering at construction time.
Reindex order-dependent parameters after sorting
When sort=True, the variable order inside the DD differs from the original input order. AbstractProblem provides a public field dd_to_original_index that maps each DD layer position i to the corresponding original variable index. Any parameter that depends on variable position — such as objective weights — must be reindexed before use:
Your sort_variables() override must update dd_to_original_index to reflect the chosen order. The base class initializes it as the identity [0, 1, ..., n-1]. The built-in create_and_solve_dd helper applies this reindexing automatically for the objective weights — see the Heuristics page for the full explanation.
3.1. Transition function
- Function Purpose: Computes the transition to a new state based on the previous state, a variable index, and its assigned value.
- Variables:
previous_stateis the current state,variable_indexidentifies the variable (its position in the DD layer),variable_valueis the value assigned to that variable, andscratch_stateis a pre-allocated buffer that the function fills with the resulting state. - Return Value: Returns a boolean indicating whether the transition is feasible. The new state is written into
scratch_stateinstead of being returned, which avoids allocating a new object on every transition. - Code example for knapsack: The state is a single scalar holding the current load of the knapsack. The transition adds the item weight when the variable is set to 1 and checks that the load does not exceed the capacity.
def transition_function(self,
previous_state: 'State',
variable_index: int,
variable_value: int,
scratch_state: list) -> bool:
if variable_value == 0:
scratch_state[0] = previous_state
return True
new_val = previous_state + self.weights[variable_index] * variable_value
scratch_state[0] = new_val
return new_val <= self.capacity
bool KnapsackProblem::transition_function(
const int* previous_state,
const int variable_index,
int variable_value,
int* scratch_state) const {
*scratch_state = *previous_state;
if (variable_value != 0) {
*scratch_state += weights[variable_index] * variable_value;
return *scratch_state <= capacity;
}
return true;
}
3.2. Priority for discard node function
- Function Purpose: Determines the priority of discarding a node based on its state. This function is needed to create a Restricted DD.
- Return Value: Returns an integer priority; lower-priority nodes are discarded first.
- Code example for knapsack: In this case, we prioritize states with higher knapsack load; we prefer to discard nodes with lower knapsack load. Since the state is the load itself, we simply return its negation.
3.3. Create priority for merge node function
- Function Purpose: Calculates the priority for merging nodes based on an ID and state. This function is needed to create a Relaxed DD.
- Variables:
node_idis the node's identifier, andstateis its current state. - Return Value: Returns an integer priority; lower-priority nodes are merged first.
- Code example for knapsack: We prefer to merge nodes with lower knapsack load, so we return the negation of the load (the state).
3.4. Create merge operator function
- Function Purpose: Defines how to merge two states (
state_oneandstate_two). This function is needed to create a Relaxed DD. - Return Value: Returns the merged state.
- Code example for knapsack: The merged node keeps the minimum load of the two merged nodes, which yields a valid relaxation (upper bound) for the knapsack.
3.5. Implement get as string function
- Function Purpose: Converts a state (
state) into its string representation. - Return Value: Returns the string representation of
state. - Code example for knapsack:
3.6. Implement get state copy function
Description:
- Function Purpose : Creates a copy of a state
- Return Value : Returns a copy of a state
- Code example for knapsack:
Python — scalar states are immutable
Because the knapsack state is an immutable int, returning it directly is safe. For mutable states (e.g., a list), return an actual copy such as state.copy().