ĐỀ THI HSG MÔN TIN HỌC THÀNH PHỐ HÀ NỘI NĂM HỌC 2023-2024 (Có đáp án Full Code)

Học lập trình web

Bài I (5,0 điểm) – Hóa học

Cho phương trình hóa học sau: 3Fe + 2O₂ → Fe₃O₄

Cho biết số mol của Fe là A, số mol của O₂ là B.

Yêu cầu: Hãy lập trình đưa ra phần nguyên số mol của chất sản phẩm Fe₃O₄.

Dữ liệu vào từ tệp văn bản HOAHOC.INP:

  • Một dòng duy nhất chứa hai số tự nhiên A (A ≤ 10¹⁸) và B (B ≤ 10¹⁸).

Kết quả ghi ra tệp văn bản HOAHOC.OUT:

  • Một số tự nhiên duy nhất là kết quả của bài toán.

Ví dụ:

HOAHOC.INPHOAHOC.OUT
10 103

Code tham khảo:

#include <bits/stdc++.h>
using namespace std;

int main() {
    // Chuyển hướng nhập xuất sang file
    freopen("HOAHOC.INP", "r", stdin);
    freopen("HOAHOC.OUT", "w", stdout);

    // Đọc dữ liệu đầu vào
    unsigned long long A, B;
    cin >> A >> B;

    // Tính số mol Fe3O4 tối đa từ Fe và O2
    unsigned long long mol_from_Fe = A / 3;   // Số mol Fe3O4 từ Fe
    unsigned long long mol_from_O2 = B / 2;  // Số mol Fe3O4 từ O2

    // Số mol Fe3O4 tối đa là giá trị nhỏ hơn giữa hai giá trị trên
    unsigned long long mol_Fe3O4 = min(mol_from_Fe, mol_from_O2);

    // Ghi kết quả vào file
    cout << mol_Fe3O4;

    return 0;
}

Bài II (5,0 điểm) – Ước chung

Cho trước hai số nguyên dương AB.

Yêu cầu:

Hãy lập trình đưa ra ước chung lớn thứ hai của AB.

Dữ liệu vào từ tệp văn bản UOCCHUNG.INP:

  • Một dòng duy nhất chứa hai số tự nhiên AB (A ≤ 10¹², B ≤ 10¹²).

Kết quả ghi ra tệp văn bản UOCCHUNG.OUT:

  • Một số nguyên dương duy nhất là kết quả của bài toán.

Ràng buộc:

  • 80% số test tương ứng 80% số điểmA ≤ 1000, B ≤ 1000.
  • 20% số test còn lại tương ứng 20% số điểm không có ràng buộc gì thêm.

Ví dụ:

UOCCHUNG.INPUOCCHUNG.OUT
30 405

Code tham khảo:

#include <bits/stdc++.h>
using namespace std;

