Hướng dẫn giải của Chạy đua và ăn kẹo


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, Pitomon, admin1, LTC

Hiểu bài toán

Bài toán yêu cầu tìm số lượng tối đa các thí sinh có thể tham gia cuộc thi. Mỗi thí sinh bắt đầu tại nút 0 và kết thúc tại nút N-1 trên một đồ thị vô hướng. Mỗi con đường i có một hộp chứa 3^i kẹo. Khi một thí sinh đi qua một con đường, họ phải lấy 1 kẹo. Nếu hộp kẹo trống, không ai được phép đi qua con đường đó. Mục tiêu là tính tổng số lượng kẹo tối đa có thể thu thập được trên các đường đi từ 0 đến N-1 (tức là số lượng kẹo tối đa có thể dùng để tài trợ cho các thí sinh).

Về bản chất, ta cần chọn một tập hợp các đường đi từ 0 đến N-1 sao cho mỗi con đường i được sử dụng nhiều nhất 3^i lần. Vì 3^i có giá trị lớn và là cơ số 3, bài toán có thể được chuyển đổi thành: tìm số cách chọn các đường đi sao cho con đường có độ ưu tiên cao (chỉ số lớn) được sử dụng tối đa. Cụ thể, ta ưu tiên dùng các con đường có chỉ số lớn nhất (3^(M-1)), tiếp theo là 3^(M-2), v.v.

Ta có thể sử dụng thuật toán tương tự Dijkstra hoặc Kruskal (sử dụng Disjoint Set Union - DSU) để xử lý các cạnh theo thứ tự ưu tiên từ cao đến thấp.

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

Cách Quy hoạch động (Dynamic Programming) - Dijkstra
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>

using namespace std;

const int MOD = 1000000007;
const int MAXN = 2005;

struct Edge {
    int u, v;
};

struct State {
    int node;
    int dist;
    bool operator>(const State& other) const {
        return dist < other.dist; // Max heap
    }
};

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int N, M;
    if (!(cin >> N >> M)) return 0;

    vector<Edge> edges(M);
    for (int i = 0; i < M; ++i) {
        cin >> edges[i].u >> edges[i].v;
    }

    // Precompute powers of 3 modulo MOD
    vector<long long> pow3(M + 1);
    pow3[0] = 1;
    for (int i = 1; i <= M; ++i) {
        pow3[i] = (pow3[i - 1] * 3) % MOD;
    }

    // Dijkstra to find the best path (lexicographically largest)
    // Actually, we need to maximize the sum of 3^i.
    // Since 3^{i+1} > sum(3^0 ... 3^i), we can use a greedy approach with DSU or Dijkstra with big integers.
    // However, given constraints and standard solutions, the DSU approach is more standard.
    // Let's implement the DSU approach which is effectively the same as the provided solution 1.
    // This snippet represents the logic of Solution 1.

    vector<int> parent(N);
    for (int i = 0; i < N; ++i) parent[i] = i;

    auto findSet = [&](int i) {
        while (i != parent[i]) {
            parent[i] = parent[parent[i]];
            i = parent[i];
        }
        return i;
    };

    auto unionSets = [&](int i, int j) {
        int root_i = findSet(i);
        int root_j = findSet(j);
        if (root_i != root_j) {
            parent[root_i] = root_j;
        }
    };

    long long result = 0;
    // Iterate from largest edge index to smallest
    for (int i = M - 1; i >= 0; --i) {
        int u = edges[i].u;
        int v = edges[i].v;

        // If u and v are already connected via edges > i, adding this edge creates a cycle.
        // In this problem, we want to maximize the sum of 3^i on paths from 0 to N-1.
        // If adding edge i connects the component of 0 and N-1, we gain 3^i.
        // The logic in the solution is:
        // If {0} and {N-1} are in different components, and edge i connects them, we add 3^i.
        // Wait, the provided solution logic is:
        // Check if (u in comp(0) && v in comp(N-1)) OR vice versa.
        // If yes, add pow3[i]. Else, union them.

        int root0 = findSet(0);
        int rootN = findSet(N - 1);
        int rootU = findSet(u);
        int rootV = findSet(v);

        if ((rootU == root0 && rootV == rootN) || (rootU == rootN && rootV == root0)) {
            result = (result + pow3[i]) % MOD;
        } else {
            unionSets(u, v);
        }
    }

    cout << result << endl;
    return 0;
}
  • Time Complexity: O(M * α(N)) hoặc O(M log M) nếu sort
  • Space Complexity: O(N + M)

Đây là cách tiếp cận tối ưu nhất. Ta xử lý các cạnh theo thứ tự từ chỉ số lớn nhất (M-1) về 0. Với mỗi cạnh i, ta kiểm tra xem nó có giúp kết nối nút 0 và nút N-1 hay không.

  • Sử dụng DSU để quản lý các thành phần liên thông.
  • Nếu thêm cạnh i vào đồ thị (khi xét từ chỉ số cao xuống thấp) tạo thành một đường nối 0 và N-1, ta thêm 3^i vào kết quả.
  • Nếu không, ta hợp nhất hai đỉnh của cạnh đó vào DSU. Cách này đảm bảo ta luôn ưu tiên các cạnh có chỉ số lớn nhất (giá trị kẹo lớn nhất) cho các đường đi.
