Bài 31.5. Bài tập tổng hợp về Fenwick Tree

Chào mừng trở lại chuỗi bài học về Cấu trúc dữ liệu và Giải thuật của FullhouseDev!

Sau khi đã tìm hiểu lý thuyết về Fenwick Tree (hay còn gọi là Binary Indexed Tree - BIT), giờ là lúc chúng ta thực sự cảm nhận được sức mạnhsự linh hoạt của nó thông qua các bài tập vận dụng. Fenwick Tree không chỉ dừng lại ở bài toán cập nhật điểm - truy vấn đoạn cơ bản; với một vài kỹ thuật khéo léo, nó có thể giải quyết nhiều loại bài toán khác một cách hiệu quả.

Bài viết này sẽ là một buổi "luyện công" thực thụ, giúp bạn nắm vững các mô hình áp dụng Fenwick Tree phổ biến trong lập trình thi đấu và giải quyết các vấn đề thực tế. Hãy cùng đào sâu vào từng dạng bài và xem Fenwick Tree xử lý chúng như thế nào nhé!

Nhắc lại nhanh về Fenwick Tree

Trước khi vào bài tập, hãy cùng lướt qua những điểm cốt lõi nhất của Fenwick Tree:

  • Là cấu trúc dữ liệu cho phép thực hiện cập nhật điểm (point update) và truy vấn tổng tiền tố (prefix sum query) trên một mảng trong thời gian O(log N).
  • Nó sử dụng một mảng phụ (thường gọi là BIT hoặc tree) có kích thước N+1 (thường dùng 1-based indexing để đơn giản), nơi mỗi phần tử tree[i] lưu tổng của một khoảng xác định trong mảng gốc, được tính toán dựa trên bit cuối cùng khác 0 của i (i & -i).
  • Thao tác update(index, delta): Cộng thêm delta vào phần tử tại index trong mảng gốc và cập nhật các phần tử liên quan trong BIT bằng cách nhảy đến các chỉ số i += i & -i.
  • Thao tác query(index): Tính tổng tiền tố từ 1 đến index trong mảng gốc bằng cách tổng hợp các giá trị từ BIT bằng cách nhảy đến các chỉ số i -= i & -i.
  • Truy vấn tổng trên một đoạn [L, R] đơn giản là query(R) - query(L-1).

OK, kiến thức đã được hâm nóng. Giờ thì "xắn tay áo" lên và giải quyết các bài toán thôi!

Bài Tập 1: Cập nhật điểm - Truy vấn đoạn (Phiên bản kinh điển)

Đây là bài toán cơ bản nhất mà Fenwick Tree được thiết kế để giải quyết.

Bài toán: Cho một mảng A gồm N phần tử. Cần thực hiện hai loại thao tác:

  1. Cập nhật giá trị của phần tử A[i] thành một giá trị mới v (hoặc cộng/trừ một lượng delta).
  2. Tính tổng các phần tử trong đoạn A[L...R].

Phân tích và Giải pháp:

Fenwick Tree trực tiếp giải quyết bài toán này. Nếu đề bài yêu cầu cập nhật giá trị mới, ta cần tính delta = new_value - current_value của A[i] trước khi gọi update. Nếu chỉ yêu cầu cộng/trừ, delta chính là lượng cần cộng/trừ. Truy vấn đoạn [L, R] được tính bằng query(R) - query(L-1).

Ta sẽ sử dụng một mảng BIT có kích thước N+1 (hoặc lớn hơn một chút tùy cài đặt).

Mã nguồn C++ minh họa:

#include <iostream>
#include <vector>

// Sử dụng 1-based indexing cho Fenwick Tree
class FenwickTree {
private:
    std::vector<long long> bit; // Mảng BIT
    int size; // Kích thước tối đa của mảng gốc (N)

public:
    // Khởi tạo BIT với kích thước N
    FenwickTree(int n) : size(n), bit(n + 1, 0) {}

    // Hàm cập nhật: Cộng delta vào phần tử tại index (1-based)
    void update(int index, long long delta) {
        // Đi lên cây, cập nhật các node chứa index
        for (; index <= size; index += index & -index) {
            bit[index] += delta;
        }
    }