// Hàm tính GCD (Ước chung lớn nhất) bằng thuật toán Euclid
long long gcd(long long a, long long b) {
    while (b != 0) {
        long long temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}

// Hàm kiểm tra xem một số có phải là số nguyên tố hay không
bool isPrime(long long n) {
    if (n <= 1) return false;
    if (n <= 3) return true;
    if (n % 2 == 0 || n % 3 == 0) return false;
    for (long long i = 5; i * i <= n; i += 6) {
        if (n % i == 0 || n % (i + 2) == 0) return false;
    }
    return true;
}

// Hàm tìm ước chung lớn thứ hai
long long secondLargestDivisor(long long n) {
    // Tìm ước nguyên tố nhỏ nhất của n
    for (long long i = 2; i * i <= n; ++i) {
        if (n % i == 0) {
            return n / i; // Trả về n chia cho ước nguyên tố nhỏ nhất
        }
    }
    // Nếu không tìm thấy ước nào khác ngoài 1 và chính nó (n là số nguyên tố)
    return 1;
}

int main() {
    // Chuyển hướng nhập xuất sang file
    freopen("UOCCHUNG.INP", "r", stdin);
    freopen("UOCCHUNG.OUT", "w", stdout);

    long long A, B;
    cin >> A >> B;

    // Tính GCD của A và B
    long long g = gcd(A, B);

    // Tìm ước chung lớn thứ hai
    long long result = secondLargestDivisor(g);

    // In kết quả
    cout << result;

    return 0;
}

Bài III (4,0 điểm) – Trò chơi

Bạn có một nhân vật cần được tăng chỉ số sức mạnh. Nhân vật của bạn có N kĩ năng được đánh thứ tự từ 1 đến N. Kĩ năng thứ i (1 ≤ i ≤ N) có hai loại chỉ số tăng tiến là sᵢeᵢ.

  • Trong lần đầu tiên tăng cấp kĩ năng thứ i, nhân vật của bạn nhận được (sᵢ + eᵢ) chỉ số sức mạnh.
  • Trong các lần tiếp theo tăng cấp kĩ năng thứ i, nhân vật của bạn chỉ nhận được thêm eᵢ chỉ số sức mạnh.
  • Bạn có thể tăng cấp một kĩ năng bất kì không giới hạn số lần.

Trò chơi diễn ra trong M phút, mỗi phút nhân vật của bạn nhận được một lần tăng cấp kĩ năng.

Yêu cầu:

Hãy tìm chỉ số sức mạnh lớn nhất mà nhân vật của bạn có thể đạt được sau M phút chơi.

Dữ liệu vào từ tệp văn bản TROCHOI.INP:

  • Dòng đầu tiên chứa hai số nguyên dương NM (1 ≤ N ≤ 10⁵, 1 ≤ M ≤ 10⁹).
  • Dòng thứ i trong N dòng tiếp theo chứa hai số nguyên dương sᵢeᵢ (sᵢ ≤ 10⁹, eᵢ ≤ 10⁹).

Kết quả ghi ra tệp văn bản TROCHOI.OUT:

  • Một số nguyên dương duy nhất là chỉ số sức mạnh lớn nhất mà nhân vật của bạn có thể đạt được.

Ràng buộc:

  • 40% số test tương ứng 40% số điểmM = 2.
  • 40% số test tương ứng 40% số điểmM ≤ 100.
  • 20% số test còn lại tương ứng 20% số điểm không có ràng buộc gì thêm.

Ví dụ:

TROCHOI.INPTROCHOI.OUTGiải thích
3 423Cách nâng cấp tối ưu nhất là:
2 2– Nâng cấp 3 lần kĩ năng 2.
2 5– Nâng cấp 1 lần kĩ năng 3.
5 1

Code tham khảo:

#include <bits/stdc++.h>
using namespace std;

// Cấu trúc lưu thông tin về mỗi kỹ năng
struct Skill {
    long long s; // Giá trị s_i
    long long e; // Giá trị e_i
};

int main() {
    // Chuyển hướng nhập xuất sang file
    freopen("TROCHOI.INP", "r", stdin);
    freopen("TROCHOI.OUT", "w", stdout);

    // Đọc dữ liệu đầu vào
    long long N, M;
    cin >> N >> M;

    vector<Skill> skills(N);
    for (int i = 0; i < N; ++i) {
        cin >> skills[i].s >> skills[i].e;
    }

    // Sắp xếp các kỹ năng theo thứ tự giảm dần của (s_i + e_i)
    sort(skills.begin(), skills.end(), [&](const Skill &a, const Skill &b) -> bool {
        return (a.s + a.e) > (b.s + b.e);
    });

    long long result = 0;

    if (M <= N) {
        // Trường hợp M <= N: Chọn M kỹ năng đầu tiên
        for (int i = 0; i < M; ++i) {
            result += skills[i].s + skills[i].e;
        }
    } else {
        // Trường hợp M > N: Tăng cấp tất cả N kỹ năng ít nhất một lần
        for (int i = 0; i < N; ++i) {
            result += skills[i].s + skills[i].e;
        }

        // Sắp xếp các kỹ năng theo thứ tự giảm dần của e_i
        sort(skills.begin(), skills.end(), [&](const Skill &a, const Skill &b) -> bool {
            return a.e > b.e;
        });

        // Phân bổ M - N lần tăng cấp còn lại vào các kỹ năng có e_i lớn nhất
        long long remaining_upgrades = M - N;
        for (int i = 0; i < min(remaining_upgrades, (long long)N); ++i) {
            result += skills[i].e;
        }
    }

    // Ghi kết quả vào file
    cout << result;

    return 0;
}

Bài IV (3,0 điểm) – Robot

Cho một bảng số nguyên dương AN hàng, M cột và một số nguyên dương K.
Số nằm ở hàng i, cột j có tọa độ là (i, j) và có giá trị là Aᵢⱼ.
Một con robot đang đứng ở ô (1, 1) và cần di chuyển đến ô (N, M).
Khi đứng ở ô (i, j), robot chỉ có thể di chuyển vào ô (i, j + 1), (i + 1, j) hoặc (i + 1, j + 1).

Cho Q truy vấn, mỗi truy vấn gồm một số tự nhiên X (X < K).
Với mỗi truy vấn, hãy cho biết đường đi của robot từ ô (1, 1) đến ô (N, M) có thể đi qua nhiều nhất bao nhiêu ô (i, j) thỏa mãn Aᵢⱼ mod K = X.

Yêu cầu:

Hãy trả lời Q truy vấn của đề bài.

Dữ liệu vào từ tệp văn bản ROBOT.INP:

  • Dòng đầu tiên chứa bốn số nguyên dương N, M, Q, K (1 ≤ N, M ≤ 1000, 1 ≤ Q ≤ 10⁵, 1 ≤ K ≤ 10⁹).
  • Trong N dòng tiếp theo, dòng thứ i chứa M số nguyên dương biểu diễn bảng A (1 ≤ Aᵢⱼ ≤ 10⁹).
  • Trong Q dòng tiếp theo, mỗi dòng chứa một số tự nhiên X thể hiện truy vấn tương ứng.

Kết quả ghi ra tệp văn bản ROBOT.OUT:

  • Gồm Q dòng, mỗi dòng chứa một số tự nhiên là kết quả của truy vấn tương ứng.

Ràng buộc:

  • 20% số test tương ứng 20% số điểmM = 1.
  • 20% số test tương ứng 20% số điểmM = 2, Q ≤ 1000.
  • 30% số test tương ứng 30% số điểmM, N, K ≤ 300.
  • 30% số test còn lại tương ứng 30% số điểm không có ràng buộc gì thêm.

Ví dụ:

ROBOT.INPROBOT.OUT
3 4 2 65
3 2 6 13
9 1 9 1
1 9 1 9
2

Code tham khảo:

#include <bits/stdc++.h>
using namespace std;

// Hằng số giới hạn
const int MAX_N = 1000;
const int MAX_K = 1e9;

int main() {
    // Chuyển hướng nhập xuất sang file
    freopen("ROBOT.INP", "r", stdin);
    freopen("ROBOT.OUT", "w", stdout);

    // Đọc dữ liệu đầu vào
    int N, M, Q, K;
    cin >> N >> M >> Q >> K;

    // Đọc bảng A và tính A[i][j] % K
    vector<vector<int>> modValues(N + 1, vector<int>(M + 1));
    for (int i = 1; i <= N; ++i) {
        for (int j = 1; j <= M; ++j) {
            long long value;
            cin >> value;
            modValues[i][j] = value % K;
        }
    }

    // Lưu trữ kết quả cho từng truy vấn
    vector<int> results(Q);

    // Đọc và xử lý từng truy vấn
    for (int q = 0; q < Q; ++q) {
        int X;
        cin >> X;

        // Khởi tạo bảng quy hoạch động
        vector<vector<int>> dp(N + 1, vector<int>(M + 1, 0));

        // Điền bảng DP
        for (int i = 1; i <= N; ++i) {
            for (int j = 1; j <= M; ++j) {
                // Số lượng ô thỏa mãn điều kiện tại (i, j)
                int count = (modValues[i][j] == X) ? 1 : 0;

                // Lấy giá trị lớn nhất từ các ô liền kề
                dp[i][j] = count;
                if (i > 1 && j > 1) {
                    dp[i][j] += max({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]});
                } else if (i > 1) {
                    dp[i][j] += dp[i-1][j];
                } else if (j > 1) {
                    dp[i][j] += dp[i][j-1];
                }
            }
        }

        // Kết quả cho truy vấn hiện tại
        results[q] = dp[N][M];
    }

    // Ghi kết quả ra file
    for (int q = 0; q < Q; ++q) {
        cout << results[q] << "\n";
    }

    return 0;
}

