Group Seat Reservation Knapsack Problem (GSR-KP)

Packing

Problem

We define the Group Seat Reservation Knapsack Problem (GSR-KP) as follows. We consider a train with a fixed number of seats and stations to visit. From a set of reservation requests, we must select a subset of requests to accept. Each request specifies a group size and a travel segment (starting station to end station). For each group, the seats assigned must be adjacent. The number of occupied seats must not exceed train seat capacity at any point along the route.

The objective is to maximize the overall train utilization, measured as the total number of seats reserved across the entire trip.

Principles learned

Data

The instances we provide come from Clausen et al. The format of the data files is as follows:

  • First line: number of reservation requests
  • Second line:
    • Total number of stations traveled
    • Train seat capacity
  • Next lines, for each request:
    • Number of stations traveled
    • Number of seats
    • Starting station
    • (Two other unused metrics)

Model

The Hexaly model for the Group Seat Reservation Knapsack Problem (GSR-KP) uses optional interval decision variables to represent the contiguous seats allocated to each request. We use the presence operator to detect whether a request is selected.

For each selected request, the length of the corresponding interval is equal to the number of seats required.

We ensure that any two selected requests that share part of the trip are assigned non-overlapping seats.

The objective is to maximize the overall train utilization. The model computes a simple upper bound on the optimal utilization and uses the hxObjectiveThreshold parameter to stop the search when this bound is reached.

Execution
hexaly group_seat_reservation_kp.hxm inFileName=instances/CGCUT01_gen_0.txt [hxTimeLimit=] [solFileName=]
// Copyright (c) Hexaly. Permission is hereby granted to use, copy,
// and modify this code for applications developed with Hexaly.
use io;

/* Read instance data */
function input() {
    local usage = "Usage: hexaly group_seat_reservation_kp.hxm "
            + "inFileName=inputFile [solFileName=solFileName] [hxTimeLimit=timeLimit]";

    if (inFileName == nil) throw usage;
    local inFile = io.openRead(inFileName);

    nbRequests = inFile.readInt();
    nbStations = inFile.readInt();
    nbSeats = inFile.readInt();

    for [j in 0...nbRequests] {
        nbStationsRequest[j] = inFile.readInt();
        nbSeatsRequest[j] = inFile.readInt();
        firstStationRequest[j] = inFile.readInt();
        inFile.readInt(); // skip value
        inFile.readInt(); // skip value
    }

    // Requests that share at least one station
    for [i in 0...nbRequests] {
        sharingStation[i] = {};
        for [j in (i + 1)...nbRequests] {
            local areSharingStation = firstStationRequest[i] < firstStationRequest[j] + nbStationsRequest[j]
                    && firstStationRequest[i] + nbStationsRequest[i] > firstStationRequest[j];
            if (areSharingStation) {
                sharingStation[i].add(j);
            }
        }
    }
    inFile.close();
}

/* Declare the optimization model */
function model() {
    // OptionalInterval: requestSelected[i] represents the seats occupied by the reservation i
    requestSelected[i in 0...nbRequests] <- optionalInterval(0, nbSeats);

    // If request i is selected then we allocate the corresponding number of seats
    for [i in 0...nbRequests] {
        constraint presence(requestSelected[i])
                ? length(requestSelected[i]) == nbSeatsRequest[i]
                : true;
    }

    // If two requests overlap on any station segment, they must not use the same seats
    for [i in 0...nbRequests] {
        for [j in sharingStation[i]] {
            constraint presence(requestSelected[i]) && presence(requestSelected[j])
                    ? requestSelected[j] < requestSelected[i] || requestSelected[i] < requestSelected[j]
                    : true;
        }
    }

    // Compute the utilization of the train
    utilization <- sum[i in 0...nbRequests]( presence(requestSelected[i])
            * nbSeatsRequest[i] * nbStationsRequest[i] );

    // Maximize the utilization of the train
    maximize utilization;
}

/* Parameterize the solver */
function param() {
    if (hxTimeLimit == nil) hxTimeLimit = 20;

    // Stop the search if the upper threshold is reached
    hxObjectiveThreshold = nbSeats * nbStations;
}

/* Write the solution in a file using the following format:
   - 1st line: total utilization of the train
   - For each selected request: which seats are occupied */
