Loading...
Searching...
No Matches
rspp-bap

Solves the resource constrained shortest path problem with idol's branch-and-price algorithm on its Dantzig-Wolfe reformulation using the Boost graph Dijkstra algorithm for the subproblem.

Resource-constrained shortest path problem (RSPP)

Given a graph \(G = (V, A)\) where \(V\) is a set of nodes and \(A\) is a set of arcs, the RSPP can be modeled as

\[\begin{align} \min_x \quad & \sum_{(i,j)\in A} c_{ij} x_{ij} \\ \text{s.t.}\quad & \sum_{j:(i,j)\in A} x_{ij} - \sum_{j:(j,i)\in A} x_{ji} = \begin{cases} 1 & i = s, \\ -1 & i = t, \\ 0 & \text{otherwise}, \end{cases} \quad \text{for all } i \in V, \\ & \sum_{(i,j)\in A} r_{ij} x_{ij} \le C, \\ & x_{ij} \in \{0,1\}, \quad \text{for all } (i,j)\in A. \end{align} \]

Here, \(c_{ij} > 0\) denotes the travel cost of arc \((i,j)\in A\), \(r_{ij} > 0\) its capacity consumption while \(C\) denotes the maximum resource consumption of a path. The binary variable \(x_{ij}\) equals \(1\) if and only if arc \((i,j)\in A\) is part of the path. The goal is to minimize the overall cost of a path.

Branch-and-Price algorithm

Let \(\Omega\) be the set of all feasible paths connecting source \(s\) to sink \(t\) in graph \(G\) that satisfy the flow conservation constraints. For each path \(p \in \Omega\), we define

  • \(c_p = \sum_{(i,j) \in p} c_{ij}\) the total cost of path \(p\),
  • \(r_p = \sum_{(i,j) \in p} r_{ij}\) the total resource consumption of path \(p\).

We introduce a binary decision variable \(\lambda_p\) for each path \(p \in \Omega\), where \(\lambda_p = 1\) if path \(p\) is selected, and \(0\) otherwise. The relationship with the original arc variables is given by \(x_{ij} = \sum_{p \in \Omega} \delta_{ij}^p \lambda_p\), where \(\delta_{ij}^p = 1\) if arc \((i,j)\) belongs to path \(p\), and \(0\) otherwise.

The Master Problem

By replacing the original arc variables with path variables, the master problem is formulated as:

\[\begin{align} \min_{\lambda} \quad & \sum_{p \in \Omega} c_p \lambda_p \\ \text{s.t.} \quad & \sum_{p \in \Omega} r_p \lambda_p \le C, \quad && (\mu \le 0) \\ & \sum_{p \in \Omega} \lambda_p = 1, \quad && (\pi) \\ & \lambda_p \in \{0,1\} \quad \forall p \in \Omega, \end{align} \]

in which \(\mu\) is the dual variable associated with the resource constraint and \(\pi\) is the dual variable associated with the convexity constraint.

Pricing Subproblem

Since the set \(\Omega\) is exponentially large, a restricted master problem (RMP) is solved over a subset of paths \(\bar{\Omega} \subset \Omega\). A path with a negative reduced cost indicates that it can improve the current solution.

The reduced cost \(\bar{c}_p\) of a path \(p\) is expressed using the optimal dual variables \(\mu\) and \(\pi\) from the RMP as

\[\bar{c}_p = c_p - \mu r_p - \pi. \]

Expressing \(c_p\) and \(r_p\) back in terms of the original arcs, we obtain

\[\bar{c}_p = \sum_{(i,j) \in p} (c_{ij} - \mu r_{ij}) - \pi. \]

The pricing subproblem aims to find a path \(p \in \Omega\) that minimizes this reduced cost. Dropping the constant \(\pi\), the optimization subproblem becomes

\[\min_{p \in \Omega} \sum_{(i,j) \in p} (c_{ij} - \mu r_{ij}). \]

Therefore, the pricing subproblem reduces to a standard shortest path problem on graph \(G\) from \(s\) to \(t\) with modified arc weights. In this example, the subproblem will be solved using the Dijkstra's algorithm from Boost Graph library.

Graph definition

We consider a simple instance defined on a directed graph \(G = (V, A)\).

digraph G {
rankdir=LR;
node [shape=circle];
s -> A [label="c=2, r=4"];
s -> B [label="c=3, r=2"];
A -> t [label="c=2, r=3"];
B -> t [label="c=2, r=2"];
A -> B [label="c=1, r=2"];
B -> A [label="c=1, r=2"];
}

The node set is given by

\[V = \{s, A, B, t\} \]

and the arc set is

\[A = \{(s,A), (s,B), (A,t), (B,t), (A,B), (B,A)\}. \]

