Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

983. Minimum Cost For Tickets - Leetcode Solution

Problem Description

You are given an integer array days where each element represents a day you will travel, and an integer array costs of length 3 where:

  • costs[0] is the cost of a 1-day pass,
  • costs[1] is the cost of a 7-day pass,
  • costs[2] is the cost of a 30-day pass.
Each pass allows for unlimited travel for the given number of consecutive days, starting from the day of purchase. You can buy as many passes as you want, but you must cover every day in days with at least one pass. Your task is to find the minimum total cost required to travel on all the days in days.

Constraints:

  • 1 ≤ days.length ≤ 365
  • 1 ≤ days[i] ≤ 365, with days strictly increasing
  • 1 ≤ costs[i] ≤ 1000
  • There is exactly one valid way to cover all the travel days.
  • You cannot reuse or split passes; each covers a fixed window.

Thought Process

At first glance, the problem seems to invite a brute-force approach: for each travel day, try every possible combination of ticket purchases and find the minimum cost. However, this would be highly inefficient, as the number of combinations grows exponentially with the number of days.

To optimize, we notice that for each travel day, our decision is local: on that day, we can buy a 1-day, 7-day, or 30-day pass. Each choice affects which future days are covered. This is a classic case for dynamic programming, where we can build up the solution by solving smaller subproblems and reusing their results.

The key insight is that the minimum cost to cover all days up to a certain day can be computed from the minimum cost to cover previous days, based on which pass we buy at each step.

Solution Approach

We use dynamic programming to solve the problem efficiently. Here is a step-by-step breakdown:

  1. State Definition:
    • Let dp[i] represent the minimum cost to cover all travel days from days[i] to the end.
  2. Recurrence Relation:
    • For each index i, we have three choices:
      • Buy a 1-day pass: costs[0] + dp[next_i_1] where next_i_1 is the first day after days[i].
      • Buy a 7-day pass: costs[1] + dp[next_i_7] where next_i_7 is the first day after days[i] + 6.
      • Buy a 30-day pass: costs[2] + dp[next_i_30] where next_i_30 is the first day after days[i] + 29.
      We take the minimum of these three choices.
  3. Implementation:
    • We process days in reverse (from last to first), filling in the dp array.
    • For each day, we use binary search or a moving pointer to find the next index not covered by the current pass.
    • The base case is when i is beyond the last index, where the cost is 0.
  4. Why This Works:
    • Dynamic programming ensures we compute the minimum cost for each subproblem only once, leading to an efficient solution.
    • We use arrays (or memoization) for O(1) lookups of previous results.

Example Walkthrough

Sample Input:

  • days = [1,4,6,7,8,20]
  • costs = [2,7,15]

Let's walk through the solution:

  1. On day 1, we can:
    • Buy a 1-day pass for $2 (covers day 1), next day is 4.
    • Buy a 7-day pass for $7 (covers days 1,4,6,7,8), next day is 20.
    • Buy a 30-day pass for $15 (covers all days), next day is beyond input.
  2. If we buy a 1-day pass:
    • Cost: $2 + min cost to cover days [4,6,7,8,20]
  3. If we buy a 7-day pass:
    • Cost: $7 + min cost to cover days [20]
  4. If we buy a 30-day pass:
    • Cost: $15, all days are covered.
  5. Recursively, we find that the minimum total cost is $11 (buy a 7-day pass for days 1-8, and a 1-day pass for day 20).

Time and Space Complexity

Brute-force:

  • Time: Exponential, as each day can branch into three choices.
  • Space: O(N) for recursion stack.
Optimized (Dynamic Programming):
  • Time: O(N), where N is the number of travel days, since each subproblem is solved once, and finding the next index can be optimized to O(1) with pointers.
  • Space: O(N), for the DP array.

The optimized approach is efficient even for the maximum constraints.

Summary

The Minimum Cost For Tickets problem is a classic example of optimizing local decisions using dynamic programming. By framing the problem as a series of overlapping subproblems—each representing the minimum cost from a certain day onward—we avoid redundant computation and efficiently find the optimal solution. The elegance lies in the simplicity of the recurrence and the efficient use of data structures to store intermediate results.

Code Implementation

class Solution:
    def mincostTickets(self, days, costs):
        n = len(days)
        dp = [0] * (n + 1)
        durations = [1, 7, 30]
        
        for i in range(n - 1, -1, -1):
            dp[i] = float('inf')
            for k in range(3):
                j = i
                # Find the next index not covered by this pass
                while j < n and days[j] < days[i] + durations[k]:
                    j += 1
                dp[i] = min(dp[i], costs[k] + dp[j])
        return dp[0]
      
class Solution {
public:
    int mincostTickets(vector<int>& days, vector<int>& costs) {
        int n = days.size();
        vector<int> dp(n + 1, 0);
        vector<int> durations = {1, 7, 30};
        for (int i = n - 1; i >= 0; --i) {
            dp[i] = INT_MAX;
            for (int k = 0; k < 3; ++k) {
                int j = i;
                while (j < n && days[j] < days[i] + durations[k]) {
                    ++j;
                }
                dp[i] = min(dp[i], costs[k] + dp[j]);
            }
        }
        return dp[0];
    }
};
      
class Solution {
    public int mincostTickets(int[] days, int[] costs) {
        int n = days.length;
        int[] dp = new int[n + 1];
        int[] durations = {1, 7, 30};
        for (int i = n - 1; i >= 0; --i) {
            dp[i] = Integer.MAX_VALUE;
            for (int k = 0; k < 3; ++k) {
                int j = i;
                while (j < n && days[j] < days[i] + durations[k]) {
                    j++;
                }
                dp[i] = Math.min(dp[i], costs[k] + dp[j]);
            }
        }
        return dp[0];
    }
}
      
var mincostTickets = function(days, costs) {
    const n = days.length;
    const dp = new Array(n + 1).fill(0);
    const durations = [1, 7, 30];
    for (let i = n - 1; i >= 0; --i) {
        dp[i] = Infinity;
        for (let k = 0; k < 3; ++k) {
            let j = i;
            while (j < n && days[j] < days[i] + durations[k]) {
                j++;
            }
            dp[i] = Math.min(dp[i], costs[k] + dp[j]);
        }
    }
    return dp[0];
};