function output() {
    if (solFileName == nil) return ;
    local solFile = io.openWrite(solFileName);
    solFile.println(utilization.value);
    for [i in 0...nbRequests] {
        local seats = requestSelected[i].value;
        if (!seats.isVoid()) {
            solFile.println(i, ": [", seats.start, "...", seats.end - 1, "]");
        }
    }
    solFile.close();
}
Execution (Windows)
set PYTHONPATH=%HX_HOME%\bin\python
python group_seat_reservation_kp.py instances\CGCUT01_gen_0.txt
Execution (Linux)
export PYTHONPATH=/opt/hexaly_15_0/bin/python
python group_seat_reservation_kp.py instances/CGCUT01_gen_0.txt
# Copyright (c) Hexaly. Permission is hereby granted to use, copy,
# and modify this code for applications developed with Hexaly.
import hexaly.optimizer
import sys

if len(sys.argv) < 2:
    print("Usage: python group_seat_reservation_kp.py inputFile [outputFile] [timeLimit]")
    sys.exit(1)

def read_integers(filename):
    with open(filename) as f:
        return [int(elem) for elem in f.read().split()]

with hexaly.optimizer.HexalyOptimizer() as optimizer:
    # Read instance data
    file_it = iter(read_integers(sys.argv[1]))

    nbRequests = int(next(file_it))
    nbStations = int(next(file_it))
    nbSeats = int(next(file_it))

    nbStationsRequest = [0] * nbRequests
    nbSeatsRequest = [0] * nbRequests
    firstStationRequest = [0] * nbRequests
    for i in range(nbRequests):
        nbStationsRequest[i] = int(next(file_it))
        nbSeatsRequest[i] = int(next(file_it))
        firstStationRequest[i] = int(next(file_it))
        int(next(file_it))  # skip value
        int(next(file_it))  # skip value

    # Requests that share at least one station
    sharingStation = [[] for _ in range(nbRequests)]
    for i in range(nbRequests):
        for j in range(i + 1, nbRequests):
            if (firstStationRequest[i] < firstStationRequest[j] + nbStationsRequest[j]
                    and firstStationRequest[i] + nbStationsRequest[i] > firstStationRequest[j]):
                sharingStation[i].append(j)

    # Declare the optimization model
    model = optimizer.model

    # OptionalInterval: requestSelected[i] represents the seats occupied by the reservation i
    requestSelected = [model.optional_interval(0, nbSeats) for _ in range(nbRequests)]

    # If request i is selected then we allocate the corresponding number of seats
    for i in range(nbRequests):
        model.constraint(
                model.iif(
                model.presence(requestSelected[i]),
                model.length(requestSelected[i]) == nbSeatsRequest[i],
                True))

    # If two requests overlap on any station segment, they must not use the same seats
    for i in range(nbRequests):
        for j in sharingStation[i]:
            model.constraint(model.iif(
                    model.and_(model.presence(requestSelected[i]),
                    model.presence(requestSelected[j])),
                    model.or_(requestSelected[j] < requestSelected[i],
                    requestSelected[i] < requestSelected[j]),
                    True))

    # Compute the utilization of the train
    utilization = model.sum([model.presence(requestSelected[i])
            * nbSeatsRequest[i]
            * nbStationsRequest[i]
            for i in range(nbRequests)])

    # Objective: Maximize the utilization of the train
    model.maximize(utilization)

    model.close()

    # Parameterize the optimizer
    if len(sys.argv) >= 4:
        optimizer.param.time_limit = int(sys.argv[3])
    else:
        optimizer.param.time_limit = 5

    # Stop the search if the upper threshold is reached
    optimizer.param.set_objective_threshold(0, nbSeats * nbStations)

    optimizer.solve()

    # Write the solution in a file using the following format:
    #  - 1st line: total utilization of the train
    #  - For each selected request: which seats are occupied
    if len(sys.argv) >= 3:
        with open(sys.argv[2], "w") as f:
            f.write("%d\n" % utilization.value)
            for i in range(nbRequests):
                seats = requestSelected[i].value
                if not seats.is_void():
                    f.write(str(i) + ": [" + str(seats.start())
                            + "..." + str(seats.end() - 1) + "]\n")