Each arc \((i,j) \in A\) is associated with a cost \(c_{ij}\) and a resource consumption \(r_{ij}\):

Arc \((i,j)\) Cost \(c_{ij}\) Resource \(r_{ij}\)
\((s,A)\) 2 4
\((s,B)\) 3 2
\((A,t)\) 2 3
\((B,t)\) 2 2
\((A,B)\) 1 2
\((B,A)\) 1 2

The resource capacity is \(C = 6\).

Implementation in idol

//
// Created by Charlotte on 08/07/2026.
//
#include <iostream>
#include "idol/mixed-integer/modeling/variables/Var.h"
#include "idol/mixed-integer/modeling/models/Model.h"
#include "idol/mixed-integer/modeling/objects/Env.h"
#include "idol/mixed-integer/modeling/expressions/operations/operators.h"
#include "idol/mixed-integer/optimizers/wrappers/GLPK/GLPK.h"
#include "idol/mixed-integer/optimizers/dantzig-wolfe/DantzigWolfeDecomposition.h"
#include "idol/mixed-integer/optimizers/branch-and-bound/BranchAndBound.h"
#include "idol/mixed-integer/optimizers/branch-and-bound/node-selection-rules/factories/BestBound.h"
#include "idol/mixed-integer/optimizers/branch-and-bound/branching-rules/factories/MostInfeasible.h"
#include "idol/mixed-integer/optimizers/dantzig-wolfe/infeasibility-strategies/FarkasPricing.h"
#include "idol/general/optimizers/LambdaOptimizer/LambdaOptimizer.h"
#include "idol/general/optimizers/LambdaOptimizer/LambdaContext.h"
#include <boost/graph/adjacency_list.hpp>
#include <boost/graph/dijkstra_shortest_paths.hpp>
using namespace idol;
int main() {
/**********************/
/* Create boost graph */
/**********************/
using Graph = boost::adjacency_list<
boost::vecS,
boost::vecS,
boost::directedS,
boost::no_property,
boost::property<boost::edge_weight_t, double>
>;
using Vertex = boost::graph_traits<Graph>::vertex_descriptor;
using EdgeKey = std::pair<Vertex, Vertex>;
Graph g;
const auto& s = add_vertex(g);
const auto& A = add_vertex(g);
const auto& B = add_vertex(g);
const auto& t = add_vertex(g);
const std::vector<char> vertex_names = {'s', 'A', 'B', 't'};
add_edge(s, A, 2.0, g);
add_edge(s, B, 3.0, g);
add_edge(A, t, 2.0, g);
add_edge(B, t, 2.0, g);
add_edge(A, B, 1.0, g);
add_edge(B, A, 1.0, g);
/**********************/
/* Create idol model */
/**********************/
Env env;
// Create model
Model model(env);
// Add variables
const auto& x = model.add_vars(Dim<1>(boost::num_edges(g)), 0, 1, idol::VarType::Binary, 0., "x");
// Create map from boost edges to idol var
std::map<EdgeKey, Var> edge_to_var;
auto [it_edges, it_edges_end] = boost::edges(g);
for (unsigned int i = 0; it_edges != it_edges_end; ++it_edges, ++i) {
edge_to_var.emplace(EdgeKey{boost::source(*it_edges, g), boost::target(*it_edges, g)}, x[i]);
}
// Create map from idol var to boost edges
std::map<Var, EdgeKey> var_to_edge;
for (const auto& [edge, var] : edge_to_var) {
var_to_edge.emplace(var, edge);
}
// Add flow constraints
for(const auto& v: boost::make_iterator_range(boost::vertices(g))){
LinExpr expr;
for (auto e : boost::make_iterator_range(boost::edges(g))) {
if (boost::target(e,g) == v) {
expr -= edge_to_var.at({boost::source(e,g), boost::target(e,g)});
}
if (boost::source(e,g) == v) {
expr += edge_to_var.at({boost::source(e,g), boost::target(e,g)});
}
}
if(v == s){
model.add_ctr(expr == 1, "flow_out_" + std::to_string(v));
}
else if(v == t){
model.add_ctr(expr == -1, "flow_in_" + std::to_string(v));
}
else{
model.add_ctr(expr == 0, "flow_null_" + std::to_string(v));
}
}
const std::vector<double> resources{4., 2., 3., 2., 2., 2.};
const double capacity = 6.;
// Add resource constraint
model.add_ctr(idol_Sum(k, Range(boost::num_edges(g)), resources[k] * x[k]) <= capacity, "resource");
// Set the objective function
auto weight_map = get(boost::edge_weight, g);
LinExpr obj_expr;
for (const auto& e : boost::make_iterator_range(edges(g))) {
obj_expr += weight_map[e] * edge_to_var.at(EdgeKey{boost::source(e, g), boost::target(e, g)});
}
model.set_obj_expr(obj_expr);
/*****************************************************************/
/* Annotate the model to perform the Dantzig-Wolfe decomposition */
/*****************************************************************/
// Create decomposition annotation
const Annotation decomposition(env, "decomposition", MasterId); // By default, everything remains in the master problem
// Annotate constraints to be moved to the subproblem
for(const auto& ctr : model.ctrs()){
if(ctr.name().rfind("flow_", 0) == 0){
ctr.set(decomposition, 0);
}
}
/**************************************************/
/* Define lambda function for solving subproblems */
/**************************************************/
const auto lambda = [&g, &s, &t, &edge_to_var](LambdaContext& t_ctx){
// get objective function from model
const auto& obj_func = t_ctx.get_model().get_obj_expr().affine().linear();
// update weight map from objective function
auto weight_map = get(boost::edge_weight, g);
for (const auto& edge : boost::make_iterator_range(boost::edges(g))) {
const auto& var = edge_to_var.at(EdgeKey{boost::source(edge, g), boost::target(edge, g)});
boost::put(boost::edge_weight, g, edge, obj_func.get(var));
}
// solve subproblem using Dijkstra shortest path
std::vector<double> dist(num_vertices(g));
std::vector<Vertex> pred(num_vertices(g));
dijkstra_shortest_paths(
g,
s,
boost::weight_map(weight_map)
.predecessor_map(&pred[0])
.distance_map(&dist[0])
);
// build solution path
std::vector<Vertex> path;
for (Vertex v = t; v != s; v = pred[v]) {
path.push_back(v);
}
path.push_back(s);
std::ranges::reverse(path);
// set solution to model
for (unsigned int i = 0; i+1 < path.size(); ++i) {
const auto& [e, found] = boost::edge(path[i], path[i+1], g);
assert(found);
auto const& var = edge_to_var.at(EdgeKey{boost::source(e, g), boost::target(e, g)});
t_ctx.set_var_primal(var, 1.);
}
t_ctx.set_status(Optimal);
const auto best_obj = dist[t] + t_ctx.get_model().get_obj_expr().affine().constant();
t_ctx.set_best_obj(best_obj);
t_ctx.set_best_bound(best_obj);
};
/*****************************************/
/* Build the column generation algorithm */
/*****************************************/
// Create the Dantzig-Wolfe decomposition algorithm
auto column_generation = DantzigWolfeDecomposition(decomposition);
// Use GLPK to solve the master problem
column_generation.with_master_optimizer(GLPK::ContinuousRelaxation());
// The subproblem will be solved using the lambda function
const auto subproblem_specifications = DantzigWolfe::SubProblem().add_optimizer(LambdaOptimizer(lambda));
column_generation.with_default_sub_problem_spec(subproblem_specifications);
// Use the Farkas pricing strategy to generate columns when the master problem is infeasible
column_generation.with_infeasibility_strategy(DantzigWolfe::FarkasPricing());
// Apply branching to the master problem
column_generation.with_hard_branching(false);
// Turn on logs
column_generation.with_logs(true);
/*****************************************/
/* Create the branch-and-bound algorithm */
/*****************************************/
auto branch_and_bound = BranchAndBound();
// Use the most infeasible branching rule
branch_and_bound.with_branching_rule(MostInfeasible());
// Select nodes according to the best bound rule
branch_and_bound.with_node_selection_rule(BestBound());
// Turn on logs
branch_and_bound.with_logs(true);
/*****************************************/
/* Create the branch-and-price algorithm */
/*****************************************/
const auto branch_and_price = branch_and_bound + column_generation;
/************************************/
/* Solve the model by decomposition */
/************************************/
model.use(branch_and_price);
model.optimize();
/************************************/
/* Print solution */
/************************************/
// Analyze the solution status
const auto status = model.get_status();
const auto reason = model.get_reason();
std::cout << "Solution status: " << status << std::endl;
std::cout << "Reason: " << reason << std::endl;
if (status == Optimal || status == Feasible) {
// Get the number of solutions in the solution pool
const auto n_solutions = model.get_n_solutions();
std::cout << "Number of solutions: " << n_solutions << std::endl;
// Print all solutions in the pool
for (unsigned int i = 0 ; i < n_solutions ; ++i) {
model.set_solution_index(i);
std::cout << "Solution " << i << std::endl;
const auto& primals = save_primal(model) ;
for (const auto& [var, value] : primals) {
const auto& edgeKey = var_to_edge[var];
std::cout << vertex_names[edgeKey.first] << " -> " << vertex_names[edgeKey.second] << std::endl;
}
}
}
return 0;
}