Bài V (3,0 điểm) – Đoạn tốt

Một đoạn thẳng trên trục số được biểu diễn bởi hai số L, R, lần lượt là điểm đầu và điểm cuối của đoạn thẳng đó.
Một tập hợp đẹp là tập hợp mà mỗi đoạn thẳng trong tập có ít nhất một điểm chung với một đoạn khác trong tập.
Tập hợp chỉ có 1 đoạn thẳng là tập hợp đẹp. Độ tốt của một tập hợp đẹp là số lượng đoạn thẳng trong tập hợp đó.

Ví dụ:

  • Tập hợp {(1, 2)} là tập hợp đẹp có độ tốt là 1.
  • Tập hợp {(4, 7), (5, 8)} là tập hợp đẹp có độ tốt là 3.
  • Tập hợp {(1, 3), (3, 5), (6, 8)} không phải tập hợp đẹp.

N đoạn thẳng được đánh số từ 1 đến N.
Đoạn thẳng thứ i được biểu diễn bởi hai số nguyên Lᵢ, Rᵢ.

Người ta thực hiện như sau: Thêm lần lượt các đoạn thẳng theo thứ tự từ 1 đến N, cụ thể:

  1. Nếu (Lᵢ, Rᵢ) có điểm chung với một tập hợp đẹp đã có thì thêm (Lᵢ, Rᵢ) vào tập hợp đó.
  2. Nếu (Lᵢ, Rᵢ) không có điểm chung với tập hợp nào, thì tạo một tập hợp mới có 1 đoạn thẳng là (Lᵢ, Rᵢ).
  3. Nếu hai tập hợp đẹp giao nhau, thì gộp hai tập hợp đó thành một tập hợp đẹp lớn.

