Cheapest Flights Within K Stops
Có n thành phố, được đánh số từ 0 đến n-1, và một danh sách các chuyến bay một chiều: flights[i] = [from, to, price] là chuyến bay từ thành phố from đến thành phố to, có giá price.
Trả về tổng giá thấp nhất của chuyến đi từ src đến dst với nhiều nhất k điểm dừng trên đường đi, nghĩa là nhiều nhất k+1 chuyến bay. Nếu không tồn tại chuyến đi như vậy, hãy trả về -1.
Hàm
- ninteger
- số lượng thành phố
- flightsinteger-2d-array
- các chuyến bay một chiều, mỗi chuyến có dạng [from, to, price]
- srcinteger
- thành phố nơi chuyến đi bắt đầu
- dstinteger
- thành phố nơi chuyến đi kết thúc
- kinteger
- số trạm dừng tối đa được phép giữa src và dst
- Trả vềinteger
- giá thấp nhất của chuyến đi có tối đa k điểm dừng, hoặc -1
Ràng buộc
2 ≤ n ≤ 10001 ≤ flights.length ≤ 4500flights[i] = [from, to, price]với0 ≤ from, to < nvàfrom ≠ to1 ≤ price ≤ 104- Không có hai chuyến bay nào có cùng cả
fromvàto. 0 ≤ src, dst < nvàsrc ≠ dst0 ≤ k < n
Ví dụ
- Đầu vào
- n = 4flights = [[0, 1, 50], [1, 2, 50], [2, 3, 50], [0, 2, 200], [1, 3, 180]]src = 0dst = 3k = 1
- Đầu ra
- 230
- Giải thích
- Một điểm dừng cho phép hai chuyến bay:
0 → 1 → 3có giá 50 + 180 = 230 và0 → 2 → 3có giá 200 + 50 = 250. Lộ trình rẻ hơn0 → 1 → 2 → 3có hai điểm dừng.
- Đầu vào
- n = 4flights = [[0, 1, 50], [1, 2, 50], [2, 3, 50], [0, 2, 200], [1, 3, 180]]src = 0dst = 3k = 2
- Đầu ra
- 150
- Giải thích
- Hai điểm dừng cho phép đi ba chuyến bay, nên giờ đây
0 → 1 → 2 → 3được phép, với chi phí 50 + 50 + 50 = 150.
- Đầu vào
- n = 4flights = [[0, 1, 50], [1, 2, 50], [2, 3, 50], [0, 2, 200], [1, 3, 180]]src = 0dst = 3k = 0
- Đầu ra
- -1
- Giải thích
- Không có điểm dừng nào, nên chuyến đi phải là một chuyến bay thẳng từ 0 đến 3, nhưng không có chuyến nào như vậy.
Câu hỏi mở rộng
Bạn sẽ trả về những thành phố trong chuyến đi rẻ nhất cùng với giá của chuyến đi đó như thế nào?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Chuyến đi rẻ nhất có thể có quá nhiều điểm dừng, vì vậy chỉ tìm đường đi ngắn nhất là chưa đủ. Giá vé chỉ hữu ích khi đi kèm với số chuyến bay đã thực hiện.
Gọi
cost[c]là giá vé rẻ nhất để đến thành phốcvới tối đarchuyến bay. Duyệt qua từng chuyến bay một lần sẽ biến giá vé này thành giá vé rẻ nhất với tối đar+1chuyến bay. Bạn cần thực hiệnk+1lần duyệt như vậy.Trong mỗi lượt, hãy đọc giá từ lượt trước và ghi vào một bản sao. Nếu cập nhật trực tiếp, một chuyến bay có thể dựa trên mức giá được thiết lập trong cùng lượt, khiến một lượt lén xử lý hai chuyến bay.
Lời giải
Đây là bài toán tìm đường đi ngắn nhất với giới hạn về số chuyến bay. Thuật toán Dijkstra chỉ xét các thành phố sẽ không hiệu quả, vì cách rẻ nhất để đến một thành phố có thể đã dùng hết số điểm dừng cần cho phần còn lại của hành trình. Bellman-Ford phù hợp với giới hạn này: vòng lặp r tìm giá vé thấp nhất với tối đa r chuyến bay, vì vậy bạn chạy k+1 vòng.
Thử mọi tuyến đường với tối đa k+1 chuyến bay
Đúng, nhưng không chạy xong với các test lớn nhất
Ý tưởng
Duyệt mọi lộ trình từ src bằng một ngăn xếp chứa (city, price, flights used). Lộ trình đến dst sẽ ghi lại giá và kết thúc, vì mọi mức giá đều dương. Lộ trình có k+1 chuyến bay sẽ bị loại bỏ; các lộ trình khác được mở rộng bằng mọi chuyến bay khởi hành từ thành phố đó. Mọi chuyến đi hợp lệ đều được tạo, nên mức giá thấp nhất được ghi lại là chính xác.
Số lộ trình tăng theo d mũ k+1, với d là số chuyến bay khởi hành từ một thành phố. Với 4 chuyến bay mỗi thành phố và k = 29, số lộ trình là 4^30 ≈ 1.2 × 10^18.
Thuật toán
- Nhóm các chuyến bay theo thành phố khởi hành.
- Đẩy
(src, 0, 0): thành phố, giá vé tính đến lúc đó, số chuyến bay đã đi. - Lấy một tuyến đường ra. Nếu thành phố của tuyến đường là
dst, giữ lại giá vé nếu đó là giá tốt nhất tính đến lúc đó. - Nếu không, nếu tuyến đường có ít hơn
k+1chuyến bay, đẩy một tuyến đường dài hơn cho mỗi chuyến bay khởi hành từ thành phố đó. - Khi ngăn xếp trống, trả về giá vé tốt nhất, hoặc -1 nếu không tuyến đường nào đến được
dst.
def findCheapestPrice(n, flights, src, dst, k):
out = [[] for _ in range(n)]
for a, b, price in flights:
out[a].append((b, price))
best = -1
# Each entry is a route in progress: (city, price so far, flights taken).
stack = [(src, 0, 0)]
while stack:
city, cost, used = stack.pop()
if city == dst:
if best == -1 or cost < best:
best = cost
continue
# k stops allow at most k+1 flights.
if used == k + 1:
continue
for nxt, price in out[city]:
stack.append((nxt, cost + price, used + 1))
return bestBellman-Ford giới hạn ở k+1 lượt
Ý tưởng
Gọi cost[c] là giá thấp nhất để đến thành phố c với nhiều nhất r chuyến bay. Khi r = 0, chỉ có thể đến src với giá 0. Một chuyến đi đến a với nhiều nhất r chuyến bay, rồi bay chuyến a → b, sẽ là chuyến đi đến b với nhiều nhất r+1 chuyến bay, nên mỗi vòng lặp cập nhật giá cost[a] + price cho b với mỗi chuyến bay. Sau k+1 vòng lặp, cost[dst] là đáp án.
Mỗi vòng lặp đọc giá từ vòng trước và ghi vào một bản sao, nên giá được cập nhật trong một vòng không thể dùng cho chuyến bay khác trong cùng vòng đó. Trong ví dụ đầu tiên với k = 1, giá đến thành phố 2 giảm xuống 100 ở vòng 2, nhưng không còn vòng nào để bay tiếp đến thành phố 3 với giá 150.
Mỗi vòng lặp đọc tất cả chuyến bay một lần: thời gian O(k · E) với E chuyến bay và bộ nhớ O(n) cho hai mảng.
Thuật toán
- Đặt
cost[src] = 0và đặt mọi giá trịcostkhác thành vô cực. - Lặp lại
k+1lần: sao chépcostvàonext. - Với mỗi chuyến bay
[a, b, price]màcost[a]là hữu hạn, nếucost[a] + price < next[b], hãy đặtnext[b] = cost[a] + price. - Thay
costbằngnextvà dừng sớm nếu không có gì thay đổi. - Trả về
cost[dst], hoặc -1 nếu giá trị này vẫn là vô cực.
def findCheapestPrice(n, flights, src, dst, k):
INF = float("inf")
# cost[c]: the cheapest price to reach city c with the flights allowed so far.
cost = [INF] * n
cost[src] = 0
# Each round allows one more flight; k stops means k+1 flights.
for _ in range(k + 1):
# Read last round's prices and write into a copy,
# so one round can never chain two flights.
nxt = cost[:]
changed = False
for a, b, price in flights:
if cost[a] != INF and cost[a] + price < nxt[b]:
nxt[b] = cost[a] + price
changed = True
cost = nxt
if not changed:
break
return -1 if cost[dst] == INF else cost[dst]
Lỗi thường gặp và trường hợp biên
- Đếm số điểm dừng như số chuyến bay.
kđiểm dừng nghĩa là cók+1chuyến bay vàk+1lượt. - Cập nhật ngay tại chỗ. Với
0 → 1,1 → 2,2 → 3, mỗi chuyến có giá 1, theo thứ tự đó, cùng với0 → 3có giá 500 vàk = 0, một lượt nối liền ba chuyến bay và trả về 3, không phải 500. - Chạy Dijkstra thông thường trên các thành phố. Với
0 → 1và1 → 2, mỗi chuyến có giá 1,0 → 2có giá 40,2 → 3có giá 1 vàk = 1, thuật toán chốt thành phố 2 với giá 2 sau hai chuyến bay và bỏ lỡ hành trình0 → 2 → 3có giá 41. - Bay từ một thành phố chưa đến được. Khi giá trị lớn nhất của kiểu int là vô cực,
cost[a] + pricebị tràn thành số âm trong 32 bit. Trước tiên, hãy kiểm tra xemcost[a]có phải là giá trị hữu hạn không.
Câu hỏi thường gặp3
Độ phức tạp thời gian của bài toán Chuyến bay rẻ nhất với tối đa K điểm dừng là gì?
Bellman-Ford giới hạn ở k+1 vòng, mất thời gian O(k · E) với E chuyến bay và cần thêm O(n) bộ nhớ cho hai mảng giá. Nếu một vòng không làm thay đổi giá nào, vòng tiếp theo sẽ có cùng mức giá, nên bạn có thể dừng sớm.
Tại sao thuật toán Dijkstra không hoạt động với bài toán tìm chuyến bay rẻ nhất trong phạm vi K điểm dừng?
Dijkstra lưu lại một mức giá tốt nhất cho mỗi thành phố và không bao giờ quay lại thành phố đã chốt. Ở đây, một đường đi đắt hơn đến một thành phố có thể hữu ích hơn nếu đường đi đó dùng ít chuyến bay hơn. Dijkstra lại hoạt động trên các trạng thái (thành phố, số chuyến bay đã dùng), khiến số trạng thái tăng lên k+1 lần.
Bạn có thể giải bài toán chuyến bay rẻ nhất với tối đa K điểm dừng bằng BFS không?
Có. Mức r lưu các thành phố có thể đến bằng đúng r chuyến bay, cùng với giá vé. Chỉ đưa một thành phố vào mức tiếp theo khi giá vé mới thấp hơn mức giá tốt nhất từng thấy của thành phố đó, và dừng sau k+1 mức. Cách này cho kết quả giống Bellman-Ford nhưng chỉ xét các chuyến bay khởi hành từ những thành phố có giá vé thay đổi.
Bài tập tương tự
Các bài tập dùng cùng ý tưởng. Giải hai hoặc ba bài là bạn sẽ nhớ được mẫu này.
def findCheapestPrice(n, flights, src, dst, k):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
n = 4 flights = [[0, 1, 50], [1, 2, 50], [2, 3, 50], [0, 2, 200], [1, 3, 180]] src = 0 dst = 3 k = 1
Đầu ra mong đợi
230