Binary Lifting - Nhảy Nhị Phân Trên Mảng¶
Tác giả: FPTOJ Team
Nội dung tham khảo từ: VNOI Wiki, CP-Algorithms - Binary Lifting
1. Bản chất vấn đề¶
Bài toán: Truy vấn phần tử thứ \(k\) trên chuỗi¶
Cho dãy \(A\) gồm \(N\) phần tử. Thực hiện \(Q\) truy vấn: phần tử thứ \(k\) (0-indexed) là gì?
Nếu chỉ có 1 mảng tĩnh \(\Rightarrow\) \(O(1)\) mỗi truy vấn.
Nhưng nếu mỗi phần tử trỏ đến phần tử tiếp theo theo quy tắc nào đó (như nhảy trên chuỗi, nhảy trên cây)?
Binary Lifting: Precompute \(2^i\)-th ancestor cho mỗi phần tử \(\Rightarrow\) truy vấn \(O(\log N)\).
Ứng dụng¶
| Bài toán | Mô tả |
|---|---|
| LCA (Lowest Common Ancestor) | Tìm tổ tiên chung gần nhất trên cây |
| Nhảy trên chuỗi | Từ phần tử \(u\), nhảy \(k\) bước là phần tử nào? |
| Floyd's Cycle Detection | Tìm chu kỳ trong dãy |
| Sparse Table | Truy vấn min/max trên đoạn |
2. Tư duy cốt lõi¶
Ý tưởng: Nhảy theo lũy thừa của 2¶
Thay vì nhảy từng bước 1 (\(O(k)\)), ta nhảy theo lũy thừa của 2:
Với \(m \le \lfloor \log_2 k \rfloor + 1\).
Ví dụ: Nhảy 13 bước = nhảy 8 bước + nhảy 4 bước + nhảy 1 bước (\(13 = 1101_2\)).
Bảng tiền xử lý¶
Gọi up[v][i] = phần tử khi nhảy \(2^i\) bước từ phần tử \(v\).
Công thức:
Nghĩa là: nhảy \(2^i\) bước = nhảy \(2^{i-1}\) bước 2 lần.
Trace chi tiết¶
Cho dãy: \(A = [2, 5, 1, 3, 4]\), với quy tắc \(f(i) = A[i] \mod 5\) (nhảy đến chỉ số \(A[i] \mod 5\)).
| \(i\) | \(A[i]\) | \(f(i) = A[i] \mod 5\) |
|---|---|---|
| 0 | 2 | 2 |
| 1 | 5 | 0 |
| 2 | 1 | 1 |
| 3 | 3 | 3 |
| 4 | 4 | 4 |
Bảng up[v][i]:
| \(v\) | \(up[v][0]\) (nhảy \(2^0=1\) bước) | \(up[v][1]\) (nhảy \(2^1=2\) bước) | \(up[v][2]\) (nhảy \(2^2=4\) bước) |
|---|---|---|---|
| 0 | 2 | \(up[2][0] = 1\) | \(up[1][1] = 0\) |
| 1 | 0 | \(up[0][0] = 2\) | \(up[2][1] = 1\) |
| 2 | 1 | \(up[1][0] = 0\) | \(up[0][1] = 1\) |
| 3 | 3 | \(up[3][0] = 3\) | \(up[3][1] = 3\) |
| 4 | 4 | \(up[4][0] = 4\) | \(up[4][1] = 4\) |
Truy vấn: Từ phần tử 0, nhảy 3 bước (\(3 = 011_2\)):
| Bước | Bit | Nhảy | Kết quả |
|---|---|---|---|
| 1 | bit 0 = 1 | \(2^0 = 1\) bước: \(up[0][0] = 2\) | Đến 2 |
| 2 | bit 1 = 1 | \(2^1 = 2\) bước: \(up[2][1] = 0\) | Đến 0 |
Kết quả: Từ 0, nhảy 3 bước → đến phần tử 0.
3. Phân tích tính đúng đắn¶
Tại sao mọi số bước đều biểu diễn được?¶
Mọi số nguyên dương \(k\) đều biểu diễn được dưới dạng tổng các lũy thừa của 2 (nhị phân). Do đó, mọi số bước \(k\) đều có thể ghép từ các nhảy \(2^0, 2^1, 2^2, \ldots\)
Tại sao up[v][i] = up[up[v][i-1]][i-1]?¶
Nhảy \(2^i\) bước từ \(v\) = nhảy \(2^{i-1}\) bước từ \(v\) → đến \(u = \text{up}[v][i-1]\) → nhảy thêm \(2^{i-1}\) bước từ \(u\) → đến \(\text{up}[u][i-1]\).
Đây là tính chất bán nhóm (semigroup) của phép nhảy.
4. Đánh giá độ phức tạp¶
| Thao tác | Thời gian | Không gian |
|---|---|---|
| Tiền xử lý | \(O(N \log N)\) | \(O(N \log N)\) |
| Nhảy \(k\) bước | \(O(\log N)\) | \(O(1)\) |
| LCA | \(O(\log N)\) mỗi truy vấn | \(O(N \log N)\) |
Code minh họa¶
Binary Lifting trên dãy — Truy vấn nhảy \(k\) bước¶
Minh họa trực quan: Nhảy nhị phân¶
Giả sử \(n=10\), \(k=22 = 10110_2\):
Quá trình truy vấn:
| Bit \(j\) | \(1 \ll j\) | k & (1<<j) | Có nhảy? | v = up[v][j] |
|:---:|:---:|:---:|:---:|:---|
| 0 | 1 | \(22 \& 1 = 0\) | ❌ | Giữ nguyên |
| 1 | 2 | \(22 \& 2 = 2 \neq 0\) | ✅ | Nhảy \(2^1=2\) bước |
| 2 | 4 | \(22 \& 4 = 4 \neq 0\) | ✅ | Nhảy \(2^2=4\) bước |
| 3 | 8 | \(22 \& 8 = 0\) | ❌ | Giữ nguyên |
| 4 | 16 | \(22 \& 16 = 16 \neq 0\) | ✅ | Nhảy \(2^4=16\) bước |
Tổng số bước nhảy: \(2 + 4 + 16 = 22 = k\) ✓
Ứng dụng mở rộng: Phát hiện chu kỳ (Cycle Detection)¶
Khi làm việc với đồ thị hàm số (mỗi đỉnh có đúng một cạnh ra), binary lifting có thể giúp trả lời các truy vấn về chu kỳ.
Bài toán con: Từ đỉnh \(v\), sau bao nhiêu bước thì vào chu kỳ? Chu kỳ có độ dài bao nhiêu?
5. Mẹo và lưu ý¶
5.1 Các trường hợp đặc biệt¶
| Trường hợp | Xử lý |
|---|---|
| \(k = 0\) | Không nhảy bước nào, kết quả là chính đỉnh \(v\). Vòng lặp bit sẽ không kích hoạt bước nhảy nào. |
| \(k\) rất lớn (vượt \(N\)) | Với đồ thị hàm số có chu kỳ, \(k\) lớn sẽ quay vòng trong chu kỳ. Dùng phương pháp chu kỳ ở trên để xử lý. |
| \(N = 1\) | Chỉ có 1 đỉnh, LOG = 1. Bảng up[0][0] có thể trỏ về chính nó (tự vòng). |
| \(k > 2^{LOG}\) | Không thể nhảy quá \(2^{LOG}-1\) bước trong bảng đã tiền xử lý. Với đồ thị hàm số, dùng modulo chu kỳ trước khi nhảy. |
5.2 Cạm bẫy thường gặp¶
- Sai kích thước
LOG: Dùng__lg(n) + 1trong C++ hoặcn.bit_length()trong Python. Nếu đặt \(LOG\) quá nhỏ, nhảy \(k\) lớn sẽ truy cập ngoài mảng. - Nhầm thứ tự vòng lặp tiền xử lý: Vòng ngoài phải là \(j\) (lũy thừa), vòng trong là \(i\) (đỉnh). Nếu đảo ngược,
up[up[i][j-1]][j-1]có thể truy cập giá trị chưa được tính. - Dùng
intcho \(k\): \(k\) có thể lớn hơn \(2^{31}\), dùnglong longtrong C++ và không cần lo trong Python. - Quên kiểm tra bit: Luôn kiểm tra
if (k & (1LL << j))thay vìif (k >> j & 1)để tránh lỗi với \(k\) âm hoặc quá lớn. - Không xử lý trường hợp không tồn tại: Với đồ thị không phải hàm số, nếu nhảy ra ngoài phạm vi, cần kiểm tra đỉnh
-1hoặc giá trị sentinel.
5.3 Mẹo tối ưu¶
- Với \(N \le 2 \times 10^5\), \(LOG \approx 18\). Bảng
uptiêu tốn khoảng \(N \times 18 \times 4\) byte (vớiint) ≈ \(14\text{ MB}\) — hoàn toàn chấp nhận được. - Khi cần lưu thêm thông tin (tổng, min, max) trên đường nhảy, tạo thêm bảng song song với
up. - Kết hợp binary lifting với tìm kiếm nhị phân để giải các bài toán "nhảy đến khi vượt ngưỡng" (ví dụ: tổng trượt \(\ge S\)).
6. Bài tập luyện tập¶
| Mã bài | Tên bài tập | Độ khó | Kiểu bài tập (Bản chất) |
|---|---|---|---|
bla-k-step |
Nhảy K Bước | ⭐ | Mô phỏng nhảy trên đồ thị hàm số bằng Binary Lifting |
bla-next-greater |
Nhảy Phải Số Lớn Hơn | ⭐⭐ | Tìm số lớn hơn tiếp theo kết hợp nhảy nhị phân |
bla-cycle-det |
Tìm Chu Kỳ | ⭐⭐ | Tìm điểm vào chu kỳ, độ dài và khoảng cách trên đồ thị hàm số |
bla-min-edge |
Giá Trị Cực Tiểu Khi Nhảy | ⭐⭐⭐ | Truy vấn giá trị cực tiểu trên đường đi của Binary Lifting |
bla-jump-limit |
Nhảy Đến Khi Vượt Ngưỡng | ⭐⭐⭐ | Tìm kiếm nhị phân kết hợp tổng lũy lũy thừa 2 trên đường đi |
bla-reach-steps |
Số Bước Để Đến V | ⭐⭐⭐ | Tính số bước tối thiểu giữa hai đỉnh hoặc kiểm tra vô nghiệm |
bla-sliding-sum |
Tổng Trượt Vượt Ngưỡng | ⭐⭐⭐ | Tìm vị trí trượt xa nhất có tổng \(\ge S\) |
bla-segment-cover |
Phủ Đoạn | ⭐⭐⭐⭐ | Chọn ít đoạn thẳng nhất phủ kín khoảng \([A, B]\) |
bla-partition |
Phân Hoạch Đoạn Con | ⭐⭐⭐⭐ | Phân chia mảng thành các đoạn có tổng \(\le M\) (RMQ + Binary Lifting) |
bla-kth-distinct |
Số Thứ K Khác Biệt | ⭐⭐⭐⭐ | Jump trên mảng với số lượng phần tử phân biệt bằng \(D\) |
Bài liên quan: * Segment Tree * Fenwick Tree (BIT) * Lowest Common Ancestor (LCA) trên cây