Bạn đang tìm kiếm một cấu trúc dữ liệu hiệu quả để xử lý các truy vấn phạm vi trên mảng tĩnh? Sparse Table chính là giải pháp! Bài viết này cung cấp một cái nhìn toàn diện về Sparse Table, từ khái niệm cơ bản đến các ứng dụng thực tế, cùng với ví dụ code JavaScript dễ hiểu. Hãy cùng khám phá sức mạnh của cấu trúc dữ liệu này!
Sparse Table là một cấu trúc dữ liệu tiền xử lý cho phép trả lời các truy vấn phạm vi trong thời gian O(1) sau khi đã tiền xử lý O(N log N). Điều này làm cho nó trở nên lý tưởng cho các mảng tĩnh, nơi các phần tử không thay đổi sau khi tiền xử lý.
Sparse Table lưu trữ các kết quả đã tính toán trước cho các mảng con chồng lấn có độ dài là lũy thừa của hai. Sử dụng thông tin đã tính toán trước này, chúng ta có thể trả lời các truy vấn trong thời gian hằng số. Chúng ta định nghĩa `st[i][j]`, trong đó:
Ví dụ: nếu `j = 3`, kích thước phạm vi là 2^3 = 8, có nghĩa là `st[i][3]` lưu trữ kết quả cho phạm vi `[i, i + 7]`.
Chúng ta điền `st[i][j]` dựa trên thuộc tính lũy đẳng:
st[i][j] = operation(st[i][j-1], st[i + 2^(j-1)][j-1])
Công thức này đảm bảo rằng mọi khoảng đều được xây dựng bằng cách sử dụng hai khoảng nhỏ hơn đã được tính toán trước đó.
Bây giờ, hãy triển khai một Sparse Table cho Truy Vấn Tìm Giá Trị Nhỏ Nhất (RMQ).
class SparseTable {
constructor(arr) {
this.n = arr.length;
this.log = new Uint8Array(this.n + 1);
this.st = Array.from({ length: this.n }, () => new Int32Array(20));
// Compute logarithm values
for (let i = 2; i <= this.n; i++) this.log[i] = this.log[i >> 1] + 1;
// Initialize Sparse Table with original array values
for (let i = 0; i < this.n; i++) this.st[i][0] = arr[i];
// Fill the table
for (let j = 1; (1 << j) <= this.n; j++) {
for (let i = 0; i + (1 << j) <= this.n; i++) {
this.st[i][j] = Math.min(this.st[i][j - 1], this.st[i + (1 << (j - 1))][j - 1]);
}
}
}
// Query for minimum in range [L, R]
query(L, R) {
let j = this.log[R - L + 1];
return Math.min(this.st[L][j], this.st[R - (1 << j) + 1][j]);
}
}
// Usage
let arr = [1, 3, 5, 7, 9, 2, 6, 4, 8, 0];
let st = new SparseTable(arr);
console.log(st.query(2, 5)); // Output: 2
console.log(st.query(0, 9)); // Output: 0
Đối với một phạm vi cho trước `[L, R]`, chúng ta sử dụng đoạn lũy thừa của hai lớn nhất phù hợp với phạm vi. Điều này có nghĩa là:
answer = min(st[L][j], st[R - 2^j + 1][j])
trong đó:
j = floor(log2(R - L + 1))
Chúng ta sử dụng hai khoảng chồng lấn để bao phủ toàn bộ phạm vi.
Vì chúng ta chỉ thực hiện hai lần tra cứu và một thao tác `Math.min()`, độ phức tạp thời gian là O(1).
Sparse Table cực kỳ hiệu quả cho các truy vấn phạm vi tĩnh, nhưng không phù hợp cho các bài toán yêu cầu sửa đổi.
Chúng ta thay thế `Math.min()` bằng `Math.max()`, và việc triển khai vẫn giữ nguyên.
Thay vì min/max, chúng ta sử dụng `gcd(a, b)`:
function gcd(a, b) {
while (b) [a, b] = [b, a % b];
return a;
}
Sau đó, thay thế `Math.min()` trong tính toán `st[i][j]` bằng `gcd()`.
Tính Năng | Sparse Table | Segment Tree |
---|---|---|
Thời Gian Tiền Xử Lý | O(n log n) | O(n) |
Thời Gian Truy Vấn | O(1) | O(log n) |
Thời Gian Cập Nhật | Không Thể | O(log n) |
Độ Phức Tạp Không Gian | O(n log n) | O(n) |
Sparse Table chiến thắng khi:
Sparse Table siêu hiệu quả để trả lời các truy vấn phạm vi khi không cần cập nhật. Bằng cách tính toán trước các đoạn lũy thừa của hai, chúng ta đảm bảo các truy vấn có thể được trả lời trong thời gian hằng số.
Hãy thử triển khai Sparse Table cho các bài toán khác nhau như:
Bằng cách làm chủ Sparse Table, bạn mở khóa một kỹ thuật mạnh mẽ để truy vấn phạm vi nhanh chóng trong lập trình cạnh tranh!
Chúc bạn coding vui vẻ!
Bài viết liên quan