Flight Discount

Problem Overview

Aspect Details
Problem Find cheapest route from city 1 to city n with one 50% discount coupon
Input n cities, m directed flights with costs
Output Minimum total cost
Technique State-space Dijkstra
Time Complexity O((n + m) log n)
Space Complexity O(n + m)

Learning Goals

  1. State-space Dijkstra: Learn to extend classic Dijkstra by adding state dimensions
  2. Problem modeling: Transform “use one coupon” into a graph with extra state
  3. Multi-dimensional shortest path: Handle problems where decisions affect future options

Problem Statement

You have n cities and m one-way flights. Each flight has a price. You have one discount coupon that reduces any single flight’s price by 50%. Find the minimum cost to travel from city 1 to city n.

Constraints:

  • 1 <= n <= 10^5
  • 1 <= m <= 2 x 10^5
  • 1 <= flight cost <= 10^9

Example:

Input:
3 4
1 2 3
2 3 6
1 3 10
2 1 1

Output: 6

Explanation: Path 1 -> 2 -> 3 with discount on edge (2,3)
Cost = 3 + 6/2 = 3 + 3 = 6

Key Insight: State-Space Extension

The core insight is to add a state dimension tracking whether we’ve used the coupon.

Instead of dist[node], we maintain:

dist[node][used_coupon]

where used_coupon is:

  • 0: coupon not yet used
  • 1: coupon already used

This transforms the problem into standard Dijkstra on an expanded state graph.

State Transitions

From state (u, 0) - coupon NOT yet used:

  • Go to (v, 0) paying full price (save coupon for later)
  • Go to (v, 1) paying half price (use coupon now)

From state (u, 1) - coupon already used:

  • Go to (v, 1) paying full price only

Visual: State Graph

Original Graph:              State-Space Graph:

    3         6                    (1,0) ----3----> (2,0) ----6----> (3,0)
1 -----> 2 -----> 3                  |  \            |  \

|                 ^                  |   \1.5        |   \3

|       10        |                  |    \          |    \
+------------ ----+                  |     v         |     v
                                     |    (2,1) ----6----> (3,1)
                                     |                      ^
                                     +----------5-----------+

State (x, 0) = at node x, coupon available
State (x, 1) = at node x, coupon used

Dry Run

Input: 3 cities, 4 flights

  • 1 -> 2, cost 3
  • 2 -> 3, cost 6
  • 1 -> 3, cost 10
  • 2 -> 1, cost 1

Priority Queue Processing:

Step Pop from PQ dist[node][state] Add to PQ
Init - dist[1][0] = 0 (0, 1, 0)
1 (0, 1, 0) dist[2][0]=3, dist[2][1]=1, dist[3][0]=10, dist[3][1]=5 (3,2,0), (1,2,1), (10,3,0), (5,3,1)
2 (1, 2, 1) dist[3][1]=min(5, 1+6)=5 -
3 (3, 2, 0) dist[3][0]=min(10,9)=9, dist[3][1]=min(5,3+3)=5 (9,3,0)
4 (5, 3, 1) DESTINATION with coupon used -

Answer: min(dist[3][0], dist[3][1]) = min(9, 5) = 5

Wait, let me recalculate: Path 1->2->3 with discount on (2,3) = 3 + 3 = 6. Path 1->3 with discount = 5. So answer is 5.

Python Solution

import heapq
from typing import List, Tuple