Compilation / Execution (Windows)
cl /EHsc group_seat_reservation_kp.cpp -I%HX_HOME%\include /link %HX_HOME%\bin\hexaly150.lib
group_seat_reservation_kp instances\CGCUT01_gen_0.txt
Compilation / Execution (Linux)
g++ group_seat_reservation_kp.cpp -I/opt/hexaly_15_0/include -lhexaly150 -lpthread -o group_seat_reservation_kp
./group_seat_reservation_kp instances/CGCUT01_gen_0.txt
// Copyright (c) Hexaly. Permission is hereby granted to use, copy,
// and modify this code for applications developed with Hexaly.
#include "optimizer/hexalyoptimizer.h"
#include <fstream>
#include <iostream>

using namespace hexaly;
using namespace std;

class GroupSeatReservationKP {
public:
    // Number of requests
    int nbRequests;
    // Number of stations
    int nbStations;
    // Number of seats
    int nbSeats;

    // Number of stations travelled in each request
    vector<int> nbStationsRequest;
    // Number of seats needed by each request
    vector<int> nbSeatsRequest;
    // First station of each request
    vector<int> firstStationRequest;

    // Requests that share at least one station
    vector<vector<int>> sharingStation;

    // Hexaly Optimizer
    HexalyOptimizer optimizer;
    // Decision Variables: requestSelected[i] represents the seats occupied by the reservation i
    vector<HxExpression> requestSelected;

    // Objective
    HxExpression utilization;

    void readInstance(const string& fileName) {
        ifstream infile;
        infile.exceptions(ifstream::failbit | ifstream::badbit);
        infile.open(fileName.c_str());

        infile >> nbRequests;
        infile >> nbStations;
        infile >> nbSeats;

        nbStationsRequest.resize(nbRequests);
        nbSeatsRequest.resize(nbRequests);
        firstStationRequest.resize(nbRequests);

        for (int i = 0; i < nbRequests; i++) {
            infile >> nbStationsRequest[i];
            infile >> nbSeatsRequest[i];
            infile >> firstStationRequest[i];
            int skip;
            infile >> skip; // skip value
            infile >> skip; // skip value
        }

        // Requests that share at least one station
        sharingStation.resize(nbRequests);
        for (int i = 0; i < nbRequests; i++) {
            for (int j = i + 1; j < nbRequests; j++) {
                if (firstStationRequest[i] < firstStationRequest[j] + nbStationsRequest[j]
                    	&& firstStationRequest[i] + nbStationsRequest[i] > firstStationRequest[j]) {
                    sharingStation[i].push_back(j);
                }
            }
        }
    }

    void solve(int limit) {
        // Declare the optimization model
        HxModel model = optimizer.getModel();

        // OptionalInterval: requestSelected[i] represents the seats occupied by the reservation i
        requestSelected.resize(nbRequests);
        for (int i = 0; i < nbRequests; i++) {
            requestSelected[i] = model.optionalIntervalVar(0, nbSeats);
        }

        // If request i is selected then we allocate the corresponding number of seats
        for (int i = 0; i < nbRequests; i++) {
            model.constraint(model.iif(model.presence(requestSelected[i]),
            		model.length(requestSelected[i]) == nbSeatsRequest[i],
                	true));
        }

        // If two requests overlap on any station segment, they must not use the same seats
        for (int i = 0; i < nbRequests; i++) {
            for (auto j : sharingStation[i]) {
                model.constraint(model.iif(model.presence(requestSelected[i])
                		&& model.presence(requestSelected[j]),
                		requestSelected[j] < requestSelected[i]
                		|| requestSelected[i] < requestSelected[j],
                		true));
            }
        }

        // Compute the utilization of the train
        utilization = model.sum();
        for (int i = 0; i < nbRequests; i++) {
            utilization.addOperand(model.presence(requestSelected[i]) * nbSeatsRequest[i]
            		* nbStationsRequest[i]);
        }

        // Maximize the utilization of the train
        model.maximize(utilization);

        model.close();

        // Parametrize the optimizer
        optimizer.getParam().setTimeLimit(limit);

        // Stop the search if the UPPER threshold is reached
        optimizer.getParam().setObjectiveThreshold(0, (hxint)(nbSeats * nbStations));

        optimizer.solve();
    }

