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
- State-space Dijkstra: Learn to extend classic Dijkstra by adding state dimensions
- Problem modeling: Transform “use one coupon” into a graph with extra state
- 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:
- Run Dijkstra forward from city 1:
dist_from[u]= min cost from 1 to u - Run Dijkstra backward from city n:
dist_to[v]= min cost from v to n - For each edge (u, v, cost), try using discount on it:
total = dist_from[u] + cost/2 + dist_to[v] - 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
Related Problems
- CSES Flight Routes - k shortest paths
- LeetCode 787 - Cheapest Flights Within K Stops (similar state-space idea)
- CSES Investigation - Shortest path with additional queries