Hướng dẫn giải của Đồng bạc cổ


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, td_mduong209, lephuochauhungvuong, trungnghia032024

Hiểu bài toán

Bài toán yêu cầu tìm đường đi từ thành phố A đến thành phố B sao cho tổng chi phí (chi phí đi đường + chi phí mua đồng tiền cổ) là nhỏ nhất. Rôn chỉ mua tiền tại một trong các thành phố có bán tiền cổ nằm trên đường đi. Ta cần tìm đường đi sao cho tổng chi phí đi từ A đến thành phố mua tiền, cộng với giá tiền tại đó, cộng với chi phí đi từ thành phố mua tiền đến B là nhỏ nhất. Nếu có nhiều cách chọn thành phố cho chi phí tổng nhỏ nhất, ta phải chọn thành phố có giá tiền lớn nhất (trong số các thành phố cùng cho chi phí tổng tối ưu), và nếu vẫn hòa thì chọn thành phố có chỉ số nhỏ nhất.

Các cách tiếp cận

Cách Dijkstra tách quãng (Dijkstra từ A và từ B)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = 1e18;

struct Edge { int v; int w; };

vector<ll> dijkstra(int n, int src, const vector<vector<Edge>>& g) {
    vector<ll> dist(n + 1, INF);
    dist[src] = 0;
    using pli = pair<ll, int>;
    priority_queue<pli, vector<pli>, greater<pli>> pq;
    pq.push({0, src});
    while (!pq.empty()) {
        auto [d, u] = pq.top(); pq.pop();
        if (d != dist[u]) continue;
        for (auto &e : g[u]) {
            ll nd = d + e.w;
            if (nd < dist[e.v]) {
                dist[e.v] = nd;
                pq.push({nd, e.v});
            }
        }
    }
    return dist;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m, k;
    if (!(cin >> n >> m >> k)) return 0;
    int A, B;
    cin >> A >> B;
    vector<ll> price(n + 1, -1);
    for (int i = 0; i < k; ++i) {
        int idx; ll p;
        cin >> idx >> p;
        price[idx] = p;
    }
    vector<vector<Edge>> g(n + 1);
    for (int i = 0; i < m; ++i) {
        int u, v, w;
        cin >> u >> v >> w;
        g[u].push_back({v, w});
        g[v].push_back({u, w});
    }

    vector<ll> distA = dijkstra(n, A, g);
    vector<ll> distB = dijkstra(n, B, g);

    ll minTotal = INF;
    int bestCity = -1;
    ll bestPrice = -1;

    for (int i = 1; i <= n; ++i) {
        if (price[i] != -1) {
            if (distA[i] == INF || distB[i] == INF) continue;
            ll total = distA[i] + distB[i] + price[i];
            if (total < minTotal) {
                minTotal = total;
                bestCity = i;
                bestPrice = price[i];
            } else if (total == minTotal) {
                if (price[i] > bestPrice) {
                    bestCity = i;
                    bestPrice = price[i];
                } else if (price[i] == bestPrice) {
                    if (i < bestCity) {
                        bestCity = i;
                    }
                }
            }
        }
    }

    cout << minTotal << " " << bestCity << "\n";
    return 0;
}
  • Time Complexity: O(m log n)
  • Space Complexity: O(n + m)

Cách tiếp cận này dựa trên quan sát: Chi phí đi từ A đến B qua thành phố i là dist(A, i) + dist(i, B) + price(i). Ta có thể tính riêng khoảng cách từ A đến mọi đỉnh và từ B đến mọi đỉnh bằng thuật toán Dijkstra. Sau đó, duyệt qua tất cả các thành phố có bán tiền, tính chi phí tổng và chọn ra thành phố tối ưu theo yêu cầu. Đây là cách hiệu quả và phổ biến nhất cho đồ thị có trọng số dương.

Cách Dijkstra 2 trạng thái (State-space Dijkstra)
// Pseudocode / Core Logic
// Sử dụng Dijkstra với priority queue lưu {cost, u, bought}
// bought = 0: chưa mua tiền, bought = 1: đã mua tiền
// dist[u][0]: chi phí nhỏ nhất để đến u mà chưa mua tiền
// dist[u][1]: chi phí nhỏ nhất để đến u mà đã mua tiền

