Hướng dẫn giải của Đường dẫn khí
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.
Lời giải này đang bị ẩn cho đến khi bạn chọn mở ra.
Chúng tôi khuyên bạn nên tự thử giải bài trước. Việc mở lời giải có thể làm lộ mất ý tưởng chính trước khi bạn có cơ hội tự giải.
Bạn phải đăng nhập để mở lời giải này.
Đăng nhậpTác giả: , , ,
Hiểu bài toán
Bài toán tìm đường đi ngắn nhất từ nút 1 đến nút N trong đồ thị có hướng (hoặc vô hướng) với chi phí trên các cạnh. Điểm đặc biệt là bạn được phép loại bỏ chi phí của tối đa K cạnh bất kỳ trên đường đi (tức là biến chi phí của K cạnh đó thành 0). Mục tiêu là tìm tổng chi phí đường đi nhỏ nhất có thể sau khi áp dụng tối đa K lần loại bỏ chi phí cạnh.
Đây là bài toán tìm đường ngắn nhất có trạng thái bổ sung: số cạnh đã được loại bỏ chi phí. Với N <= 10000, M <= 50000, K <= 20, ta có thể sử dụng thuật toán Dijkstra trên đồ thị mở rộng có N*(K+1) đỉnh.
Các cách tiếp cận
Cách Dijkstra với trạng thái (Standard)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = 1e18;
const int MAXN = 10005;
const int MAXK = 25;
struct Edge {
int to;
ll cost;
};
int N, M, K;
vector<Edge> adj[MAXN];
ll dist[MAXN][MAXK];
void dijkstra() {
// priority_queue sap xep theo cost (lon nhat o dau)
// nen dung greater de co cost nho nhat len dau
priority_queue<tuple<ll, int, int>, vector<tuple<ll, int, int>>, greater<>> pq;
// Khoi tao du lieu
for (int i = 1; i <= N; ++i)
for (int k = 0; k <= K; ++k)
dist[i][k] = INF;
dist[1][0] = 0;
pq.push({0, 1, 0}); // cost, node, pipes_used
while (!pq.empty()) {
auto [d, u, pipes_used] = pq.top(); pq.pop();
// Neu duong di hien tai dai hon duong da luu thi bo qua
if (d > dist[u][pipes_used]) continue;
for (auto [v, w] : adj[u]) {
// Truong hop 1: Khong su dung ong dan (chi phi goc)
if (dist[v][pipes_used] > dist[u][pipes_used] + w) {
dist[v][pipes_used] = dist[u][pipes_used] + w;
pq.push({dist[v][pipes_used], v, pipes_used});
}
// Truong hop 2: Su dung ong dan (chi phi = 0)
// Chi duoc phep neu so ong da su dung < K
if (pipes_used < K && dist[v][pipes_used + 1] > dist[u][pipes_used]) {
dist[v][pipes_used + 1] = dist[u][pipes_used];
pq.push({dist[v][pipes_used + 1], v, pipes_used + 1});
}
}
}
ll ans = INF;
for (int i = 0; i <= K; ++i) {
ans = min(ans, dist[N][i]);
}
cout << ans;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
if (fopen("gaspipe.inp", "r")) {
freopen("gaspipe.inp", "r", stdin);
freopen("gaspipe.out", "w", stdout);
}
cin >> N >> M >> K;
for (int i = 0; i < M; ++i) {
int u, v; ll w;
cin >> u >> v >> w;
adj[u].push_back({v, w});
// Neu de bai la do thi co huong, dong nay chi duoc su dung cho canh u->v
// De bai anh huong den chieu duong di tu 1 den N, nhung dau vao co the co 2 chieu
// De an toan cho bai toan duong di, ta them canh nguoc lai neu can
// Tuy nhien, trong bai nay, chi them u->v la du (va dung) cho duong di tu 1 den N
// Neu de bai yeu cau duyet ca 2 chieu (do thi vo huong), them:
adj[v].push_back({u, w});
}
dijkstra();
return 0;
}
- Time Complexity: O(M * K * log(N * K))
- Space Complexity: O(N * K)
Giải pháp này là biến thể của thuật toán Dijkstra. Ta duy trì một mảng dist[u][k] biểu diễn chi phí ngắn nhất từ nút 1 đến nút u khi đã sử dụng k đường ống dẫn (cắt giảm chi phí).
- Trạng thái:
(cost, node, pipes_used). - Khi duyệt từ nút
usang nútvvới chi phíw:- Không dùng ống dẫn: Nếu
dist[v][pipes_used]lớn hơndist[u][pipes_used] + w, cập nhật lại và đẩy vào hàng đợi. - Dùng ống dẫn: Nếu
pipes_used < K, ta thử cập nhậtdist[v][pipes_used + 1]bằngdist[u][pipes_used](tức chi phí đi qua cạnh này là 0).
- Không dùng ống dẫn: Nếu
- Độ phức tạp: Số lượng trạng thái là
N * (K+1). Mỗi cạnh được xét tối đaK+1lần. Do đó độ phức tạp làO(M * K * log(N * K)). Với N=10^4, M=5*10^4, K=20, đây là lời giải hoàn hảo.
Cách Dijkstra với State struct
#include <bits/stdc++.h>
using namespace std;
struct Edge {
int to;
long long cost;
};
struct State {
long long cost;
int node;
int pipes_used;
bool operator>(const State &other) const {
return cost > other.cost;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, k;
cin >> n >> m >> k;
vector<vector<Edge>> adj(n + 1);
for (int i = 0; i < m; i++) {
int u, v;
long long c;
cin >> u >> v >> c;
adj[u].push_back({v, c});
adj[v].push_back({u, c}); // Giả sử đồ thị vô hướng
}
const long long INF = 1e18;
// dist[node][pipes_used]
vector<vector<long long>> dist(n + 1, vector<long long>(k + 1, INF));
priority_queue<State, vector<State>, greater<State>> pq;
dist[1][0] = 0;
pq.push({0, 1, 0});
while (!pq.empty()) {
State cur = pq.top(); pq.pop();
int u = cur.node;
long long c = cur.cost;
int used = cur.pipes_used;
if (c > dist[u][used]) continue;
for (auto &edge : adj[u]) {
int v = edge.to;
long long w = edge.cost;
// Truong hop 1: Khong su dung ong
if (dist[v][used] > c + w) {
dist[v][used] = c + w;
pq.push({dist[v][used], v, used});
}
// Truong hop 2: Su dung ong
if (used < k && dist[v][used + 1] > c) {
dist[v][used + 1] = c;
pq.push({c, v, used + 1});
}
}
}
long long ans = INF;
for (int i = 0; i <= k; i++) {
ans = min(ans, dist[n][i]);
}
cout << ans << endl;
return 0;
}
- Time Complexity: O(M * K * log(N * K))
- Space Complexity: O(N * K)
Cách tiếp cận này tương tự như cách đầu tiên, nhưng thay vì dùng tuple, ta định nghĩa một cấu trúc State rõ ràng hơn. Logic cơ bản vẫn là Dijkstra trên đồ thị với N*(K+1) đỉnh. Việc sử dụng struct giúp code dễ đọc và dễ bảo trì hơn.
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(M * K * log(N * K)) | O(N * K) | Dijkstra với trạng thái (Standard) |
| 2 | O(M * K * log(N * K)) | O(N * K) | Dijkstra với State struct |
Bài học kinh nghiệm
- Bài toán có thể chuyển hóa thành bài toán tìm đường ngắn nhất trên đồ thị có trọng số với 'chiều sâu' hoặc 'trạng thái' bổ sung là số lượng ống dẫn đã được xây dựng.
- Thuật toán Dijkstra là lựa chọn tối ưu do các chi phí đều dương và cần tìm đường đi ngắn nhất toàn cục.
- Khi sử dụng ống dẫn trên một cạnh, ta effectively coi cạnh đó có trọng số bằng 0 và tăng biến đếm số ống dẫn lên 1.
Lỗi thường gặp
- Quên kiểm tra điều kiện
pipes_used < Ktrước khi thực hiện bước 'dùng ống dẫn', dẫn đến truy cập ngoài vùng nhớ hoặc tính toán sai. - Sử dụng priority queue mặc định (đỉnh lớn nhất) thay vì min-heap (đỉnh nhỏ nhất) khi tìm đường đi ngắn nhất.
- Không xử lý đúng hướng của đồ thị: Nếu input là đồ thị có hướng, chỉ thêm cạnh
u -> v. Nếu là đồ thị vô hướng, phải thêm cảv -> u.
Bình luận