    // Hàm truy vấn: Tính tổng tiền tố từ 1 đến index (1-based)
    long long query(int index) {
        long long sum = 0;
        // Đi xuống cây, tổng hợp các node cần thiết
        for (; index > 0; index -= index & -index) {
            sum += bit[index];
        }
        return sum;
    }

    // Hàm truy vấn tổng đoạn từ L đến R (1-based)
    long long query_range(int L, int R) {
        if (L > R) return 0;
        return query(R) - query(L - 1);
    }
};

int main() {
    int n; // Kích thước mảng gốc
    std::cout << "Nhap kich thuoc mang N: ";
    std::cin >> n;

    // Khởi tạo Fenwick Tree
    FenwickTree ft(n);

    // Giả sử mảng ban đầu toàn 0, sau đó có thể thêm các cập nhật ban đầu nếu cần
    // Ví dụ: Đọc mảng ban đầu và update vào BIT
    // std::cout << "Nhap N phan tu ban dau: ";
    // for (int i = 1; i <= n; ++i) {
    //     long long val;
    //     std::cin >> val;
    //     ft.update(i, val); // Update từng phan tu vao BIT
    // }

    int num_operations;
    std::cout << "Nhap so luong thao tac: ";
    std::cin >> num_operations;

    for (int i = 0; i < num_operations; ++i) {
        int type; // 1: update, 2: query
        std::cout << "Nhap loai thao tac (1: update, 2: query range): ";
        std::cin >> type;

        if (type == 1) {
            int index;
            long long delta; // Luong can cong/tru
            std::cout << "Nhap index (1-based) va luong thay doi delta: ";
            std::cin >> index >> delta;
            if (index >= 1 && index <= n) {
                ft.update(index, delta);
                std::cout << "Da cap nhat index " << index << " voi delta " << delta << std::endl;
            } else {
                 std::cout << "Index khong hop le!" << std::endl;
            }
        } else if (type == 2) {
            int L, R;
            std::cout << "Nhap doan [L, R] (1-based) de truy van tong: ";
            std::cin >> L >> R;
            if (L >= 1 && L <= R && R <= n) {
                 long long total_sum = ft.query_range(L, R);
                 std::cout << "Tong tren doan [" << L << ", " << R << "] la: " << total_sum << std::endl;
            } else {
                 std::cout << "Doan truy van khong hop le!" << std::endl;
            }
        } else {
            std::cout << "Loai thao tac khong hop le!" << std::endl;
        }
    }

    return 0;
}

Giải thích mã nguồn:

  • Lớp FenwickTree bao gồm mảng bit và kích thước size.
  • Hàm update(index, delta): Bắt đầu từ index, ta lặp và cộng delta vào các phần tử bit[index], bit[index + (index & -index)], ... cho đến khi vượt quá size. Biểu thức index & -index cho ra giá trị của bit cuối cùng khác 0 của index, đây là chiều dài của đoạn mà bit[index] đang quản lý.
  • Hàm query(index): Bắt đầu từ index, ta lặp và cộng bit[index], bit[index - (index & -index)], ... vào sum cho đến khi index về 0.
  • Hàm query_range(L, R): Sử dụng tính chất tổng tiền tố: Sum[L..R] = Sum[1..R] - Sum[1..L-1].
  • Trong main, chúng ta khởi tạo FenwickTree với kích thước N và sau đó xử lý các loại thao tác theo input. Ví dụ này chỉ thực hiện cộng thêm vào giá trị hiện tại. Nếu cần thay đổi giá trị tại một index, bạn cần lưu mảng gốc hoặc truy vấn giá trị cũ (query_range(index, index)) để tính delta = new_value - old_value.

Độ phức tạp: Mỗi thao tác updatequery đều mất thời gian O(log N). Truy vấn đoạn cũng là O(log N).

Bài Tập 2: Cập nhật đoạn - Truy vấn điểm

Đây là một biến thể thú vị. Thay vì cập nhật một điểm, ta cập nhật toàn bộ một đoạn, và truy vấn giá trị tại một điểm cụ thể. Fenwick Tree không trực tiếp hỗ trợ cập nhật đoạn, nhưng ta có thể sử dụng kỹ thuật mảng sai phân (difference array).