def flight_discount(n: int, flights: List[Tuple[int, int, int]]) -> int:
  """
  Find minimum cost from city 1 to city n with one 50% discount.

  Args:
    n: number of cities (1-indexed)
    flights: list of (from, to, cost) tuples

  Returns:
    Minimum cost to reach city n from city 1
  """
  INF = float('inf')

  # Build adjacency list
  adj = [[] for _ in range(n + 1)]
  for u, v, cost in flights:
    adj[u].append((v, cost))

  # dist[node][used] = minimum cost to reach node with coupon state
  dist = [[INF] * 2 for _ in range(n + 1)]
  dist[1][0] = 0

  # Priority queue: (cost, node, used_coupon)
  pq = [(0, 1, 0)]

  while pq:
    d, u, used = heapq.heappop(pq)

    # Skip if we've found a better path
    if d > dist[u][used]:
      continue

    for v, cost in adj[u]:
      # Option 1: Don't use coupon on this edge
      new_dist = d + cost
      if new_dist < dist[v][used]:
        dist[v][used] = new_dist
        heapq.heappush(pq, (new_dist, v, used))

      # Option 2: Use coupon on this edge (if not already used)
      if used == 0:
        new_dist = d + cost // 2
        if new_dist < dist[v][1]:
          dist[v][1] = new_dist
          heapq.heappush(pq, (new_dist, v, 1))

  # Answer: best of reaching n with or without using coupon
  return min(dist[n][0], dist[n][1])


# Example usage
if __name__ == "__main__":
  n, m = 3, 4
  flights = [(1, 2, 3), (2, 3, 6), (1, 3, 10), (2, 1, 1)]
  print(flight_discount(n, flights))  # Output: 5

Common Mistakes

Mistake Why It’s Wrong Fix
Only checking dist[n][1] Optimal path might not use the coupon at all Check min(dist[n][0], dist[n][1])
Using float for half price Integer division is required; floats cause precision errors Use cost // 2 or cost / 2 (integer division)
Not using long long in C++ Costs can be up to 10^9, path sums can overflow int Use long long throughout
Forgetting to skip processed states Causes TLE from processing same state multiple times Add if (d > dist[u][used]) continue;

Alternative Approach: Two Dijkstras

Instead of state-space Dijkstra, we can:

  1. Run Dijkstra forward from city 1: dist_from[u] = min cost from 1 to u
  2. Run Dijkstra backward from city n: dist_to[v] = min cost from v to n
  3. For each edge (u, v, cost), try using discount on it:
    total = dist_from[u] + cost/2 + dist_to[v]
    
  4. Answer = min over all edges
def flight_discount_two_dijkstras(n, flights):
  """Alternative: Two Dijkstra runs + edge enumeration"""
  INF = float('inf')

  # Forward graph and backward graph
  adj_fwd = [[] for _ in range(n + 1)]
  adj_bwd = [[] for _ in range(n + 1)]

  for u, v, cost in flights:
    adj_fwd[u].append((v, cost))
    adj_bwd[v].append((u, cost))

  def dijkstra(start, adj):
    dist = [INF] * (n + 1)
    dist[start] = 0
    pq = [(0, start)]
    while pq:
      d, u = heapq.heappop(pq)
      if d > dist[u]:
        continue
      for v, cost in adj[u]:
        if d + cost < dist[v]:
          dist[v] = d + cost
          heapq.heappush(pq, (dist[v], v))
    return dist

  dist_from = dijkstra(1, adj_fwd)    # From city 1
  dist_to = dijkstra(n, adj_bwd)      # To city n (backward)

  # Try discount on each edge
  ans = dist_from[n]  # No discount case
  for u, v, cost in flights:
    if dist_from[u] < INF and dist_to[v] < INF:
      ans = min(ans, dist_from[u] + cost // 2 + dist_to[v])

  return ans

Trade-offs:

  • Same time complexity: O((n + m) log n)
  • Two Dijkstra approach uses slightly more code but might be conceptually clearer
  • State-space approach generalizes better to k coupons

Complexity Analysis

Time Complexity: O((n + m) log n)

  • Each state (node, used) is processed at most once
  • Total states: 2n
  • Each edge creates at most 2 transitions per state
  • Priority queue operations: O(log(2n)) = O(log n)

Space Complexity: O(n + m)

  • Adjacency list: O(m)
  • Distance array: O(2n) = O(n)
  • Priority queue: O(n) states at most
  • CSES Flight Routes - k shortest paths
  • LeetCode 787 - Cheapest Flights Within K Stops (similar state-space idea)
  • CSES Investigation - Shortest path with additional queries