I'm working on a small project where I need help in finding the best and cheapest tickets based on some input from the user:
- Between what periods (start & end date)?
- Within that period, are you skipping 1 or several dates?
- How many times do you need to use the ticket each day?
There are x number of tickets. A ticket can cover:
- Single ticket, to be used only once, price $5.
- Period ticket (unlimited rides each day), to be used as much as you want from 1 day/$10, 3 days/$30, 7 days/$45..
I guess I'm looking for some kind of algorithm to determine the best combination of tickets based on periods (including or excluding skipping dates), and also their price.
Also, I guess there needs to be considered the case where it will be a better and cheaper outcome for me to buy a period ticket that covers more days than I actually need, but is cheaper based on how many rides I'm going for each day...
UPDATE (based on Petr suggestion..)
<?php
$tickets = array(
array("price"=>5, "label"=>"single", "period"=>null),
array("price"=>10, "label"=>"1 day", "period"=>1),
array("price"=>30, "label"=>"3 days", "period"=>3),
array("price"=>45, "label"=>"7 days", "period"=>7)
);
$trips = 2;
$startDate = new DateTime("2015-06-23");
$endDate = new DateTime("2015-06-30");
$endDate->modify("+1 day");
$interval = DateInterval::createFromDateString('1 day');
$period = new DatePeriod($startDate, $interval, $endDate);
$cost = array();
$day = 1;
foreach( $period as $date ){
$span = $startDate->diff($date);
$days = ( $span->format('%a') + 1 );
$ticket = getCheapestTicket( $days );
$cost[ $day ] = $ticket;
$day++;
}
function getCheapestTicket( $days ){
global $tickets, $trips;
$lowestSum = null;
$cheapestTicket = null;
echo "-- getCheapestTicket --" . PHP_EOL;
echo "DAYS TO COVER: " . $days . " / TRIPS: " . $trips . PHP_EOL;
foreach( $tickets as $ticket ){
$price = $ticket['price'];
$period = $ticket['period'] ? $ticket['period'] : -1;
if( $ticket['period'] ){
$units = ceil( $days / $period );
$sum = round( $units * $price );
}else{
$units = ceil( $days * $trips );
$sum = round( ( $days * $price ) * $trips );
}
if( $sum <= $lowestSum || !$lowestSum ){
if( $ticket['period'] > $cheapestTicket['period'] ){
$cheapestTicket = $ticket;
$lowestSum = $sum;
}else{
$lowestSum = $sum;
$cheapestTicket = $ticket;
}
}
echo "TICKET: " . $ticket['label'] . " / Units to cover days: " . $units . " / Sum: " . $sum . " / Period: " . $period . PHP_EOL;
}
echo "CHEAPEST TICKET: " . $cheapestTicket['label'] .
" / PRICE PER UNIT: " . $cheapestTicket['price'] . " / SUM: " . $lowestSum . PHP_EOL. PHP_EOL;
return $cheapestTicket;
}
I'm not sure if this is on the way yet :)
You can solve this problem using a dynamic programming approach.
Firstly, for simplicity of the algorithm, let's for each
l
calculate the cheapest single ticket that can be used to coverl
consecutive days. For your example this will be: 1 day $10, 2 days $30 (buy a 3-day ticket and use it only for 2 days), 3 days $30, 4-7 days $45, etc. (There will obviously be some maximal value ofl
beyond which there will be no such ticket.) Denote these results ascost[l]
.Now the main dynamic programming solution. For each date
i
in your [begin
,end
] range, calculateans[i]
= the minimal cost to buy tickets to cover at least interval frombegin
toi
.Assuming that you have already calculated all the values before date
i
, calculation for datei
is simple. You will need some ticket that ends on dayi
. Let's say it covers length ofl
days, then the price for this last ticket will becost[l]
, and you will also have to cover the days frombegin
toi-l
, which will costans[i-l]
(or zero ifi-l
is beforebegin
). So for a giveni
iterate over all possiblel
s and find the one that minimizes the solution.This gives you the
O(NL)
solution, whereN
is the number of days andL
is the maximal span of a single ticket.This all assumes that each ticked covers several full consecutive days. If it covers, say, 24 full hours (from the hour of buying to the same hour next day), then just calculate answers for each hour.