    /* Write the solution in a file using the following format:
            - 1st line: total utilization of the train
            - For each selected request: which seats are occupied */
    void writeSolution(const string& fileName) {
        ofstream outfile;
        outfile.exceptions(ofstream::failbit | ofstream::badbit);
        outfile.open(fileName.c_str());
        outfile << utilization.getValue() << "\n";
        for (int i = 0; i < nbRequests; i++) {
            HxInterval seats = requestSelected[i].getIntervalValue();
            if (!seats.isVoid()) {
                outfile << i << ": [" << seats.getStart() << "..." << seats.getEnd() - 1 << "]\n";
            }
        }
        outfile.close();
    }
};

int main(int argc, char** argv) {
    if (argc < 2) {
        cerr << "Usage: group_seat_reservation_kp inputFile [outputFile] [timeLimit]" << endl;
        return 1;
    }
    const char* instanceFile = argv[1];
    const char* solFile = argc > 2 ? argv[2] : NULL;
    const char* strTimeLimit = argc > 3 ? argv[3] : "5";

    try {
        GroupSeatReservationKP model;
        model.readInstance(instanceFile);
        model.solve(atoi(strTimeLimit));
        if (solFile != NULL)
            model.writeSolution(solFile);
        return 0;
    } catch (const exception &e) {
        cerr << "An error occurred: " << e.what() << endl;
        return 1;
    }
}
Compilation / Execution (Windows)
copy %HX_HOME%\bin\Hexaly.NET.dll .
csc group_seat_reservation_kp.cs /reference:Hexaly.NET.dll
group_seat_reservation_kp instances\CGCUT01_gen_0.txt
// Copyright (c) Hexaly. Permission is hereby granted to use, copy,
// and modify this code for applications developed with Hexaly.
using System;
using System.Collections.Generic;
using System.IO;
using Hexaly.Optimizer;

public class GroupSeatReservationKP : IDisposable
{
    // Data from the problem
    private int nbRequests;
    private int nbStations;
    private int nbSeats;
    private int[] nbStationsRequest;
    private int[] nbSeatsRequest;
    private int[] firstStationRequest;
    private List<int>[] sharingStation;

    // Hexaly Optimizer
    private readonly HexalyOptimizer optimizer;

    // Decision variable: requestSelected[i] represents the seats occupied by the reservation i
    private HxExpression[] requestSelected;

    // Objective
    private HxExpression utilization;

    public GroupSeatReservationKP()
    {
        optimizer = new HexalyOptimizer();
    }

    public void Dispose()
    {
        if (optimizer != null)
            optimizer.Dispose();
    }

    /* Read instance data */
    public static string[] ReadNextLine(StreamReader input)
    {
        return input.ReadLine().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
    }

    private void ReadInstance(string fileName)
    {
        using (StreamReader input = new StreamReader(fileName))
        {
            nbRequests = int.Parse(input.ReadLine());
            string[] splitted = ReadNextLine(input);

            nbStations = int.Parse(splitted[0]);
            nbSeats = int.Parse(splitted[1]);

            nbStationsRequest = new int[nbRequests];
            nbSeatsRequest = new int[nbRequests];
            firstStationRequest = new int[nbRequests];
            for (int i = 0; i < nbRequests; i++)
            {
                splitted = ReadNextLine(input);
                nbStationsRequest[i] = int.Parse(splitted[0]);
                nbSeatsRequest[i] = int.Parse(splitted[1]);
                firstStationRequest[i] = int.Parse(splitted[2]);
            }

            // Requests that share at least one station
            sharingStation = new List<int>[nbRequests];
            for (int i = 0; i < nbRequests; i++)
            {
                sharingStation[i] = new List<int>();
                for (int j = i + 1; j < nbRequests; j++)
                {
                    if (firstStationRequest[i] < firstStationRequest[j] + nbStationsRequest[j]
                            && firstStationRequest[i] + nbStationsRequest[i] > firstStationRequest[j])
                    {
                        sharingStation[i].Add(j);
                    }

                }
            }
        }
    }