Cách Giải thích chi tiết thuật toán DSU
// Mô phỏng logic DSU
#include <vector>
using namespace std;

int solve(int N, int M, vector<pair<int,int>>& edges) {
    int mod = 1000000007;
    vector<int> parent(N);
    for(int i=0; i<N; i++) parent[i] = i;

    // Precompute powers
    vector<long long> p3(M+1, 1);
    for(int i=1; i<=M; i++) p3[i] = (p3[i-1] * 3) % mod;

    long long res = 0;
    // Duyệt ngược từ cạnh cuối
    for(int i = M-1; i >= 0; i--) {
        int u = edges[i].first;
        int v = edges[i].second;

        // Tìm root
        int root_u = u; while(root_u != parent[root_u]) root_u = parent[root_u];
        int root_v = v; while(root_v != parent[root_v]) root_v = parent[root_v];
        // Path compression có thể thêm vào

        int root_0 = 0; while(root_0 != parent[root_0]) root_0 = parent[root_0];
        int root_n1 = N-1; while(root_n1 != parent[root_n1]) root_n1 = parent[root_n1];

        // Kiểm tra điều kiện: cạnh này có nối comp chứa 0 và comp chứa N-1 không?
        // Nếu 2 đỉnh của cạnh thuộc 2 comp khác nhau mà 1 trong 2 comp là comp(0) và comp kia là comp(N-1)
        // Hoặc đơn giản hơn: nếu (root_u == root_0 && root_v == root_n1) || (root_u == root_n1 && root_v == root_0)
        // Nếu có, cộng dồn 3^i
        if ((root_u == root_0 && root_v == root_n1) || (root_u == root_n1 && root_v == root_0)) {
            res = (res + p3[i]) % mod;
        } else {
            // Nếu không, hợp nhất hai thành phần
            // Lưu ý: chỉ hợp nhất nếu chúng chưa nối với nhau qua đường khác
            // Thực tế logic bài giải là: Nếu không tạo đường đi trực tiếp, ta thêm cạnh vào DSU để mở rộng thành phần
            if (root_u != root_v) parent[root_u] = root_v;
        }
    }
    return res;
}
  • Time Complexity: O(M * α(N))
  • Space Complexity: O(N + M)

Đoạn code này minh họa rõ ràng logic của Solution 1.

  1. Chuẩn bị: Khởi tạo DSU, tính trước 3^i.
  2. Vòng lặp: Duyệt từ cạnh có chỉ số cao nhất (M-1) về 0.
  3. Kiểm tra: Xem cạnh hiện tại (u, v) có nằm trên đường nối trực tiếp giữa thành phần chứa 0 và thành phần chứa N-1 hay không.
    • Nếu có: Cạnh này là bắt buộc để có đường đi (vì nó có giá trị lớn nhất trong các cạnh còn lại xét trên đường đi đó), cộng 3^i vào kết quả.
    • Nếu không: Hợp nhất 2 thành phần (nếu chúng khác nhau) để chuẩn bị cho các bước kiểm tra tiếp theo. Lưu ý quan trọng: Việc hợp nhất chỉ xảy ra khi cạnh không tạo thành đường đi trực tiếp giữa 0 và N-1. Điều này đảm bảo ta không 'lãng phí' các cạnh tốt vào việc tạo thành các chu trình không cần thiết mà thay vào đó giữ chúng cho việc tạo đường đi ở các bước sau (với chỉ số nhỏ hơn).

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

Cách tiếp cận Time Space Tên
1 O(M * α(N)) hoặc O(M log M) nếu sort O(N + M) Quy hoạch động (Dynamic Programming) - Dijkstra
2 O(M * α(N)) O(N + M) Giải thích chi tiết thuật toán DSU

Bài học kinh nghiệm

  • Giá trị của kẹo trên các con đường tăng theo cấp số nhân (3^i). Do đó, ưu tiên hàng đầu là tối đa hóa số lượng kẹo từ các con đường có chỉ số i lớn nhất.
  • Bài toán có thể được chuyển đổi thành bài toán tìm cây bao trùm (Spanning Tree) biến thể, nơi ta muốn các cạnh có trọng số lớn nhất nằm trên đường đi giữa 0 và N-1.
  • Thuật toán Union-Find (DSU) kết hợp với việc duyệt các cạnh theo thứ tự giảm dần của chỉ số i là cách tiếp cận hiệu quả nhất để giải quyết bài toán này.

Lỗi thường gặp

  • Sử dụng int thay vì long long cho việc tính toán giá trị 3^i, có thể gây tràn số (overflow) vì 3^2000 là một số rất lớn.
  • Sai lệch trong logic kiểm tra điều kiện hợp nhất (Union) của DSU. Phải đảm bảo rằng cạnh chỉ được 'mua' (add to result) khi nó nối trực tiếp component của 0 và component của N-1.
  • Quên khởi tạo DSU cho các node từ 0 đến N-1.

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.