site.base_url }}/">The Bear's Den -->

The Bear's Den

Enter at your own risk

Return of the Kit

Task 2: Travel Expenditure

Submitted by: Mohammad S Anwar


You are given two list, @costs and @days.

The list @costs contains the cost of three different types of travel cards you can buy.

For example @costs = (5, 30, 90)

Index 0 element represent the cost of  1 day  travel card.
Index 1 element represent the cost of  7 days travel card.
Index 2 element represent the cost of 30 days travel card.

The list @days contains the day number you want to travel in the year.

For example: @days = (1, 3, 4, 5, 6)

The above example means you want to travel on day 1, day 3, day 4, day 5 and day 6 of the year. Write a script to find the minimum travel cost.

Example 1

Input: @costs = (2, 7, 25)
       @days  = (1, 5, 6, 7, 9, 15)
Output: 11

On day 1, we buy a one day pass for 2 which would cover the day 1.
On day 5, we buy seven days pass for 7 which would cover days 5 - 9.
On day 15, we buy a one day pass for 2 which would cover the day 15.

So the total cost is 2 + 7 + 2 => 11.

Example 2

Input: @costs = (2, 7, 25)
       @days  = (1, 2, 3, 5, 7, 10, 11, 12, 14, 20, 30, 31)
Output: 20

On day 1, we buy a seven days pass for 7 which would cover days 1 - 7.
On day 10, we buy a seven days pass for 7 which would cover days 10 - 14.
On day 20, we buy a one day pass for 2 which would cover day 20.
On day 30, we buy a one day pass for 2 which would cover day 30.
On day 31, we buy a one day pass for 2 which would cover day 31.

So the total cost is 7 + 7 + 2 + 2 + 2 => 20.

Preliminary Note

See note in challenge 216.

Solution

My comment from the Octave solution:

This task is very similar to task 2 from week 216. Again, it may be regarded as an integer linear programming task. Each travel day needs to be covered by at least one travel card. Let N be the number of days, then we have 3 * N possible travel cards: starting at each given day with a duration of 1, 7 or 30 days. This leads to N inequalities in 3 * N variables: The count of cards that are valid at each day must be at least one. The objective function as the sum of the prices of the selected cards shall be minimized.

The core of the formulation of an integer linear program is a ternary relation between a travel day \(d_i\), a travel card valid from day \(f_j\) having a duration \(d_k\) that yields true if the travel card is valid on that day.

We may build a \(\mathit{days} \times \mathit{fromdates} \times \mathit{cardtypes}\) 3-d “matrix” \(a_{ijk}^*\) representing this ternary relation. To arrive at an linear program we need to flatten the dimensions \(\mathit{fromdates}\;f_j\) and \(\mathit{cardtypes}\;d_k\) into a single dimension representing a travel card valid from \(f_j\) and valid for \(d_k\) days and yield a matrix \((a_{il})\). The inequalities right-hand-sides are all one, as we need at least one valid travel card per travel day and finally the prices \(c_l\) depend on the card type only.

This leads to the following binary linear program:

\[\begin{gathered} \text{minimize:}\hfill & \sum_l c_l x_l\\ \text{subject to:}\hfill & \sum_l a_{il} x_l & \ge 1\\ & x_l & \in \{0, 1\} \end{gathered}\]

The PDL implementation follows this recipe by creating the 3-d relation ndarray, flatten it to 2-d and building the objecive function coefficients as \(\mathit{days}\)-long blocks of the card prices. Input args are an AoA holding a list of card duration and price and an array of travel days.

use PDL;
use PDL::NiceSlice;
use PDL::Opt::GLPK;
use feature 'signatures';

sub travel_cost ($crd, $dy) {
    my $cards = long @$crd;
    my $days = long $dy;
    my $from = $days->dummy(1);
    my $to = $from + $cards((0))->dummy(0);
    my $valid = ($days->dummy(1) >= $from->dummy(0)) &
        ($days->dummy(1) < $to->dummy(0));

    my $a = $valid->clump(1,2)->xchg(0,1);
    my $b = ones($days);
    my $c = $cards((1))->dummy(0,$days->dim(0))->clump(-1);

    my $xopt = null;
    my $fopt = null;
    my $status = null;

    glpk($c, $a, $b, zeros($c), zeros($c), GLP_LO * ones($b),
        GLP_BV * ones($c), GLP_MIN, $xopt, $fopt, $status);

    my $selection = $xopt->reshape($to->dims);
    my $solution = whichND $selection;

    ($days->dice($solution((0)))->dummy(0)
        ->glue(0, $cards->dice('X', $solution((1))))->qsortvec->unpdl, $fopt);
}

See the full solution.


If you have a question about this post or if you like to comment on it, feel free to open an issue in my github repository.