Bài toán: Cho một mảng A gồm N phần tử (ban đầu coi là 0). Cần thực hiện hai loại thao tác:

  1. Cộng thêm một giá trị v vào tất cả các phần tử trong đoạn A[L...R].
  2. Truy vấn giá trị hiện tại của phần tử A[i].

Phân tích và Giải pháp:

Ý tưởng là sử dụng Fenwick Tree để quản lý mảng sai phân B, nơi B[i] lưu sự thay đổi giữa A[i]A[i-1] (với A[0]=0). Khi đó, A[i] chính là tổng tiền tố của mảng B đến index i: A[i] = B[1] + B[2] + ... + B[i].

Một thao tác cập nhật đoạn [L, R] thêm v vào mảng A tương đương với:

  • Tăng A[L] lên v. Điều này làm B[L] tăng v.
  • Sau index R, sự thay đổi v không còn nữa. Điều này có nghĩa là A[R+1] không tăng v như A[R]. Sự thay đổi giữa A[R+1]A[R] giảm đi v. Tương đương với việc giảm B[R+1] đi v.

Vậy, cập nhật đoạn [L, R] với giá trị v trong mảng A chỉ cần thực hiện hai cập nhật điểm trên mảng sai phân B:

  • Cộng v vào B[L].
  • Trừ v vào B[R+1].

Để tính A[i], ta chỉ cần tính tổng tiền tố của mảng B đến i, tức là query(i) trên Fenwick Tree quản lý mảng B.

Mã nguồn C++ minh họa:

#include <iostream>
#include <vector>

// Sử dụng 1-based indexing cho Fenwick Tree
class FenwickTree {
private:
    std::vector<long long> bit;
    int size;

public:
    FenwickTree(int n) : size(n), bit(n + 1, 0) {}

    void update(int index, long long delta) {
        for (; index <= size; index += index & -index) {
            bit[index] += delta;
        }
    }

    long long query(int index) {
        long long sum = 0;
        for (; index > 0; index -= index & -index) {
            sum += bit[index];
        }
        return sum;
    }

    // Không cần query_range cho bài này, chỉ query point.
};

int main() {
    int n; // Kích thước mảng gốc
    std::cout << "Nhap kich thuoc mang N: ";
    std::cin >> n;

    // Khởi tạo Fenwick Tree để quản lý mảng sai phân (difference array)
    // Mảng sai phân cũng có kích thước N, nhưng ta cần node R+1 cho cập nhật đoạn
    // nên kích thước BIT vẫn là N+1.
    FenwickTree ft_diff(n);

    // Mảng ban đầu coi như toàn 0 -> mảng sai phân ban đầu toàn 0

    int num_operations;
    std::cout << "Nhap so luong thao tac: ";
    std::cin >> num_operations;

    for (int i = 0; i < num_operations; ++i) {
        int type; // 1: update range, 2: query point
        std::cout << "Nhap loai thao tac (1: update range, 2: query point): ";
        std::cin >> type;

        if (type == 1) {
            int L, R;
            long long val; // Gia tri can cong them
            std::cout << "Nhap doan [L, R] (1-based) va gia tri v: ";
            std::cin >> L >> R >> val;
            if (L >= 1 && L <= R && R <= n) {
                // Cap nhat point L tren mang sai phan
                ft_diff.update(L, val);
                // Cap nhat point R+1 tren mang sai phan (neu R+1 <= N)
                if (R + 1 <= n) {
                    ft_diff.update(R + 1, -val);
                }
                 std::cout << "Da cap nhat doan [" << L << ", " << R << "] cong them " << val << std::endl;
            } else {
                 std::cout << "Doan cap nhat khong hop le!" << std::endl;
            }
        } else if (type == 2) {
            int index; // Index (1-based) can truy van
            std::cout << "Nhap index (1-based) can truy van gia tri: ";
            std::cin >> index;
            if (index >= 1 && index <= n) {
                 // Gia tri tai index chinh la tong tien to den index tren mang sai phan
                 long long value_at_index = ft_diff.query(index);
                 std::cout << "Gia tri tai index " << index << " la: " << value_at_index << std::endl;
            } else {
                 std::cout << "Index khong hop le!" << std::endl;
            }
        } else {
            std::cout << "Loai thao tac khong hop le!" << std::endl;
        }
    }

    return 0;
}