Yêu cầu:

Sau khi thêm đoạn thẳng thứ i, hãy tính tích độ tốt của các tập hợp đẹp lúc đó.

Dữ liệu vào từ tệp văn bản DOANTOT.INP:

  • Dòng đầu tiên là số nguyên dương N (1 ≤ N ≤ 10⁵).
  • Trong N dòng tiếp theo, dòng thứ i chứa hai số nguyên dương Lᵢ, Rᵢ (Lᵢ ≤ Rᵢ ≤ 10⁹).

Kết quả ghi ra tệp văn bản DOANTOT.OUT:

  • Gồm N dòng, dòng thứ i chứa một số nguyên dươngsố tích độ tốt của các tập hợp đẹp sau khi thêm đoạn thẳng thứ i, chia dư cho 10⁹ + 7.

Ràng buộc:

  • 30% số test tương ứng 30% số điểmLᵢ = Rᵢ1 ≤ i ≤ N, N ≤ 30.
  • 50% số test tương ứng 50% số điểmRᵢ ≤ 10001 ≤ i ≤ N.
  • 20% số test còn lại tương ứng 20% số điểm không có ràng buộc gì thêm.

Ví dụ:

DOANTOT.INPDOANTOT.OUT
61
1 31
3 52
4 72
5 83
9 113