    private void Solve(int timeLimit)
    {
        // Declare the optimization model
        HxModel model = optimizer.GetModel();

        // OptionalInterval: requestSelected[i] represents the seats occupied by the reservation i
        requestSelected = new HxExpression[nbRequests];
        // Declare decisions
        for (int i = 0; i < nbRequests; i++)
        {
            requestSelected[i] = model.OptionalInterval(0, nbSeats);
        }

        // If request i is selected then we allocate the corresponding number of seats
        for (int i = 0; i < nbRequests; i++)
        {
            model.Constraint(model.If(
                    model.Presence(requestSelected[i]),
                    model.Eq(model.Length(requestSelected[i]), nbSeatsRequest[i]),
                    1));
        }

        // If two requests overlap on any station segment, they must not use the same seats
        for (int i = 0; i < nbRequests; i++)
        {
            foreach (int j in sharingStation[i])
            {
                model.Constraint(model.If(
                        model.And(model.Presence(requestSelected[i]), model.Presence(requestSelected[j])),
                        model.Or(requestSelected[j] < requestSelected[i], requestSelected[j] > requestSelected[i]),
                        1));
            }
        }

        // Compute the utilization of the train
        utilization = model.Sum();
        for (int i = 0; i < nbRequests; i++)
        {
            utilization.AddOperand(
                    model.Presence(requestSelected[i]) * nbSeatsRequest[i] * nbStationsRequest[i]);
        }
        // Objective: Maximize utilization
        model.Maximize(utilization);

        model.Close();

        // Parameterize the optimizer
        optimizer.GetParam().SetTimeLimit(timeLimit);

        // Stop the search if the upper threshold is reached
        optimizer.GetParam().SetObjectiveThreshold(0, nbSeats * nbStations);

        optimizer.Solve();
    }

    /* Write the solution in a file using the following format:
        - 1st line: total utilization of the train
        - For each selected request: which seats are occupied */
    public void WriteSolution(string fileName)
    {
        using (StreamWriter output = new StreamWriter(fileName))
        {
            output.WriteLine(utilization.GetValue());

            for (int i = 0; i < nbRequests; i++)
            {
                HxInterval seats = requestSelected[i].GetIntervalValue();
                if (!seats.IsVoid())
                {
                    output.WriteLine(i + ": [" + seats.Start().ToString()
                            + "..." + (seats.End() - 1).ToString() + "]");
                }
            }
        }
    }

    public static void Main(string[] args)
    {
        if (args.Length < 1)
        {
            Console.WriteLine("Usage: GroupSeatReservationKP instanceFile [outputFile] [timeLimit]");
            Environment.Exit(1);
        }

        string instanceFile = args[0];
        string outputFile = args.Length > 1 ? args[1] : null;
        string strTimeLimit = args.Length > 2 ? args[2] : "60";

        using (GroupSeatReservationKP model = new GroupSeatReservationKP())
        {
            model.ReadInstance(instanceFile);
            model.Solve(int.Parse(strTimeLimit));
            if (outputFile != null)
                model.WriteSolution(outputFile);
        }
    }
}

Compilation / Execution (Windows)
javac group_seat_reservation_kp.java -cp %HX_HOME%\bin\hexaly.jar
java -cp %HX_HOME%\bin\hexaly.jar;. group_seat_reservation_kp instances\CGCUT01_gen_0.txt
Compilation / Execution (Linux)
javac group_seat_reservation_kp.java -cp /opt/hexaly_15_0/bin/hexaly.jar
java -cp /opt/hexaly_15_0/bin/hexaly.jar:. group_seat_reservation_kp instances/CGCUT01_gen_0.txt
// Copyright (c) Hexaly. Permission is hereby granted to use, copy,
// and modify this code for applications developed with Hexaly.
import java.io.*;
import java.util.*;

import com.hexaly.optimizer.*;

public class GroupSeatReservationKP {
    // Data from the problem
    private int nbRequests;
    private int nbStations;
    private int nbSeats;
    private int[] nbStationsRequest;
    private int[] nbSeatsRequest;
    private int[] firstStationRequest;
    private List<List<Integer>> sharingStation;

    // Hexaly Optimizer
    private final HexalyOptimizer optimizer;

    // Decision variable: requestSelected[i] represents the seats occupied by the
    // reservation i
    private HxExpression[] requestSelected;

    // Objective
    private HxExpression utilization;

    private GroupSeatReservationKP(HexalyOptimizer optimizer) {
        this.optimizer = optimizer;
    }