Giải thích mã nguồn:

  • Chúng ta chỉ cần một Fenwick Tree (ft_diff) để quản lý mảng sai phân ẩn.
  • Hàm update(L, R, val) không tồn tại trực tiếp. Thay vào đó, thao tác cập nhật đoạn được mô phỏng bằng hai lần gọi ft_diff.update(): một lần ft_diff.update(L, val) và một lần ft_diff.update(R+1, -val). Lưu ý kiểm tra R+1 <= n để tránh truy cập ngoài phạm vi mảng.
  • Hàm query_point(index) cũng không tồn tại trực tiếp. Giá trị tại index i của mảng gốc chính là tổng tiền tố đến i của mảng sai phân, được tính bằng ft_diff.query(index).

Độ phức tạp: Mỗi thao tác cập nhật đoạn (update_range) vẫn là O(log N) vì nó gọi 2 lần update điểm. Thao tác truy vấn điểm (query_point) là O(log N).

Bài Tập 3: Cập nhật đoạn - Truy vấn đoạn

Đây là bài toán phức tạp nhất trong ba bài tập cơ bản, kết hợp cả hai dạng trước.

Bài toán: Cho một mảng A gồm N phần tử (ban đầu coi là 0). Cần thực hiện hai loại thao tác:

  1. Cộng thêm một giá trị v vào tất cả các phần tử trong đoạn A[L...R].
  2. Tính tổng các phần tử trong đoạn A[L...R].

Phân tích và Giải pháp:

Chúng ta đã biết cách thực hiện cập nhật đoạn bằng mảng sai phân. A[i] = sum(B[1...i]). Giờ ta cần tính tổng đoạn Sum[L...R] = A[L] + A[L+1] + ... + A[R].

Thay thế A[i] bằng tổng tiền tố của B: Sum[L...R] = sum(B[1...L]) + sum(B[1...L+1]) + ... + sum(B[1...R])

Đây không phải là một tổng tiền tố đơn giản của B. Hãy xem xét tổng tiền tố của A: PrefixSumA[k] = A[1] + A[2] + ... + A[k] PrefixSumA[k] = (B[1]) + (B[1]+B[2]) + ... + (B[1]+B[2]+...+B[k])

Nhóm lại các B[j]: PrefixSumA[k] = k*B[1] + (k-1)*B[2] + ... + 1*B[k] PrefixSumA[k] = sum_{j=1 to k} (k - j + 1) * B[j] PrefixSumA[k] = sum_{j=1 to k} (k+1)*B[j] - sum_{j=1 to k} j*B[j] PrefixSumA[k] = (k+1) * sum_{j=1 to k} B[j] - sum_{j=1 to k} j*B[j]

Để tính PrefixSumA[k], ta cần tính hai loại tổng tiền tố của mảng sai phân B:

  1. sum_{j=1 to k} B[j]: Tổng tiền tố của B. Ta có thể dùng một Fenwick Tree (BIT1) để quản lý mảng B.
  2. sum_{j=1 to k} j*B[j]: Tổng tiền tố của mảng j*B[j]. Ta cần một Fenwick Tree thứ hai (BIT2) để quản lý mảng này.

Khi cập nhật đoạn [L, R] thêm v vào mảng A:

  • Trên mảng B: B[L] += v, B[R+1] -= v.
    • Điều này cần cập nhật BIT1 tại L thêm v và tại R+1 bớt v.
  • Trên mảng j*B[j]: L*B[L] thay đổi L*v, (R+1)*B[R+1] thay đổi (R+1)*(-v).
    • Điều này cần cập nhật BIT2 tại L thêm L*v và tại R+1 bớt (R+1)*v.

Vậy, thao tác cập nhật đoạn [L, R] thêm v:

  • BIT1.update(L, v)
  • BIT1.update(R+1, -v)
  • BIT2.update(L, v * L)
  • BIT2.update(R+1, -v * (R + 1))

Thao tác truy vấn tổng tiền tố PrefixSumA[k]:

  • sum(A[1...k]) = (k+1) * BIT1.query(k) - BIT2.query(k)

Thao tác truy vấn tổng đoạn Sum[L...R]:

  • Sum[L...R] = PrefixSumA[R] - PrefixSumA[L-1]
  • Sum[L...R] = ((R+1) * BIT1.query(R) - BIT2.query(R)) - (L * BIT1.query(L-1) - BIT2.query(L-1))

