Hướng dẫn giải của Đường dẫn khí


Chỉ dùng lời giải này khi không có ý tưởng, và đừng copy-paste code từ lời giải này. Hãy tôn trọng người ra đề và người viết lời giải.
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ập

Tác giả: Hiếu Nguyễn, lephuochauhungvuong, congtyluuthaibao1978, trungnghia032024

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 u sang nút v với chi phí w:
    1. Không dùng ống dẫn: Nếu dist[v][pipes_used] lớn hơn dist[u][pipes_used] + w, cập nhật lại và đẩy vào hàng đợi.
    2. Dùng ống dẫn: Nếu pipes_used < K, ta thử cập nhật dist[v][pipes_used + 1] bằng dist[u][pipes_used] (tức chi phí đi qua cạnh này là 0).
  • Độ phức tạp: Số lượng trạng thái là N * (K+1). Mỗi cạnh được xét tối đa K+1 lầ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 < K trướ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

Please read the guidelines before commenting.


Không có bình luận tại thời điểm này.