983. Minimum Cost For Tickets
Problem Description
You're planning train travel for a year and have a list of specific days when you'll need to travel. These travel days are given as an integer array days
, where each day is a number from 1
to 365
.
There are three types of train passes available for purchase:
- A 1-day pass costs
costs[0]
dollars and covers travel for 1 consecutive day - A 7-day pass costs
costs[1]
dollars and covers travel for 7 consecutive days - A 30-day pass costs
costs[2]
dollars and covers travel for 30 consecutive days
When you buy a pass on a specific day, it covers that day and the specified number of consecutive days following it. For example, if you buy a 7-day pass on day 2, you can travel on days 2, 3, 4, 5, 6, 7, and 8.
Your goal is to find the minimum total cost needed to cover all the travel days in your list. You need to strategically choose which passes to buy and when to buy them so that every day in your days
array is covered by at least one pass, while minimizing the total amount spent.
The solution uses dynamic programming with memoization. The function dfs(i)
calculates the minimum cost to cover all travel days starting from index i
in the sorted days
array. For each position, it considers buying each of the three pass types and uses binary search to find the next uncovered travel day after the current pass expires. The algorithm recursively calculates costs for all possibilities and returns the minimum.
Intuition
The key insight is that we need to make a decision at each travel day: which type of pass should we buy to minimize the total cost? This naturally leads to a dynamic programming approach where we explore all possible choices and select the optimal one.
Think of it this way: when we're standing at a particular travel day, we have three options - buy a 1-day, 7-day, or 30-day pass. Each choice will cover some future travel days, and then we'll need to solve the same problem again for the remaining uncovered days. This recursive structure suggests we can break down the problem into smaller subproblems.
For each travel day at index i
, we ask: "What's the minimum cost to cover all travel days from index i
to the end?" To answer this, we:
- Try buying a 1-day pass (costs
costs[0]
) which covers only the current day - Try buying a 7-day pass (costs
costs[1]
) which might cover multiple upcoming travel days - Try buying a 30-day pass (costs
costs[2]
) which might cover even more travel days
After buying any pass, we need to find the next uncovered travel day. Since our travel days are sorted, we can use binary search to efficiently find the first day that isn't covered by our current pass. For example, if we're on day 5 and buy a 7-day pass, we need to find the first travel day that's >= day 12 (5 + 7).
The recursive formula becomes: dfs(i) = min(cost_of_pass + dfs(next_uncovered_day))
for each pass type. We use memoization to avoid recalculating the same subproblems multiple times, as different combinations of passes might lead us to the same remaining travel days.
The base case is when we've covered all travel days (i >= n
), at which point the cost is 0 since no more passes are needed.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution implements a top-down dynamic programming approach with memoization and binary search optimization.
Implementation Details:
-
Main Function Structure: We define a recursive function
dfs(i)
that calculates the minimum cost to cover all travel days starting from indexi
in thedays
array. The answer to our problem isdfs(0)
. -
Memoization with @cache: The
@cache
decorator is used to automatically memoize the results ofdfs(i)
. This prevents redundant calculations when the same subproblem is encountered multiple times through different paths. -
Base Case:
if i >= n: return 0
When
i
reaches or exceeds the total number of travel daysn
, we've covered all required days, so the additional cost is 0. -
Recursive Case: For each position
i
, we try all three pass types:ans = inf for c, v in zip(costs, valid): j = bisect_left(days, days[i] + v) ans = min(ans, c + dfs(j))
- We initialize
ans
to infinity to track the minimum cost valid = [1, 7, 30]
represents the duration of each pass type- We iterate through each pass type using
zip(costs, valid)
- We initialize
-
Binary Search Optimization:
j = bisect_left(days, days[i] + v)
For each pass type with duration
v
, we usebisect_left
to find the indexj
of the first travel day that would NOT be covered by this pass. Specifically:- If we're at
days[i]
and buy a pass valid forv
days - The pass covers days from
days[i]
todays[i] + v - 1
- We search for the first day >=
days[i] + v
in the sorteddays
array - This gives us the starting index for the next subproblem
- If we're at
-
Cost Calculation: For each pass type, we calculate:
- Current pass cost:
c
- Cost for remaining days:
dfs(j)
- Total cost:
c + dfs(j)
We keep track of the minimum among all three options.
- Current pass cost:
Time Complexity: O(n × log n)
where n
is the length of the days
array. Each of the n
states is computed once due to memoization, and each state performs 3 binary searches, each taking O(log n)
time.
Space Complexity: O(n)
for the memoization cache and recursion stack.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Example Input:
days = [1, 4, 6, 7, 8, 20]
costs = [2, 7, 15]
(1-day: 7, 30-day: $15)
Step-by-step execution of dfs(i)
:
Initial call: dfs(0)
(starting at day 1)
- Current day:
days[0] = 1
- Option 1: Buy 1-day pass ($2)
- Covers day 1 only
- Next uncovered day:
bisect_left([1,4,6,7,8,20], 1+1=2)
→ index 1 (day 4) - Cost: $2 +
dfs(1)
- Option 2: Buy 7-day pass ($7)
- Covers days 1-7
- Next uncovered day:
bisect_left([1,4,6,7,8,20], 1+7=8)
→ index 4 (day 8) - Cost: $7 +
dfs(4)
- Option 3: Buy 30-day pass ($15)
- Covers days 1-30
- Next uncovered day:
bisect_left([1,4,6,7,8,20], 1+30=31)
→ index 6 (beyond array) - Cost: 15 + 0 = $15
Exploring Option 1: dfs(1)
(starting at day 4)
- Current day:
days[1] = 4
- Option 1: Buy 1-day pass ($2)
- Next uncovered: index 2 (day 6)
- Cost: $2 +
dfs(2)
- Option 2: Buy 7-day pass ($7)
- Covers days 4-10
- Next uncovered: index 5 (day 20)
- Cost: $7 +
dfs(5)
- Option 3: Buy 30-day pass ($15)
- Covers all remaining days
- Cost: 15
Continuing dfs(2)
(starting at day 6)
- Current day:
days[2] = 6
- Option 1: Buy 1-day pass ($2)
- Next uncovered: index 3 (day 7)
- Cost: $2 +
dfs(3)
- Option 2: Buy 7-day pass ($7)
- Covers days 6-12
- Next uncovered: index 5 (day 20)
- Cost: $7 +
dfs(5)
And dfs(3)
(starting at day 7)
- Current day:
days[3] = 7
- Option 1: Buy 1-day pass ($2)
- Next uncovered: index 4 (day 8)
- Cost: $2 +
dfs(4)
Then dfs(4)
(starting at day 8)
- Current day:
days[4] = 8
- Option 1: Buy 1-day pass ($2)
- Next uncovered: index 5 (day 20)
- Cost: $2 +
dfs(5)
Finally dfs(5)
(starting at day 20)
- Current day:
days[5] = 20
- Option 1: Buy 1-day pass ($2)
- Next uncovered: index 6 (end of array)
- Cost: 2
- Option 2: Buy 7-day pass ($7)
- Cost: 7
- Option 3: Buy 30-day pass ($15)
- Cost: 15
- Minimum: $2
Working backwards with memoized results:
dfs(5) = 2
dfs(4) = 2 + 2 = 4
dfs(3) = 2 + 4 = 6
dfs(2) = min(2 + 6, 7 + 2) = min(8, 9) = 8
dfs(1) = min(2 + 8, 7 + 2, 15) = min(10, 9, 15) = 9
dfs(0) = min(2 + 9, 7 + 4, 15) = min(11, 11, 15) = 11
Final answer: $11
The optimal strategy is to buy a 7-day pass on day 1 (covering days 1,4,6,7) and a 1-day pass on day 8, and another 1-day pass on day 20, for a total of 2 + 11.
Solution Implementation
1class Solution:
2 def mincostTickets(self, days: List[int], costs: List[int]) -> int:
3 from functools import cache
4 from bisect import bisect_left
5 from math import inf
6 from typing import List
7
8 @cache
9 def calculate_min_cost(day_index: int) -> int:
10 """
11 Calculate minimum cost starting from day_index using dynamic programming.
12
13 Args:
14 day_index: Current index in the days array
15
16 Returns:
17 Minimum cost to cover all travel days from day_index onwards
18 """
19 # Base case: if we've covered all days, no more cost needed
20 if day_index >= total_days:
21 return 0
22
23 min_cost = inf
24
25 # Try each ticket type (1-day, 7-day, 30-day)
26 for ticket_cost, ticket_duration in zip(costs, ticket_durations):
27 # Find the next day index after current ticket expires
28 # Current ticket covers from days[day_index] to days[day_index] + ticket_duration - 1
29 next_uncovered_day = days[day_index] + ticket_duration
30 next_day_index = bisect_left(days, next_uncovered_day)
31
32 # Calculate total cost: current ticket + minimum cost for remaining days
33 total_cost = ticket_cost + calculate_min_cost(next_day_index)
34 min_cost = min(min_cost, total_cost)
35
36 return min_cost
37
38 # Initialize variables
39 total_days = len(days)
40 ticket_durations = [1, 7, 30] # Duration coverage for each ticket type
41
42 # Start calculation from the first day
43 return calculate_min_cost(0)
44
1class Solution {
2 // Pass durations in days for 1-day, 7-day, and 30-day passes
3 private final int[] PASS_DURATIONS = {1, 7, 30};
4
5 // Input arrays for travel days and pass costs
6 private int[] travelDays;
7 private int[] passCosts;
8
9 // Memoization array to store minimum cost from index i to end
10 private Integer[] memo;
11
12 // Total number of travel days
13 private int totalDays;
14
15 public int mincostTickets(int[] days, int[] costs) {
16 // Initialize instance variables
17 totalDays = days.length;
18 memo = new Integer[totalDays];
19 this.travelDays = days;
20 this.passCosts = costs;
21
22 // Start DFS from day 0
23 return calculateMinCost(0);
24 }
25
26 private int calculateMinCost(int dayIndex) {
27 // Base case: if we've covered all travel days, no more cost needed
28 if (dayIndex >= totalDays) {
29 return 0;
30 }
31
32 // Return memoized result if already calculated
33 if (memo[dayIndex] != null) {
34 return memo[dayIndex];
35 }
36
37 // Initialize minimum cost for current position as maximum value
38 memo[dayIndex] = Integer.MAX_VALUE;
39
40 // Try each type of pass (1-day, 7-day, 30-day)
41 for (int passType = 0; passType < 3; passType++) {
42 // Calculate the day until which current pass is valid
43 int validUntilDay = travelDays[dayIndex] + PASS_DURATIONS[passType];
44
45 // Find the next travel day index after the pass expires
46 // Using binary search to find the first day not covered by current pass
47 int nextDayIndex = Arrays.binarySearch(travelDays, validUntilDay);
48
49 // If exact match not found, convert to insertion point (first day after pass expires)
50 nextDayIndex = nextDayIndex < 0 ? -nextDayIndex - 1 : nextDayIndex;
51
52 // Calculate minimum cost: current pass cost + minimum cost from next uncovered day
53 memo[dayIndex] = Math.min(
54 memo[dayIndex],
55 calculateMinCost(nextDayIndex) + passCosts[passType]
56 );
57 }
58
59 return memo[dayIndex];
60 }
61}
62
1class Solution {
2public:
3 int mincostTickets(vector<int>& days, vector<int>& costs) {
4 // Define the validity periods for each ticket type (1-day, 7-day, 30-day)
5 int ticketDurations[3] = {1, 7, 30};
6 int numDays = days.size();
7
8 // Dynamic programming memoization array
9 // dp[i] represents the minimum cost to cover all travel days starting from index i
10 int dp[numDays];
11 memset(dp, 0, sizeof(dp));
12
13 // Recursive function with memoization to calculate minimum cost
14 function<int(int)> calculateMinCost = [&](int dayIndex) {
15 // Base case: if we've covered all days, no more cost needed
16 if (dayIndex >= numDays) {
17 return 0;
18 }
19
20 // Return memoized result if already calculated
21 if (dp[dayIndex]) {
22 return dp[dayIndex];
23 }
24
25 // Initialize minimum cost for current position as maximum value
26 dp[dayIndex] = INT_MAX;
27
28 // Try each ticket type and find the minimum cost
29 for (int ticketType = 0; ticketType < 3; ++ticketType) {
30 // Find the next day index that won't be covered by current ticket
31 // days[dayIndex] + ticketDurations[ticketType] gives the first day not covered
32 int nextDayIndex = lower_bound(days.begin(), days.end(),
33 days[dayIndex] + ticketDurations[ticketType]) - days.begin();
34
35 // Update minimum cost: current ticket cost + cost for remaining days
36 dp[dayIndex] = min(dp[dayIndex],
37 calculateMinCost(nextDayIndex) + costs[ticketType]);
38 }
39
40 return dp[dayIndex];
41 };
42
43 // Start calculation from the first day (index 0)
44 return calculateMinCost(0);
45 }
46};
47
1/**
2 * Calculates the minimum cost to buy tickets for all travel days
3 * @param days - Array of days when travel is needed (sorted in ascending order)
4 * @param costs - Array of ticket costs [1-day pass, 7-day pass, 30-day pass]
5 * @returns Minimum cost to cover all travel days
6 */
7function mincostTickets(days: number[], costs: number[]): number {
8 const totalDays: number = days.length;
9 // Memoization array to store minimum cost from each day index
10 const memo: number[] = Array(totalDays).fill(0);
11 // Ticket validity periods in days
12 const ticketDurations: number[] = [1, 7, 30];
13
14 /**
15 * Binary search to find the first day index that is >= target day
16 * @param targetDay - The day to search for
17 * @returns Index of the first day >= targetDay, or totalDays if none found
18 */
19 const binarySearch = (targetDay: number): number => {
20 let left: number = 0;
21 let right: number = totalDays;
22
23 while (left < right) {
24 const mid: number = (left + right) >> 1;
25 if (days[mid] >= targetDay) {
26 right = mid;
27 } else {
28 left = mid + 1;
29 }
30 }
31
32 return left;
33 };
34
35 /**
36 * Dynamic programming with memoization to find minimum cost from day index
37 * @param dayIndex - Current day index to process
38 * @returns Minimum cost to cover all days from dayIndex onwards
39 */
40 const calculateMinCost = (dayIndex: number): number => {
41 // Base case: no more days to cover
42 if (dayIndex >= totalDays) {
43 return 0;
44 }
45
46 // Return memoized result if already calculated
47 if (memo[dayIndex]) {
48 return memo[dayIndex];
49 }
50
51 // Initialize minimum cost as infinity
52 memo[dayIndex] = Infinity;
53
54 // Try each ticket type and find minimum cost
55 for (let ticketType = 0; ticketType < 3; ++ticketType) {
56 // Find next uncovered day after current ticket expires
57 const nextDayIndex: number = binarySearch(days[dayIndex] + ticketDurations[ticketType]);
58 // Update minimum cost for current position
59 memo[dayIndex] = Math.min(
60 memo[dayIndex],
61 calculateMinCost(nextDayIndex) + costs[ticketType]
62 );
63 }
64
65 return memo[dayIndex];
66 };
67
68 // Start calculation from first day
69 return calculateMinCost(0);
70}
71
Time and Space Complexity
Time Complexity: O(n × log n)
The time complexity is determined by the recursive DFS function with memoization. There are n
possible states (one for each index i
from 0 to n-1), and each state is computed at most once due to the @cache
decorator. For each state, we iterate through 3 ticket options (1-day, 7-day, and 30-day passes), and for each option, we perform a binary search using bisect_left
to find the next index, which takes O(log n)
time. Therefore, the overall time complexity is O(n × 3 × log n) = O(n × log n)
.
Space Complexity: O(n)
The space complexity consists of two main components:
- The recursion call stack, which can go up to depth
n
in the worst case when we process each day sequentially - The memoization cache from the
@cache
decorator, which stores at mostn
computed results (one for each possible starting index)
Both contribute O(n)
space, resulting in an overall space complexity of O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Off-by-One Error in Pass Coverage Calculation
The Pitfall: A common mistake is incorrectly calculating when a pass expires. Many developers mistakenly use:
j = bisect_left(days, days[i] + v - 1) # WRONG!
This searches for the last day covered by the pass, not the first uncovered day.
Why It's Wrong:
- A 7-day pass bought on day 2 covers days 2-8 (inclusive)
- The first uncovered day is day 9, which is
2 + 7 = 9
- Using
days[i] + v - 1
would give us day 8, which is still covered
The Correct Approach:
j = bisect_left(days, days[i] + v) # CORRECT
This correctly finds the index of the first day that needs a new pass.
2. Using bisect_right Instead of bisect_left
The Pitfall:
j = bisect_right(days, days[i] + v - 1) # Subtle bug!
Why It's Wrong:
If days[i] + v - 1
exists in the array, bisect_right
would skip over it and potentially miss calculating costs for some days. Consider:
days = [1, 4, 6]
, currently atdays[0] = 1
with a 3-day pass- Pass covers days 1, 2, 3
bisect_right(days, 3)
returns index 1 (correctly)- But if
days = [1, 3, 4, 6]
and we usebisect_right(days, 3)
, it returns index 2, skipping day 4
The Fix:
Always use bisect_left
with days[i] + v
to consistently find the first uncovered day.
3. Forgetting to Handle Edge Cases in Binary Search
The Pitfall:
Not considering that binary search might return an index equal to len(days)
:
j = bisect_left(days, days[i] + v) next_cost = costs[pass_type] + dfs(j) # j might be >= len(days)
Why It Matters:
When the pass covers all remaining travel days, bisect_left
returns len(days)
. The base case if i >= n: return 0
handles this correctly, but developers sometimes add unnecessary bounds checking that can introduce bugs.
The Solution:
Trust the base case to handle i >= n
. The algorithm naturally handles this scenario without additional checks.
4. Incorrect Pass Duration Values
The Pitfall:
valid = [0, 6, 29] # WRONG! Zero-indexed thinking
Why It's Wrong: The pass durations represent the number of days covered, not array indices:
- 1-day pass covers 1 day (not 0)
- 7-day pass covers 7 days (not 6)
- 30-day pass covers 30 days (not 29)
The Correct Values:
valid = [1, 7, 30] # Actual number of days covered
5. Not Memoizing the Recursive Function
The Pitfall:
Forgetting the @cache
decorator leads to exponential time complexity:
def dfs(i): # Missing @cache decorator
if i >= n:
return 0
# ... rest of the code
Why It's Critical: Without memoization, the same subproblems are solved multiple times. For example, different combinations of passes might lead to the same remaining days to cover, causing redundant calculations.
The Solution:
Always use @cache
or implement manual memoization with a dictionary to store computed results.
Given a sorted array of integers and an integer called target, find the element that
equals to the target and return its index. Select the correct code that fills the
___
in the given code snippet.
1def binary_search(arr, target):
2 left, right = 0, len(arr) - 1
3 while left ___ right:
4 mid = (left + right) // 2
5 if arr[mid] == target:
6 return mid
7 if arr[mid] < target:
8 ___ = mid + 1
9 else:
10 ___ = mid - 1
11 return -1
12
1public static int binarySearch(int[] arr, int target) {
2 int left = 0;
3 int right = arr.length - 1;
4
5 while (left ___ right) {
6 int mid = left + (right - left) / 2;
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
1function binarySearch(arr, target) {
2 let left = 0;
3 let right = arr.length - 1;
4
5 while (left ___ right) {
6 let mid = left + Math.trunc((right - left) / 2);
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!