Mã nguồn C++ minh họa:

#include <iostream>
#include <vector>

// Sử dụng 1-based indexing cho Fenwick Tree
class FenwickTree {
private:
    std::vector<long long> bit;
    int size;

public:
    FenwickTree(int n) : size(n), bit(n + 1, 0) {}

    void update(int index, long long delta) {
        for (; index <= size; index += index & -index) {
            bit[index] += delta;
        }
    }

    long long query(int index) {
        long long sum = 0;
        for (; index > 0; index -= index & -index) {
            sum += bit[index];
        }
        return sum;
    }
};

int main() {
    int n; // Kích thước mảng gốc
    std::cout << "Nhap kich thuoc mang N: ";
    std::cin >> n;

    // Can 2 Fenwick Trees cho bai toan nay
    // ft1: Quan ly mang sai phan B (cap nhat delta vao B[L], B[R+1])
    // ft2: Quan ly mang sai phan B nhan voi chi so j (cap nhat delta*L vao B[L], delta*(R+1) vao B[R+1])
    FenwickTree ft1(n);
    FenwickTree ft2(n);

    int num_operations;
    std::cout << "Nhap so luong thao tac: ";
    std::cin >> num_operations;

    for (int i = 0; i < num_operations; ++i) {
        int type; // 1: update range, 2: query range
        std::cout << "Nhap loai thao tac (1: update range, 2: query range): ";
        std::cin >> type;

        if (type == 1) {
            int L, R;
            long long val; // Gia tri can cong them
            std::cout << "Nhap doan [L, R] (1-based) va gia tri v: ";
            std::cin >> L >> R >> val;
            if (L >= 1 && L <= R && R <= n) {
                // Cap nhat BIT1 (mang B)
                ft1.update(L, val);
                if (R + 1 <= n) {
                    ft1.update(R + 1, -val);
                }

                // Cap nhat BIT2 (mang j*B[j])
                ft2.update(L, val * (L - 1)); // Tại L: delta * (L-1)
                if (R + 1 <= n) {
                    ft2.update(R + 1, -val * R); // Tại R+1: -(delta * R)
                }
                 std::cout << "Da cap nhat doan [" << L << ", " << R << "] cong them " << val << std::endl;
            } else {
                 std::cout << "Doan cap nhat khong hop le!" << std::endl;
            }
        } else if (type == 2) {
            int L, R; // Doan can truy van
            std::cout << "Nhap doan [L, R] (1-based) de truy van tong: ";
            std::cin >> L >> R;
            if (L >= 1 && L <= R && R <= n) {
                 // Ham tinh tong tien to cua mang goc A den k
                 auto query_prefix_sum_A = [&](int k) {
                     if (k <= 0) return 0LL;
                     // Sum[1..k] = k * sum(B[1..k]) - sum(j*B[j] for j=1..k)
                     // Cong thuc chinh xac hon voi 1-based indexing cua BIT:
                     // Sum[1..k] = (k) * BIT1.query(k) - BIT2.query(k)
                     // (Xem giai thich chi tiet hon o phan phan tich, do cong thuc co the bien doi tuy phep nhan chi so)
                     // Quay lai cong thuc tong quat: PrefixSumA[k] = sum_{j=1 to k} A[j] = sum_{j=1 to k} (sum_{i=1 to j} B[i])
                     // = sum_{i=1 to k} B[i] * (so luong j >= i)
                     // = sum_{i=1 to k} B[i] * (k - i + 1)
                     // = sum_{i=1 to k} B[i]*(k+1) - sum_{i=1 to k} B[i]*i
                     // = (k+1) * sum(B[1..k]) - sum(i*B[i] for i=1..k)

                     // Tuy nhien, cach cap nhat BIT2 cua chung ta la val*(L-1) va -val*R
                     // Nen tong tien to den k la: k*sum(B[1..k]) - sum(i*B[i] for i=1..k) + sum_from_update_L (val*(L-1)) + sum_from_update_R+1 (-val*R)
                     // Tong tien to cua A den k (PrefixSumA[k]) sau khi update (L, R, val)
                     // = tong cac val * (so lan val anh huong den A[i] voi i <= k)
                     // Mot cap nhat (L, R, val) anh huong den A[i] them val voi moi i thuoc [L, min(R, k)]. Co min(R, k) - L + 1 phan tu.
                     // Sum A[1..k] = sum (tong cac cap nhat toi A[i]) voi i tu 1 toi k
                     // = sum_over_updates (val * do dai doan giao [L_u, R_u] va [1, k])
                     // Day la cach nghi phuc tap. Hay dung lai cong thuc mảng sai phan:
                     // A[i] = sum(B[1..i])
                     // Sum(A[L..R]) = Sum(A[1..R]) - Sum(A[1..L-1])
                     // Sum(A[1..k]) = sum_{i=1..k} A[i] = sum_{i=1..k} (sum_{j=1..i} B[j])
                     // = sum_{j=1..k} B[j] * (k-j+1)
                     // = (k+1)*sum_{j=1..k} B[j] - sum_{j=1..k} j*B[j]
                     // BIT1.query(k) = sum_{j=1..k} B[j]
                     // BIT2.query(k) = sum_{j=1..k} j*B[j] (do cach chung ta update BIT2: update(idx, val*idx))
                     // Wait, cai dat o tren update(L, val*(L-1)) va update(R+1, -val*R)
                     // Khi update L, R voi val, ta them +val vao B[L] va -val vao B[R+1]
                     // A[i] = sum_{j=1..i} B[j]
                     // Sum A[1..k] = sum_{i=1..k} A[i] = sum_{i=1..k} sum_{j=1..i} B[j]
                     // = sum_{j=1..k} B[j] * (k-j+1)
                     // = (k+1) * sum_{j=1..k} B[j] - sum_{j=1..k} j*B[j]
                     // Ta can 2 BIT:
                     // BIT1: luu B[j] -> query(k) cho ra sum_{j=1..k} B[j]
                     // BIT2: luu j*B[j] -> query(k) cho ra sum_{j=1..k} j*B[j]
                     // Update (L, R, val):
                     // B[L] += val, B[R+1] -= val
                     // BIT1.update(L, val), BIT1.update(R+1, -val)
                     // j*B[j] update:
                     // L*B[L] += L*val
                     // (R+1)*B[R+1] -= (R+1)*val
                     // BIT2.update(L, L*val), BIT2.update(R+1, -(R+1)*val)
                     // Công thức truy vấn tổng tiền tố Sum(A[1..k]) = (k+1)*BIT1.query(k) - BIT2.query(k) là đúng với cách update này.

                     long long sum_B = ft1.query(k); // sum_{j=1..k} B[j]
                     long long sum_jB = ft2.query(k); // sum_{j=1..k} j*B[j]
                     return (long long)(k + 1) * sum_B - sum_jB;
                 };

                 long long total_sum = query_prefix_sum_A(R) - query_prefix_sum_A(L - 1);
                 std::cout << "Tong tren doan [" << L << ", " << R << "] la: " << total_sum << std::endl;
            } else {
                 std::cout << "Doan truy van khong hop le!" << std::endl;
            }
        } else {
            std::cout << "Loai thao tac khong hop le!" << std::endl;
        }
    }

    return 0;
}