    /* Read instance data */
    private void readInstance(String fileName) throws IOException {
        try (Scanner input = new Scanner(new File(fileName))) {
            input.useLocale(Locale.ROOT);
            nbRequests = input.nextInt();
            nbStations = input.nextInt();
            nbSeats = input.nextInt();

            nbStationsRequest = new int[nbRequests];
            nbSeatsRequest = new int[nbRequests];
            firstStationRequest = new int[nbRequests];
            for (int i = 0; i < nbRequests; i++) {
                nbStationsRequest[i] = input.nextInt();
                nbSeatsRequest[i] = input.nextInt();
                firstStationRequest[i] = input.nextInt();
                input.nextInt(); // skip
                input.nextInt(); // skip
            }
            // Requests that share at least one station
            sharingStation = new ArrayList<>();
            for (int i = 0; i < nbRequests; i++) {
                sharingStation.add(new ArrayList<>());
                for (int j = i + 1; j < nbRequests; j++) {
                    if (firstStationRequest[i] < firstStationRequest[j] + nbStationsRequest[j]
                            && firstStationRequest[i] + nbStationsRequest[i] > firstStationRequest[j]) {
                        sharingStation.get(i).add(j);
                    }
                }
            }
        }
    }

    private void solve(int timeLimit) {
        // Declare the optimization model
        HxModel model = optimizer.getModel();

        // OptionalInterval: requestSelected[i] represents the seats occupied by the
        // reservation i
        requestSelected = new HxExpression[nbRequests];
        // Declare decisions
        for (int i = 0; i < nbRequests; i++) {
            requestSelected[i] = model.optionalIntervalVar(0, nbSeats);
        }

        // If request i is selected then we allocate the corresponding number of seats
        for (int i = 0; i < nbRequests; i++) {
            model.constraint(model.iif(model.presence(requestSelected[i]),
                    model.eq(model.length(requestSelected[i]), nbSeatsRequest[i]), 1));
        }

        // If two requests overlap on any station segment, they must not use the same
        // seats
        for (int i = 0; i < nbRequests; i++) {
            for (int j : sharingStation.get(i)) {
                model.constraint(
                        model.iif(model.and(model.presence(requestSelected[i]), model.presence(requestSelected[j])),
                        model.or(model.lt(requestSelected[j], requestSelected[i]),
                        model.gt(requestSelected[j], requestSelected[i])),
                        1));
            }
        }

        // Compute the utilization of the train
        utilization = model.sum();
        for (int i = 0; i < nbRequests; i++) {
            utilization.addOperand(
                    model.prod(model.presence(requestSelected[i]), nbStationsRequest[i] * nbSeatsRequest[i]));
        }

        // Objective: Maximize utilization
        model.maximize(utilization);

        model.close();

        // Parameterize the optimizer
        optimizer.getParam().setTimeLimit(timeLimit);
        // Stop the search if the upper threshold is reached
        optimizer.getParam().setObjectiveThreshold(0, nbSeats * nbStations);

        optimizer.solve();
    }

    /*
     * Write the solution in a file using the following format:
     * - 1st line: total utilization of the train
     * - For each selected request: which seats are occupied
     */
    private void writeSolution(String fileName) throws IOException {
        try (PrintWriter output = new PrintWriter(fileName)) {
            output.println(utilization.getValue());
            for (int i = 0; i < nbRequests; i++) {
                HxInterval seats = requestSelected[i].getIntervalValue();
                if (!seats.isVoid()) {
                    output.println(i + ": [" + seats.getStart() + "..." + (seats.getEnd() - 1) + "]");
                }
            }
        }
    }

    public static void main(String[] args) {
        if (args.length < 1) {
            System.err.println("Usage: java GroupSeatReservationKP inputFile [outputFile] [timeLimit]");
            System.exit(1);
        }
        String instanceFile = args[0];
        String outputFile = args.length > 1 ? args[1] : null;
        String strTimeLimit = args.length > 2 ? args[2] : "5";
        try (HexalyOptimizer optimizer = new HexalyOptimizer()) {
            GroupSeatReservationKP model = new GroupSeatReservationKP(optimizer);
            model.readInstance(instanceFile);
            model.solve(Integer.parseInt(strTimeLimit));
            if (outputFile != null) {
                model.writeSolution(outputFile);
            }
        } catch (Exception ex) {
            System.err.println(ex);
            ex.printStackTrace();
            System.exit(1);
        }
    }
}