Giải thích:

Sau khi thêm đoạn thẳng thứ i, ta có:

  1. Thêm (1, 3) → Có 1 tập đẹp độ tốt 1.
  2. Thêm (3, 5) → Có 1 tập đẹp độ tốt 2.
  3. Thêm (4, 7) → Có 2 tập đẹp: độ tốt 1, độ tốt 2.
  4. Thêm (5, 8) → Có 1 tập đẹp độ tốt 3.
  5. Thêm (9, 11) → Có 2 tập đẹp: độ tốt 1, độ tốt 3.
  6. Tích độ tốt sau mỗi bước ghi vào tệp đầu ra.

Code tham khảo:

#include <bits/stdc++.h>
using namespace std;

// Hằng số MOD
const int MOD = 1e9 + 7;

// Cấu trúc Disjoint Set Union (DSU)
struct DSU {
    vector<int> parent;
    vector<int> size;

    DSU(int n) : parent(n + 1), size(n + 1, 1) {
        for (int i = 0; i <= n; ++i) {
            parent[i] = i;
        }
    }

    // Tìm gốc của tập hợp chứa x
    int find_set(int x) {
        if (parent[x] != x) {
            parent[x] = find_set(parent[x]);
        }
        return parent[x];
    }

    // Gộp hai tập hợp chứa x và y
    void union_set(int x, int y) {
        int root_x = find_set(x);
        int root_y = find_set(y);
        if (root_x != root_y) {
            if (size[root_x] < size[root_y]) {
                swap(root_x, root_y);
            }
            parent[root_y] = root_x;
            size[root_x] += size[root_y];
        }
    }

    // Lấy kích thước của tập hợp chứa x
    int get_size(int x) {
        return size[find_set(x)];
    }
};

int main() {
    // Chuyển hướng nhập xuất sang file
    freopen("DOANTOT.INP", "r", stdin);
    freopen("DOANTOT.OUT", "w", stdout);

    // Đọc dữ liệu đầu vào
    int N;
    cin >> N;

    // Lưu trữ các đoạn thẳng
    vector<pair<int, int>> segments(N);
    for (int i = 0; i < N; ++i) {
        cin >> segments[i].first >> segments[i].second;
    }

    // Khởi tạo DSU
    DSU dsu(N);

    // Sắp xếp các đoạn thẳng theo thứ tự tăng dần của Lᵢ và Rᵢ
    sort(segments.begin(), segments.end());

    // Lưu trữ kết quả
    vector<long long> results(N);

    // Biến lưu tích độ tốt hiện tại
    long long product = 1;

    // Duyệt qua từng đoạn thẳng
    for (int i = 0; i < N; ++i) {
        int L = segments[i].first;
        int R = segments[i].second;

        // Kiểm tra xem đoạn thẳng này có thể gộp vào tập hợp nào không
        bool merged = false;
        for (int j = 0; j < i; ++j) {
            int prev_L = segments[j].first;
            int prev_R = segments[j].second;

            // Nếu có điểm chung, gộp hai tập hợp
            if (!(R < prev_L || L > prev_R)) {
                dsu.union_set(i, j);
                merged = true;
            }
        }

        // Nếu không thể gộp, tạo tập hợp mới
        if (!merged) {
            product = (product * 1) % MOD;
        } else {
            // Cập nhật tích độ tốt
            product = 1;
            for (int j = 0; j <= i; ++j) {
                if (dsu.find_set(j) == j) {
                    product = (product * dsu.get_size(j)) % MOD;
                }
            }
        }

        // Lưu kết quả
        results[i] = product;
    }

    // Ghi kết quả ra file
    for (int i = 0; i < N; ++i) {
        cout << results[i] << "\n";
    }

    return 0;
}