Giải thích mã nguồn:

  • Chúng ta sử dụng hai Fenwick Tree: ft1 để quản lý mảng sai phân B, và ft2 để quản lý mảng j*B[j].
  • Hàm update_range(L, R, val) (được mô phỏng trong main): Thực hiện hai cập nhật điểm trên ft1 (L với val, R+1 với -val) và hai cập nhật điểm trên ft2 (L với val * L, R+1 với -val * (R+1)).
  • Hàm query_prefix_sum_A(k) (một lambda function trong main): Tính tổng tiền tố của mảng gốc A đến index k sử dụng công thức (k+1) * ft1.query(k) - ft2.query(k).
  • Hàm query_range(L, R) (được mô phỏng trong main): Tính tổng đoạn của mảng gốc A từ L đến R bằng cách lấy hiệu hai tổng tiền tố: query_prefix_sum_A(R) - query_prefix_sum_A(L - 1).

Độ phức tạp: Mỗi thao tác cập nhật đoạn (update_range) là O(log N) (gọi 4 lần update điểm). Mỗi thao tác truy vấn đoạn (query_range) là O(log N) (gọi 2 lần query prefix sum, mỗi lần query prefix sum gọi 2 lần query trên các BIT).

Bài tập ví dụ:

Đếm Phân Biệt