void solve() {
    // Khởi tạo dist[][] = INF
    // dist[A][0] = 0
    // Nếu A có bán tiền, dist[A][1] = price[A]
    // PQ push({0, A, 0})
    // Nếu A có bán tiền, push({price[A], A, 1})

    // while (!PQ.empty()) {
    //   pop node {d, u, state}
    //   if (d > dist[u][state]) continue;
    //   
    //   for (edge u-v weight w) {
    //     // Case 1: Chưa mua tiền
    //     if (state == 0) {
    //       // Đi tiếp mà vẫn chưa mua
    //       if (d + w < dist[v][0]) update(dist[v][0], d+w);
    //       
    //       // Nếu v có bán tiền, quyết định mua ở đây
    //       if (price[v] != -1) {
    //         if (d + w + price[v] < dist[v][1]) update(dist[v][1], d+w+price[v]);
    //       }
    //     }
    //     // Case 2: Đã mua tiền
    //     else {
    //       // Đi tiếp, chỉ cộng chi phí đường
    //       if (d + w < dist[v][1]) update(dist[v][1], d+w);
    //     }
    //   }
    // }

    // // Kết quả là dist[B][1]
    // // Lưu ý: Cách này chỉ tìm được chi phí nhỏ nhất, không直接 tìm được thành phố mua tiền.
    // // Để tìm thành phố, ta cần truy vết (backtracking) hoặc kết hợp với cách 1.
}
  • Time Complexity: O(m log n)
  • Space Complexity: O(n + m)

Đây là cách tiếp cận tổng quát hơn. Ta mô phỏng quá trình đi đường và mua tiền như một bài toán shortest path trên đồ thị có state. Đỉnh (u, 0) đại diện cho việc đang ở u chưa mua tiền, đỉnh (u, 1) đại diện cho việc đang ở u đã mua tiền. Khi đi qua một cạnh, ta cập nhật trạng thái phù hợp. Nếu duyệt node (u, 0) và đi đến v, ta có thể chọn mua tiền tại v (nếu v có bán) để chuyển sang state 1.

Cách này đảm bảo tìm được chi phí nhỏ nhất, nhưng để xác định chính xác thành phố nào đã mua tiền (theo tiêu chí phụ), ta cần lưu thêm thông tin về thành phố mua tiền trong quá trình relax, hoặc đơn giản là cách 1 hiệu quả và dễ implement hơn cho bài toán này.

Cách Brute Force (Không khả thi)
// Ý tưởng:
// 1. Tìm tất cả đường đi từ A đến B.
// 2. Với mỗi đường đi, kiểm tra xem có thành phố nào bán tiền không.
// 3. Tính chi phí.
// Độ phức tạp: Quá lớn, không khả thi với n <= 5000.
  • Time Complexity: Exponential
  • Space Complexity: Exponential

Không sử dụng do độ phức tạp quá cao, không thể chạy trong thời gian cho phép.

Phân tích độ phức tạp

Cách tiếp cận Time Space Tên
1 O(m log n) O(n + m) Dijkstra tách quãng (Dijkstra từ A và từ B)
2 O(m log n) O(n + m) Dijkstra 2 trạng thái (State-space Dijkstra)
3 Exponential Exponential Brute Force (Không khả thi)

Bài học kinh nghiệm

  • Bài toán có thể tách thành 2 bài toán tìm đường đi riêng biệt từ A đến mọi đỉnh và từ B đến mọi đỉnh. Chi phí đi qua thành phố i là tổng của 3 phần: chi phí A->i, chi phí i->B, và giá tiền tại i.
  • Thuật toán Dijkstra (hoặc SPFA) là phù hợp do đồ thị có trọng số dương và kích thước vừa phải (n=5000, m=100000).

Lỗi thường gặp

  • Lưu ý về kiểu dữ liệu: Chi phí có thể lên tới 10^9 (giá tiền) + 10^5 * (số cạnh) ~ 10^10, nên cần dùng long long (64-bit) để tránh tràn số.
  • Đồ thị có thể không liên thông: Cần kiểm tra xem A có thể đến B không (qua các thành phố bán tiền). Nếu không có đường đi, đề bài không nói rõ output gì, nhưng thường đảm bảo có đường đi.
  • Thứ tự ưu tiên: Chi phí nhỏ nhất trước, sau đó đến giá tiền lớn nhất, cuối cùng là chỉ số thành phố nhỏ nhất.

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.