DSA sheet · Graphs · Dijkstra pattern

Cheapest Flights Within K Stops

This looks like plain Dijkstra (cheapest route from src to dst), but there's a twist: you may make at most k stops. That one limit breaks the usual "keep only the cheapest arrival" rule, because a more expensive arrival with fewer stops can be the only one that still reaches the destination in time. The teacher first solves it with a min-heap (correct, but it ran out of memory on LeetCode), then shows that a plain BFS queue is enough here and is accepted. This page also adds a third, round-by-round version (Bellman-Ford style), which has the cleanest guarantee.

Every problem below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

Directed, weighted graph

Cities are nodes, flights are edges. A flight [from, to, price] is a directed edge (one way: a flight from 0 to 1 does not mean there's one from 1 to 0) with a weight (the price). The cost of a trip is the sum of its flight prices.

Adjacency list

flights → adjacency list (directed)
n = 4
flights = [[0, 1, 100], [1, 2, 100], [2, 0, 100], [1, 3, 600], [2, 3, 200]]

graph = [[] for _ in range(n)]
for u, v, price in flights:
    graph[u].append((v, price))      # one way only

print(graph)   # [[(1, 100)], [(2, 100), (3, 600)], [(0, 100), (3, 200)], []]

Stops vs flights

A stop is a city you land in between the start and the destination. The start and the destination don't count, the same way the station you leave from and the station you arrive at aren't "stops" on a train trip.

  0 ——→ 1 ——→ 2 ——→ 3
        ^     ^
      stop  stop        3 flights = 2 stops

So flights = stops + 1. "At most k stops" means "at most k + 1 flights". In the code, the teacher's counter (she calls it stops) is really the number of flights taken so far: 0 at src, +1 for every flight. A node reached with counter ≤ k may still take one more flight; a node reached with counter k + 1 may not.

BFS layers, visited, and Dijkstra (quick recap)

This problem adds a second number per route, the stops. Dijkstra's "keep only the cheapest" rule thinks about cost only. That's exactly where it goes wrong here (Part A, step 4).


Part A · Min-heap with (cost, node, stops)

LeetCode 787

1The question in simple words

n cities, numbered 0 to n−1. flights[i] = [from, to, price]. Given src, dst and k, return the cheapest price from src to dst using at most k stops. If there's no such route, return −1.

The teacher's picture (rebuilt from the routes and totals she reads out), with src = 0, dst = 3, k = 2:

          100
   (0) ————————→ (1)
    |            /
 400|       200 /
    ↓          ↙
   (2) ——————500——————→ (3)
     \                  ↑
   100\                 |100
       ↘                |
        (4) ————————————┘
routepriceflightsstopsallowed (k = 2)?
0 → 2 → 3400 + 500 = 90021yes
0 → 1 → 2 → 3100 + 200 + 500 = 80032yes
0 → 2 → 4 → 3400 + 100 + 100 = 60032yes ← cheapest allowed
0 → 1 → 2 → 4 → 3100 + 200 + 100 + 100 = 50043no, 3 stops

The overall cheapest route (500) is not allowed. The answer is 600.

Pencil note: in the video she compares 900, 800 and 500 and says the answer is 800. She misses 0 → 2 → 4 → 3 = 600, which also has only 2 stops. Her board run skips it because of the "only if cheaper" rule. That's the very bug she fixes later in the video (step 4 below), and her final code does return 600.

2What the constraints tell us

3Intuition

Do Dijkstra, but let each heap entry also carry how many flights it has used. The heap still pops the cheapest entry first, so the first time dst is popped, that's the cheapest allowed price. Entries that have used up their flights are not expanded any more.

The big change from Dijkstra: don't throw away an arrival just because it is more expensive, if it used fewer stops. It may be the only one with enough stops left to finish.

4Building the logic from her examples

What goes into the heap? → (cost, node, stops)

Start: (0, src, 0) and dist[src] = 0. Each flight adds the price to cost and 1 to stops.

Rule 1: popping dst → return the cost right away

The heap only ever contains allowed routes (we never push a route with too many flights). It pops them cheapest first. So the first popped entry for dst is the cheapest allowed route. Return it; no need to empty the heap. In her words, in the best or average case you finish early instead of exploring everything.

Rule 2: too many flights → don't expand

If we pop a node that is not dst and stops > k (we've already taken k + 1 flights), one more flight would make k + 1 stops. Skip it: continue.

Doubt: why stops > k and not stops >= k?
→ stops counts flights. With exactly k flights we've made k − 1 stops, so one more flight lands somewhere with k stops made in between, which is allowed. Only at k + 1 flights must we stop. That's the "k + 1 is allowed" point she makes at the start.

Rule 3, the new one: keep a pricier arrival if it used fewer stops

This is the heart of the problem. Let's run plain Dijkstra's push rule, "push only if new_cost < dist[nei]", on her picture (this is her first board run):

poppopped (cost, node, stops)pushes with the "only if cheaper" ruledist[0..4] after
1(0, 0, 0)(100, 1, 1), (400, 2, 1)[0, 100, 400, ∞, ∞]
2(100, 1, 1)(300, 2, 2): 300 < 400[0, 100, 300, ∞, ∞]
3(300, 2, 2)(800, 3, 3), (400, 4, 3)[0, 100, 300, 800, 400]
4(400, 2, 1)to 3: 900 < 800? no · to 4: 500 < 400? no → thrown awaysame
5(400, 4, 3)3 flights used, not dst → can't fly onsame
6(800, 3, 3)dst → return 800

The arrival at node 4 with cost 500 but only 2 flights was thrown away because dist[4] already held 400. But that 400 came with 3 flights and is stuck: it can't fly to 3. The 500 one could have flown on to 3 for a total of 600 with 2 stops. Wrong answer.

Her fix (the "change one line" part of the video): even if the new cost is not smaller, still push it if this route has stops left to continue. "Greater now" doesn't mean "useless later".

  if new_cost < dist[nei]  or  stops < k:
      push (new_cost, nei, stops + 1)
      dist[nei] = min(dist[nei], new_cost)
Doubt: why stops < k in that condition?
→ The neighbour would be reached with stops + 1 flights. If that's ≤ k, the neighbour can still fly on, so this route might lead somewhere useful even though it's pricier: push it. If stops == k, the neighbour is reached with k + 1 flights and can't fly further. Then it only matters if it is dst and cheaper than what dst already has, which the new_cost < dist[nei] half checks.
Doubt: why dist[nei] = min(...) and not just = new_cost?
→ Because now we can enter this block with a cost that is not smaller (through the or). Writing it directly could raise dist from 400 back up to 500. She spots this while coding and switches to min.

So why exactly does plain Dijkstra fail here?

Dijkstra's rule "once I know a cheaper way to X, every pricier way to X is useless" is true when cost is the only thing that matters. Here a route has two numbers, cost and stops used. A route can be worse in cost but better in stops, and then neither one beats the other.

arrival at node 4costflights usedcan still fly to 3?
via 0 → 1 → 2 → 4400 (cheaper)3no (k = 2 allows 3 flights in total)
via 0 → 2 → 4500 (pricier)2yes → 600 at node 3

And if you ignore stops completely, plain Dijkstra returns 500 (route 0→1→2→4→3), which uses 3 stops and isn't allowed. So plain Dijkstra fails in both directions: without a stop check it breaks the limit, and with a stop check but "cheapest only" pruning it gives up on routes it needed.

WRONG: plain Dijkstra pruning (cost only) plus a stops check
import heapq

def dijkstra_cost_only(n, flights, src, dst, k):
    graph = [[] for _ in range(n)]
    for u, v, price in flights:
        graph[u].append((v, price))
    dist = [float('inf')] * n
    dist[src] = 0
    pq = [(0, src, 0)]
    while pq:
        cost, node, stops = heapq.heappop(pq)
        if node == dst:
            return cost
        if cost > dist[node] or stops > k:
            continue
        for nei, price in graph[node]:
            if cost + price < dist[nei]:        # BUG: pricier-but-fewer-stops routes are lost
                dist[nei] = cost + price
                heapq.heappush(pq, (cost + price, nei, stops + 1))
    return -1

her = [[0, 1, 100], [0, 2, 400], [1, 2, 200], [2, 3, 500], [2, 4, 100], [4, 3, 100]]
print(dijkstra_cost_only(5, her, 0, 3, 2))   # 800  (wrong: the answer is 600)

5Approach steps

  1. Build a directed adjacency list.
  2. dist = [inf] * n, dist[src] = 0, heap = [(0, src, 0)].
  3. Pop the cheapest (cost, node, stops).
  4. If node is dst → return cost.
  5. If stops > k → continue (no flights left).
  6. For each flight (nei, price): new_cost = cost + price. If new_cost < dist[nei] or stops < k: push (new_cost, nei, stops + 1) and set dist[nei] = min(dist[nei], new_cost).
  7. Heap empty and dst never popped → return −1.

6Code (Python)

Cheapest Flights with a min-heap (correct, but heavy)
import heapq

class Solution:
    def findCheapestPrice(self, n, flights, src, dst, k):
        graph = [[] for _ in range(n)]
        for u, v, price in flights:
            graph[u].append((v, price))             # directed

        dist = [float('inf')] * n
        dist[src] = 0
        pq = [(0, src, 0)]                          # (cost, node, flights used)

        while pq:
            cost, node, stops = heapq.heappop(pq)
            if node == dst:                         # first pop of dst = cheapest allowed
                return cost
            if stops > k:                           # no flights left from here
                continue
            for nei, price in graph[node]:
                new_cost = cost + price
                if new_cost < dist[nei] or stops < k:   # cheaper, OR still has stops to spare
                    heapq.heappush(pq, (new_cost, nei, stops + 1))
                    dist[nei] = min(dist[nei], new_cost)
        return -1

7Code line by line

linewhat it means
graph[u].append((v, price))Flights are one way, so only u's list gets it.
pq = [(0, src, 0)]At src: paid 0, taken 0 flights. Cost first, so the heap sorts by price.
cost, node, stops = heapq.heappop(pq)The cheapest route found so far that still waits to be explored.
if node == dst: return costEvery route in the heap is allowed, and they come out cheapest first, so this is the answer.
if stops > k: continuek + 1 flights already used: landing anywhere else would add a stop too many.
new_cost = cost + pricePrice of the route extended by this flight.
if new_cost < dist[nei] or stops < k:Keep the route if it's the cheapest so far to nei, or if it will still have flights left after landing. This or is the one line that differs from Dijkstra.
heappush(pq, (new_cost, nei, stops + 1))One more flight taken.
dist[nei] = min(dist[nei], new_cost)dist keeps the cheapest cost seen; never let a pricier route raise it.
return -1dst was never popped: no route within k stops.

8Dry run: heap and dist after every pop

Her picture, src 0, dst 3, k = 2. Heap entries are (cost, node, stops), shown in sorted order. dist = [node0 … node4].

poppoppedflights checkedheap AFTERdist AFTER
1(0, 0, 0)→1: 100 < ∞ ✓ · →2: 400 < ∞ ✓(100,1,1) (400,2,1)[0, 100, 400, ∞, ∞]
2(100, 1, 1)→2: 300 < 400 ✓(300,2,2) (400,2,1)[0, 100, 300, ∞, ∞]
3(300, 2, 2)→3: 800 < ∞ ✓ · →4: 400 < ∞ ✓(400,2,1) (400,4,3) (800,3,3)[0, 100, 300, 800, 400]
4(400, 2, 1)→3: 900, not cheaper, but stops 1 < 2 ✓ push
→4: 500, not cheaper, but stops 1 < 2 ✓ push
(400,4,3) (500,4,2) (800,3,3) (900,3,2)[0, 100, 300, 800, 400] (min keeps 800, 400)
5(400, 4, 3)3 > k → no flights left, skip(500,4,2) (800,3,3) (900,3,2)same
6(500, 4, 2)→3: 600 < 800 ✓(600,3,3) (800,3,3) (900,3,2)[0, 100, 300, 600, 400]
7(600, 3, 3)node 3 = dst → return 600

Compare pop 4 with the wrong board run in step 4: the or stops < k keeps (500, 4, 2) alive, and that's the entry that finds 600.

node 4 after pop 4: two live entries
  (400, 4, 3)  cheap, but stuck
  (500, 4, 2)  pricier, can still fly
winning route
  (0) —400—→ (2) —100—→ (4) —100—→ (3)
  flights 3, stops 2, total 600

9Complexity & why LeetCode said "Memory Limit Exceeded"

Doubt: is k × E really the limit?
→ It's actually worse. While stops < k, the condition pushes every route, even a second route to the same node with the same stop count. So the heap can hold one entry per path of up to k flights, and on dense graphs the number of paths grows like (number of neighbours)k. That's why the memory blows up.
Doubt: can a heap version be made light? (not in the video)
→ Yes: remember, for each node, the fewest flights it has been expanded with. The heap pops cheapest first, so if a node comes out again with no fewer flights than before, it is worse in both cost and stops: skip it. Each node is then expanded at most k + 2 times, so the heap gets at most about (k + 2) × E entries.
extra: heap version that skips routes worse in both cost and stops
import heapq

def cheapest_heap_pruned(n, flights, src, dst, k):
    graph = [[] for _ in range(n)]
    for u, v, price in flights:
        graph[u].append((v, price))
    fewest = [float('inf')] * n             # fewest flights a node was expanded with
    pq = [(0, src, 0)]
    while pq:
        cost, node, stops = heapq.heappop(pq)
        if node == dst:
            return cost
        if stops > k or stops >= fewest[node]:   # out of flights, or cheaper AND not-more-stops route already used
            continue
        fewest[node] = stops
        for nei, price in graph[node]:
            heapq.heappush(pq, (cost + price, nei, stops + 1))
    return -1

her = [[0, 1, 100], [0, 2, 400], [1, 2, 200], [2, 3, 500], [2, 4, 100], [4, 3, 100]]
print(cheapest_heap_pruned(5, her, 0, 3, 2))   # 600
Remember Part AHeap of (cost, node, stops). Return when dst is popped. Skip when stops > k. Push when cheaper OR stops < k, because a pricier route with stops to spare can still win. Correct, but heavy: MLE on LeetCode.

Part B · Plain BFS queue (her accepted solution)

1The question

Same question. The idea: we don't really need the heap. The real limit here is the number of flights, and a plain BFS queue already handles routes in order of flights: first everything 1 flight away, then 2 flights, and so on. So explore all allowed routes and read dist[dst] at the end.

2What the constraints tell us

k × E ≈ 5·105 → an O(k × E) method with no log and no pile-up of entries fits easily in both time and memory.

3Intuition

Think of BFS levels as "number of flights taken". Level 1 = cities 1 flight from src, level 2 = 2 flights, …, and we stop expanding after level k + 1. Along the way, dist[x] keeps the cheapest price seen for x. Because the queue hands out routes in order of flights used, any route that arrives later has used at least as many flights as earlier ones. So if it's not cheaper either, it's worse in both ways and safe to drop. That's why "push only if cheaper" is correct again here, while it wasn't in the heap version (the heap hands routes out by cost, so a fewer-flights route can arrive later).

4What changes from Part A (her edits on screen)

  1. The heap becomes a normal queue (deque, first in, first out).
  2. Remove "if node == dst: return cost". With a normal queue the first arrival at dst is the one with the fewest flights, not the cheapest. A later route with more flights may be cheaper (LeetCode example below). So we explore everything allowed and read dist[dst] at the end.
  3. Keep "if stops > k: continue". We still must not expand routes that are out of flights.
  4. Remove the or stops < k. Push only if new_cost < dist[nei], and then set dist[nei] = new_cost directly (it's smaller, so no min needed).
  5. At the end: dist[dst] still infinity → −1, else dist[dst].
Doubt: why is dist[dst] guaranteed to use at most k stops?
→ We only push from entries with stops ≤ k, so every route that ever writes into dist has at most k + 1 flights, which means at most k stops. dist never holds a too-long route.
Doubt: but dist[4] in her picture gets 400 from a 3-flight route. Can't that block the useful 500 route like before?
→ Not in BFS. The 2-flight route to 4 (cost 500) comes out of the queue before any 3-flight route, because the queue is in order of flights. So 500 is written first; the 3-flight 400 comes later, and it's cheaper, so it is also kept. Both get explored. In the heap version, 400 came out first just because it was cheaper, and that's what blocked 500. Watch pop 3 and pop 4 in the dry run.
Doubt: she starts by saying "I can go with DFS". Does DFS work?
→ Her heap version behaves like Dijkstra, not like DFS, and her final version is BFS. A plain DFS would have to try every path up to k + 1 flights, which is exponential. BFS's natural "level = flights" order is what makes this problem easy.

5Approach steps

  1. Build the directed adjacency list.
  2. dist = [inf] * n, dist[src] = 0, queue = [(0, src, 0)].
  3. Pop from the front: (cost, node, stops). If stops > k → continue.
  4. For every flight (nei, price): if cost + price < dist[nei] → update dist[nei] and append (cost + price, nei, stops + 1).
  5. Return −1 if dist[dst] is infinity, else dist[dst].

6Code (Python)

Cheapest Flights with a plain BFS queue (accepted)
from collections import deque

class Solution:
    def findCheapestPrice(self, n, flights, src, dst, k):
        graph = [[] for _ in range(n)]
        for u, v, price in flights:
            graph[u].append((v, price))

        dist = [float('inf')] * n
        dist[src] = 0
        queue = deque([(0, src, 0)])                # (cost, node, flights used)

        while queue:
            cost, node, stops = queue.popleft()
            if stops > k:                           # out of flights
                continue
            for nei, price in graph[node]:
                new_cost = cost + price
                if new_cost < dist[nei]:            # cheaper than anything seen
                    dist[nei] = new_cost
                    queue.append((new_cost, nei, stops + 1))

        return -1 if dist[dst] == float('inf') else dist[dst]

7Code line by line

linewhat it means
queue = deque([(0, src, 0)])Normal FIFO queue. Entries come out in order of flights used.
cost, node, stops = queue.popleft()Oldest entry first, not cheapest. No early return.
if stops > k: continuek + 1 flights already: can't fly on.
if new_cost < dist[nei]:Only a cheaper arrival is worth exploring; a pricier one that came later also has no fewer flights, so it's worse in both ways.
dist[nei] = new_costCheapest price to nei so far (always within the stop limit).
queue.append((new_cost, nei, stops + 1))Explore onward from nei later.
return -1 if dist[dst] == float('inf') else dist[dst]Never reached in time → −1.

8Dry run: queue and dist after every pop

Her picture (src 0, dst 3, k = 2)

poppopped (cost, node, stops)flights checkedqueue AFTER (front first)dist AFTER
1(0, 0, 0)→1: 100 ✓ · →2: 400 ✓(100,1,1) (400,2,1)[0, 100, 400, ∞, ∞]
2(100, 1, 1)→2: 300 < 400 ✓(400,2,1) (300,2,2)[0, 100, 300, ∞, ∞]
3(400, 2, 1)→3: 900 ✓ · →4: 500 ✓(300,2,2) (900,3,2) (500,4,2)[0, 100, 300, 900, 500]
4(300, 2, 2)→3: 800 < 900 ✓ · →4: 400 < 500 ✓(900,3,2) (500,4,2) (800,3,3) (400,4,3)[0, 100, 300, 800, 400]
5(900, 3, 2)node 3 has no flights out(500,4,2) (800,3,3) (400,4,3)same
6(500, 4, 2)→3: 600 < 800 ✓(800,3,3) (400,4,3) (600,3,3)[0, 100, 300, 600, 400]
7–9(800,3,3) (400,4,3) (600,3,3)each has stops 3 > 2 → skipemptysame

dist[3] = 600 ✓. The 2-flight arrival at node 4 (500, pop 3) was written before the cheaper 3-flight one (400, pop 4), so it wasn't blocked.

LeetCode example 1 (why we can't return at the first dst arrival)

  flights: 0→1 (100), 1→2 (100), 2→0 (100), 1→3 (600), 2→3 (200)
  src 0, dst 3, k = 1  (at most 2 flights)
  1. pop (0,0,0) → 1 gets 100. queue: (100,1,1).
  2. pop (100,1,1) → 2 gets 200, 3 gets 700. queue: (200,2,2) (700,3,2).
  3. pop (200,2,2) → stops 2 > 1, skip. (Its route 0→1→2→3 = 400 would use 2 stops.)
  4. pop (700,3,2) → skip. Queue empty. dist[3] = 700 ✓.

Here the first arrival happens to be the answer, but in her picture the first arrival at 3 was 900 (pop 3) and the answer turned out to be 600 (pop 6). That's why the BFS version must not return early.

9Complexity & remember

To be exact: in an unlucky order a node can be improved and queued more than once in the same level, so k × E is a typical figure rather than a hard cap. Part C gives a version where (k + 1) × E is guaranteed.

Remember Part BFIFO queue of (cost, node, stops) · skip if stops > k · push only if cheaper · no early return · answer is dist[dst] (or −1).
Her closing lesson: here plain BFS was enough. In other problems (like the next one) the heap is really needed. Decide from the problem.

Part C · Bellman-Ford style rounds (extra)

Not in the video, but it's the classic answer for "shortest path with at most k + 1 edges", and it makes the BFS idea completely strict.

1Idea

Keep dist = "cheapest price using at most r flights". Start with r = 0: only src is 0. Each round allows one more flight: for every flight u → v, try dist[u] + price for v. After k + 1 rounds, dist[dst] is the cheapest price with at most k + 1 flights.

2The one trap: copy the array each round

Read from the old round and write into a copy. If you updated in place, a round could use a value written earlier in the same round and chain two flights in one round, breaking the count.

Doubt: show me the trap.
→ Flights 0→1 (1), 1→2 (1), k = 0 (only 1 flight). Round 1 in place: 0→1 makes dist[1] = 1, then 1→2 reads that fresh 1 and makes dist[2] = 2, a 2-flight route in a 1-flight round. With a copy, round 1 reads the old dist[1] = ∞, so dist[2] stays ∞ ✓.

3Steps

  1. dist = [inf] * n, dist[src] = 0.
  2. Repeat k + 1 times: new = dist[:]; for each flight u → v: if dist[u] + price < new[v], set new[v]. Then dist = new.
  3. Return −1 if dist[dst] is infinity, else dist[dst].

4Code (Python)

Cheapest Flights with k + 1 rounds
class Solution:
    def findCheapestPrice(self, n, flights, src, dst, k):
        dist = [float('inf')] * n
        dist[src] = 0
        for _ in range(k + 1):                      # one round = one more flight allowed
            new = dist[:]                           # read old, write copy
            for u, v, price in flights:
                if dist[u] + price < new[v]:
                    new[v] = dist[u] + price
            dist = new
        return -1 if dist[dst] == float('inf') else dist[dst]

5Dry run on her picture (k = 2 → 3 rounds)

after roundmeaningdist[0..4]
00 flights[0, ∞, ∞, ∞, ∞]
1≤ 1 flight[0, 100, 400, ∞, ∞]
2≤ 2 flights[0, 100, 300, 900, 500]
3≤ 3 flights[0, 100, 300, 600, 400]

Round 3 computes dist[3] from round 2's values: min(900, 300 + 500, 500 + 100) = 600. It reads the 2-flight 500 for node 4, not round 3's fresh 400 (which used 3 flights). That's the copy at work.

6Complexity

Time O((k + 1) × E) exactly, space O(n). Short, strict, and no graph building needed.

Remember Part Ck + 1 rounds · each round relaxes every flight from a copy of the last round.

Part D · Revision page

A: heap (cost, node, stops)B: BFS queueC: k + 1 rounds
order of workcheapest firstfewest flights firstround by round
push / update whencheaper or stops < kcheapercheaper than in the copy
early returnyes, when dst is poppedno, read dist[dst] at the endno
stops checkstops > k → skipstops > k → skiploop runs k + 1 times
costheavy; MLE on LeetCode≈ O(kE), acceptedO((k + 1)E), O(n) space
plain Dijkstra on cost only…result on her picture (k = 2)
…ignoring stops500 (route with 3 stops, not allowed)
…with a stops check, but "push only if cheaper"800 (the 600 route was thrown away at node 4)
correct answer600 (0 → 2 → 4 → 3)
If you remember only 5 lines 1. k stops = k + 1 flights. Carry the flights used in every entry.
2. Pop with flights > k (not dst) → can't fly on.
3. Cheaper isn't everything: a pricier route with fewer stops can still win, so Dijkstra's "cheaper only" rule fails.
4. Heap version: push if cheaper OR stops < k; return when dst is popped (heavy).
5. BFS version: queue goes by flights, so "push if cheaper" is safe again; answer = dist[dst] at the end.
Mistakes to avoid ✗ treating k as the number of flights (it's stops: flights = k + 1)
✗ using plain Dijkstra pruning with the heap (loses fewer-stop routes)
✗ returning early at dst in the BFS version (first arrival = fewest flights, not cheapest)
✗ dist[nei] = new_cost in the heap version (must be min)
✗ updating in place in the rounds version (forgetting the copy)
✗ hard-coding 0 instead of src (she did this once on screen)
test it yourself (paste under any of the three solutions)
s = Solution()
her = [[0, 1, 100], [0, 2, 400], [1, 2, 200], [2, 3, 500], [2, 4, 100], [4, 3, 100]]
print(s.findCheapestPrice(5, her, 0, 3, 2))                                                            # 600
print(s.findCheapestPrice(5, her, 0, 3, 3))                                                            # 500
print(s.findCheapestPrice(4, [[0, 1, 100], [1, 2, 100], [2, 0, 100], [1, 3, 600], [2, 3, 200]], 0, 3, 1))  # 700
print(s.findCheapestPrice(3, [[0, 1, 100], [1, 2, 100], [0, 2, 500]], 0, 2, 1))                         # 200
print(s.findCheapestPrice(3, [[0, 1, 100], [1, 2, 100], [0, 2, 500]], 0, 2, 0))                         # 500
print(s.findCheapestPrice(3, [[0, 1, 100]], 0, 2, 1))                                                   # -1

Based on this video: Cheapest Flights Within K Stops