Trong một chuyến đi đánh cá, FullHouse Dev đã thu thập được một mảng số liệu về số lượng cá trong từng lưới. Để phân loại hiệu quả của mẻ lưới, họ cần phân tích số lượng loại cá khác nhau trong mỗi mẻ. Với tinh thần nghiêm túc, FullHouse Dev bắt đầu phân loại các mẻ lưới thành ba dạng: Tốt, Xấu và Trung bình.

Bài toán

Cho một mảng \(A\) gồm \(N\) số nguyên và một số nguyên \(X\). Hãy phân loại mảng này theo số lượng phần tử phân biệt:

  • Tốt: nếu mảng chứa đúng \(X\) phần tử phân biệt
  • Xấu: nếu mảng chứa ít hơn \(X\) phần tử phân biệt
  • Trung bình: nếu mảng chứa nhiều hơn \(X\) phần tử phân biệt
INPUT FORMAT:
  • Dòng đầu tiên chứa số nguyên \(T\) - số lượng test case
  • Dòng đầu tiên của mỗi test case chứa hai số nguyên \(N\) và \(X\)
  • Dòng thứ hai của mỗi test case chứa \(N\) số nguyên - các phần tử của mảng \(A\)
OUTPUT FORMAT:
  • Với mỗi test case, in ra kết quả phân loại trên một dòng mới: "Good", "Bad" hoặc "Average"
Ví dụ
INPUT
4
4 1
1 4 2 5
4 2
4 2 1 5
4 3
5 2 4 1
4 4
1 2 4 5
OUTPUT
Average
Average
Average
Good
Giải thích
  • Test case 1: Mảng có 4 phần tử phân biệt (1, 2, 4, 5), nhiều hơn \(X = 1\) nên là "Average"
  • Test case 2: Mảng có 4 phần tử phân biệt, nhiều hơn \(X = 2\) nên là "Average"
  • Test case 3: Mảng có 4 phần tử phân biệt, nhiều hơn \(X = 3\) nên là "Average"
  • Test case 4: Mảng có đúng 4 phần tử phân biệt, bằng \(X = 4\) nên là "Good" Chào bạn, đây là hướng dẫn giải bài "Đếm Phân Biệt" bằng C++ một cách ngắn gọn, không kèm mã nguồn hoàn chỉnh.
  1. Mục tiêu chính: Nhiệm vụ cốt lõi là đếm được số lượng phần tử phân biệt (distinct elements) trong mảng A.
  2. Cách đếm phần tử phân biệt: Phương pháp hiệu quả nhất trong C++ để đếm số lượng phần tử phân biệt là sử dụng các cấu trúc dữ liệu kiểu set.
    • Bạn có thể dùng std::set hoặc std::unordered_set. std::unordered_set thường nhanh hơn (O(1) trung bình cho thao tác thêm) so với std::set (O(log N) cho thao tác thêm).
    • Duyệt qua từng phần tử trong mảng A.
    • Với mỗi phần tử, thêm nó vào std::set (hoặc std::unordered_set). Set tự động loại bỏ các phần tử trùng lặp.
    • Sau khi duyệt hết mảng, kích thước (size()) của set chính là số lượng phần tử phân biệt trong mảng A.
  3. Phân loại: Lấy kích thước của set (số lượng phần tử phân biệt) và so sánh với số nguyên X:
    • Nếu kích thước set bằng X: In ra "Good".
    • Nếu kích thước set nhỏ hơn X: In ra "Bad".
    • Nếu kích thước set lớn hơn X: In ra "Average".
  4. Xử lý nhiều test case: Bọc toàn bộ logic trên trong một vòng lặp chạy T lần.

Tóm tắt các bước thực hiện cho mỗi test case:

  1. Đọc N và X.
  2. Khởi tạo một std::set<int> (hoặc std::unordered_set<int>).
  3. Đọc N phần tử của mảng A, và với mỗi phần tử đọc được, thêm nó vào set.
  4. Lấy kích thước của set: set.size().
  5. So sánh set.size() với X và in ra kết quả tương ứng ("Good", "Bad", "Average").

Làm thêm nhiều bài tập miễn phí tại đây

Comments

There are no comments at the moment.