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

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ạnh và sự 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ặctree
) 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ủai
(i & -i
). - Thao tác
update(index, delta)
: Cộng thêmdelta
vào phần tử tạiindex
trong mảng gốc và cập nhật các phần tử liên quan trongBIT
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 đếnindex
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:
- Cập nhật giá trị của phần tử
A[i]
thành một giá trị mớiv
(hoặc cộng/trừ một lượngdelta
). - 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ảngbit
và kích thướcsize
. - Hàm
update(index, delta)
: Bắt đầu từindex
, ta lặp và cộngdelta
vào các phần tửbit[index]
,bit[index + (index & -index)]
, ... cho đến khi vượt quásize
. Biểu thứcindex & -index
cho ra giá trị của bit cuối cùng khác 0 củaindex
, đâ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ộngbit[index]
,bit[index - (index & -index)]
, ... vàosum
cho đến khiindex
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ạoFenwickTree
với kích thướcN
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ínhdelta = new_value - old_value
.
Độ phức tạp: Mỗi thao tác update
và query
đề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:
- Cộng thêm một giá trị
v
vào tất cả các phần tử trong đoạnA[L...R]
. - 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]
và 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ênv
. Điều này làmB[L]
tăngv
. - Sau index
R
, sự thay đổiv
không còn nữa. Điều này có nghĩa làA[R+1]
không tăngv
nhưA[R]
. Sự thay đổi giữaA[R+1]
vàA[R]
giảm điv
. Tương đương với việc giảmB[R+1]
điv
.
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àoB[L]
. - Trừ
v
vàoB[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ọift_diff.update()
: một lầnft_diff.update(L, val)
và một lầnft_diff.update(R+1, -val)
. Lưu ý kiểm traR+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 indexi
của mảng gốc chính là tổng tiền tố đếni
của mảng sai phân, được tính bằngft_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:
- Cộng thêm một giá trị
v
vào tất cả các phần tử trong đoạnA[L...R]
. - 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:
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.sum_{j=1 to k} j*B[j]
: Tổng tiền tố của mảngj*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ạiL
thêmv
và tạiR+1
bớtv
.
- Điều này cần cập nhật
- Trên mảng
j*B[j]
:L*B[L]
thay đổiL*v
,(R+1)*B[R+1]
thay đổi(R+1)*(-v)
.- Điều này cần cập nhật
BIT2
tạiL
thêmL*v
và tạiR+1
bớt(R+1)*v
.
- Điều này cần cập nhật
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ânB
, vàft2
để quản lý mảngj*B[j]
. - Hàm
update_range(L, R, val)
(được mô phỏng trongmain
): Thực hiện hai cập nhật điểm trênft1
(L
vớival
,R+1
với-val
) và hai cập nhật điểm trênft2
(L
vớival * L
,R+1
với-val * (R+1)
). - Hàm
query_prefix_sum_A(k)
(một lambda function trongmain
): Tính tổng tiền tố của mảng gốc A đến indexk
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 trongmain
): Tính tổng đoạn của mảng gốc A từL
đếnR
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.
- 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.
- 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ặcstd::unordered_set
.std::unordered_set
thường nhanh hơn (O(1)
trung bình cho thao tác thêm) so vớistd::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ặcstd::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.
- Bạn có thể dùng
- 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".
- 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:
- Đọc N và X.
- Khởi tạo một
std::set<int>
(hoặcstd::unordered_set<int>
). - Đọ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.
- Lấy kích thước của set:
set.size()
. - So sánh
set.size()
với X và in ra kết quả tương ứng ("Good", "Bad", "Average").
Comments