Cây phân đoạn (Segment Tree)
Cây Fenwick làm tốt tổng đoạn có cập nhật, nhưng không làm được nhỏ nhất trên đoạn khi dãy thay đổi, hay gán cả đoạn bằng một giá trị. Cây phân đoạn làm được gần như mọi câu hỏi trên đoạn, miễn là thông tin của một đoạn ghép được từ thông tin của hai nửa.
Cấu trúc cây
Phần tiêu đề “Cấu trúc cây”Nút gốc quản lý cả dãy [0, n - 1]. Mỗi nút quản lý đoạn [l, r] có hai con: con trái quản lý [l, giữa], con phải quản lý [giữa + 1, r]. Nút lá quản lý một phần tử. Mỗi nút giữ thông tin của đoạn mình quản lý, ví dụ số nhỏ nhất.
Dãy 5 2 4 7 1 3 (đoạn ghi theo vị trí, giá trị nhỏ nhất trong ngoặc):
[0,5] (1) / \ [0,2] (2) [3,5] (1) / \ / \ [0,1] (2) [2] (4) [3,4] (1) [5] (3) / \ / \[0] (5) [1] (2) [3] (7) [4] (1)Cây có khoảng 2n nút, cao khoảng log2n tầng. Lưu trong mảng: nút gốc là số 1, con của nút i là 2i và 2i + 1, mảng cần khoảng 4n ô.
Hỏi đoạn [x, y]: đi từ gốc. Đoạn của nút nằm hẳn trong [x, y] thì lấy luôn thông tin của nút; nằm hẳn ngoài thì bỏ qua; còn lại thì hỏi tiếp hai con rồi ghép. Mỗi tầng chỉ có nhiều nhất bốn nút được thăm, nên mỗi câu hỏi khoảng 4 × log2n bước.
Sửa một phần tử: sửa lá, rồi đi ngược lên gốc, tính lại mỗi nút cha từ hai con. Khoảng log2n bước.
import syssys.setrecursionlimit(10000)
def xay(nut, l, r): if l == r: cay[nut] = a[l] return giua = (l + r) // 2 xay(2 * nut, l, giua) xay(2 * nut + 1, giua + 1, r) cay[nut] = min(cay[2 * nut], cay[2 * nut + 1])
def sua(nut, l, r, vi_tri, gia_tri): if l == r: cay[nut] = gia_tri return giua = (l + r) // 2 if vi_tri <= giua: sua(2 * nut, l, giua, vi_tri, gia_tri) else: sua(2 * nut + 1, giua + 1, r, vi_tri, gia_tri) cay[nut] = min(cay[2 * nut], cay[2 * nut + 1])
def hoi(nut, l, r, x, y): if y < l or r < x: # hẳn ngoài return float('inf') if x <= l and r <= y: # hẳn trong return cay[nut] giua = (l + r) // 2 return min(hoi(2 * nut, l, giua, x, y), hoi(2 * nut + 1, giua + 1, r, x, y))
du_lieu = sys.stdin.read().split()n, q = int(du_lieu[0]), int(du_lieu[1])a = list(map(int, du_lieu[2:2 + n]))cay = [0] * (4 * n)xay(1, 0, n - 1)ket_qua = []vt = 2 + nfor _ in range(q): loai, x, y = du_lieu[vt], int(du_lieu[vt + 1]), int(du_lieu[vt + 2]) vt += 3 if loai == '1': sua(1, 0, n - 1, x - 1, y) # gán a[x] = y else: ket_qua.append(hoi(1, 0, n - 1, x - 1, y - 1)) # nhỏ nhất từ x đến yprint("\n".join(map(str, ket_qua)))#include <iostream>#include <vector>#include <algorithm>#include <climits>using namespace std;
int n;vector<long long> a, cay;
void xay(int nut, int l, int r) { if (l == r) { cay[nut] = a[l]; return; } int giua = (l + r) / 2; xay(2 * nut, l, giua); xay(2 * nut + 1, giua + 1, r); cay[nut] = min(cay[2 * nut], cay[2 * nut + 1]);}
void sua(int nut, int l, int r, int viTri, long long giaTri) { if (l == r) { cay[nut] = giaTri; return; } int giua = (l + r) / 2; if (viTri <= giua) sua(2 * nut, l, giua, viTri, giaTri); else sua(2 * nut + 1, giua + 1, r, viTri, giaTri); cay[nut] = min(cay[2 * nut], cay[2 * nut + 1]);}
long long hoi(int nut, int l, int r, int x, int y) { if (y < l || r < x) return LLONG_MAX; // hẳn ngoài if (x <= l && r <= y) return cay[nut]; // hẳn trong int giua = (l + r) / 2; return min(hoi(2 * nut, l, giua, x, y), hoi(2 * nut + 1, giua + 1, r, x, y));}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int q; cin >> n >> q; a.resize(n); for (auto &x : a) cin >> x; cay.assign(4 * n, 0); xay(1, 0, n - 1); while (q--) { int loai, x; long long y; cin >> loai >> x >> y; if (loai == 1) sua(1, 0, n - 1, x - 1, y); // gán a[x] = y else cout << hoi(1, 0, n - 1, x - 1, y - 1) << "\n"; // nhỏ nhất từ x đến y } return 0;}Ghép thông tin của hai nửa
Phần tiêu đề “Ghép thông tin của hai nửa”Đổi min thành phép khác là được cây cho bài khác. Điều kiện duy nhất: biết thông tin của hai nửa thì
tính được thông tin của cả đoạn.
| Thông tin của nút | Ghép từ con trái T và con phải P |
|---|---|
| tổng | T + P |
| nhỏ nhất, lớn nhất, ƯCLN | min, max, ƯCLN của T và P |
| lớn nhất và số lần xuất hiện | lấy bên lớn hơn; bằng nhau thì cộng số lần |
| đoạn con tổng lớn nhất | giữ bốn số: tổng, tiền tố lớn nhất, hậu tố lớn nhất, đoạn con lớn nhất; đoạn con tốt nhất của cả đoạn nằm hẳn bên trái, hẳn bên phải, hoặc vắt qua giữa (hậu tố của T + tiền tố của P) |
Cập nhật lười
Phần tiêu đề “Cập nhật lười”Cộng v vào cả đoạn [x, y]: nếu sửa từng phần tử thì mỗi lần mất tới n bước. Cập nhật lười (lazy propagation): khi một nút nằm hẳn trong [x, y], chỉ sửa thông tin của nút đó và ghi chú “cả đoạn này còn nợ cộng v” vào nút, không đi xuống các con. Khi nào cần đi xuống con (để hỏi hay sửa một phần), mới đẩy ghi chú xuống hai con.
Với tổng: nút quản lý đoạn dài len, cộng v vào cả đoạn thì tổng tăng v × len. Nhờ ghi chú, mỗi lần cộng đoạn chỉ thăm khoảng 4 × log2n nút, giống một câu hỏi.
def day_xuong(nut, l, r): if no[nut]: giua = (l + r) // 2 for con, cl, cr in ((2 * nut, l, giua), (2 * nut + 1, giua + 1, r)): tong[con] += no[nut] * (cr - cl + 1) no[con] += no[nut] no[nut] = 0
def cong_doan(nut, l, r, x, y, v): if y < l or r < x: return if x <= l and r <= y: tong[nut] += v * (r - l + 1) no[nut] += v # ghi chú, chưa đi xuống return day_xuong(nut, l, r) giua = (l + r) // 2 cong_doan(2 * nut, l, giua, x, y, v) cong_doan(2 * nut + 1, giua + 1, r, x, y, v) tong[nut] = tong[2 * nut] + tong[2 * nut + 1]
def tong_doan(nut, l, r, x, y): if y < l or r < x: return 0 if x <= l and r <= y: return tong[nut] day_xuong(nut, l, r) giua = (l + r) // 2 return tong_doan(2 * nut, l, giua, x, y) + tong_doan(2 * nut + 1, giua + 1, r, x, y)vector<long long> tong, no;
void dayXuong(int nut, int l, int r) { if (no[nut] == 0) return; int giua = (l + r) / 2; tong[2 * nut] += no[nut] * (giua - l + 1); no[2 * nut] += no[nut]; tong[2 * nut + 1] += no[nut] * (r - giua); no[2 * nut + 1] += no[nut]; no[nut] = 0;}
void congDoan(int nut, int l, int r, int x, int y, long long v) { if (y < l || r < x) return; if (x <= l && r <= y) { tong[nut] += v * (r - l + 1); no[nut] += v; // ghi chú, chưa đi xuống return; } dayXuong(nut, l, r); int giua = (l + r) / 2; congDoan(2 * nut, l, giua, x, y, v); congDoan(2 * nut + 1, giua + 1, r, x, y, v); tong[nut] = tong[2 * nut] + tong[2 * nut + 1];}
long long tongDoan(int nut, int l, int r, int x, int y) { if (y < l || r < x) return 0; if (x <= l && r <= y) return tong[nut]; dayXuong(nut, l, r); int giua = (l + r) / 2; return tongDoan(2 * nut, l, giua, x, y) + tongDoan(2 * nut + 1, giua + 1, r, x, y);}Lưu ý: cây phân đoạn viết bằng Python chạy chậm hơn C++ nhiều lần (mỗi câu hỏi gọi đệ quy vài chục lần). Với n, q tới 2 × 105, nên dùng C++ khi thi.
Luyện tập
Phần tiêu đề “Luyện tập”Gồm 16 bài, chia thành 5 nhóm từ dễ đến khó. Mỗi bài có một dữ liệu kiểm tra: các bạn chạy chương trình của mình với dữ liệu đó rồi so với đáp số.
Nhóm 1. Sửa một phần tử, hỏi đoạn
Phần tiêu đề “Nhóm 1. Sửa một phần tử, hỏi đoạn”Bài 1. Sửa phần tử, nhỏ nhất trên đoạn
Phần tiêu đề “Bài 1. Sửa phần tử, nhỏ nhất trên đoạn”Dòng đầu có n, q (1 ≤ n, q ≤ 2 × 105), dòng thứ hai có n số nguyên (|ai| ≤ 109), q dòng sau mỗi dòng là 1 i v (gán ai = v) hoặc 2 l r (hỏi số nhỏ nhất từ al đến ar). In ra đáp số các câu hỏi loại 2.
Ví dụ:
| Nhập | In ra |
|---|---|
6 45 2 4 7 1 32 1 32 3 61 5 92 3 6 | 213 |
Kiểm tra: nhập
8 64 -2 7 0 3 3 -8 12 1 81 7 102 1 82 5 81 2 62 1 3Bài 2. Sửa phần tử, ƯCLN trên đoạn
Phần tiêu đề “Bài 2. Sửa phần tử, ƯCLN trên đoạn”Dòng đầu có n, q (1 ≤ n, q ≤ 2 × 105), dòng thứ hai có n số nguyên dương (ai ≤ 109), q dòng sau mỗi dòng là 1 i v (gán ai = v, 1 ≤ v ≤ 109) hoặc 2 l r (hỏi ƯCLN của al, …, ar). In ra đáp số các câu hỏi loại 2.
Ví dụ:
| Nhập | In ra |
|---|---|
5 412 18 24 9 62 1 31 4 362 1 52 4 4 | 6636 |
Kiểm tra: nhập
6 5100 75 50 30 45 602 1 32 4 61 3 72 1 32 2 6Bài 3. Sửa phần tử, tổng đoạn
Phần tiêu đề “Bài 3. Sửa phần tử, tổng đoạn”Dòng đầu có n, q (1 ≤ n, q ≤ 2 × 105), dòng thứ hai có n số nguyên (|ai| ≤ 109), q dòng sau mỗi dòng là 1 i v (gán ai = v) hoặc 2 l r (hỏi tổng al + … + ar). In ra đáp số các câu hỏi loại 2.
Ví dụ:
| Nhập | In ra |
|---|---|
5 41 2 3 4 52 2 41 3 -72 1 52 3 3 | 95-7 |
Kiểm tra: nhập
6 510 -10 20 -20 30 -302 1 61 1 02 1 31 6 1002 4 6Bài 4. Lớn nhất và vị trí
Phần tiêu đề “Bài 4. Lớn nhất và vị trí”Dòng đầu có n, q (1 ≤ n, q ≤ 2 × 105), dòng thứ hai có n số nguyên (|ai| ≤ 109), q dòng sau mỗi dòng là 1 i v (gán ai = v) hoặc 2 l r. Với câu hỏi loại 2, in ra số lớn nhất trong đoạn và vị trí của nó (đánh số từ 1; có nhiều vị trí thì in vị trí nhỏ nhất).
Ví dụ:
| Nhập | In ra |
|---|---|
5 43 8 1 8 22 1 51 2 02 1 52 3 3 | 8 28 41 3 |
Kiểm tra: nhập
6 55 5 5 5 5 52 2 51 4 92 1 61 4 -12 3 6Nhóm 2. Ghép thông tin phức tạp hơn
Phần tiêu đề “Nhóm 2. Ghép thông tin phức tạp hơn”Bài 5. Lớn nhất và số lần xuất hiện
Phần tiêu đề “Bài 5. Lớn nhất và số lần xuất hiện”Dòng đầu có n, q (1 ≤ n, q ≤ 2 × 105), dòng thứ hai có n số nguyên (|ai| ≤ 109), q dòng sau mỗi dòng là 1 i v (gán ai = v) hoặc 2 l r. Với câu hỏi loại 2, in ra số lớn nhất trong đoạn và số lần nó xuất hiện trong đoạn.
Ví dụ:
| Nhập | In ra |
|---|---|
6 44 7 7 2 7 12 1 62 1 31 3 52 1 6 | 7 37 27 2 |
Kiểm tra: nhập
8 53 3 3 1 3 2 2 32 1 82 4 71 1 92 1 82 6 7Bài 6. Đoạn con tổng lớn nhất có cập nhật
Phần tiêu đề “Bài 6. Đoạn con tổng lớn nhất có cập nhật”Dòng đầu có n, q (1 ≤ n, q ≤ 105), dòng thứ hai có n số nguyên (|ai| ≤ 109), q dòng sau mỗi dòng là 1 i v (gán ai = v) hoặc 2 l r. Với câu hỏi loại 2, in ra tổng lớn nhất của một đoạn con khác rỗng nằm trong [l, r].
Ví dụ:
| Nhập | In ra |
|---|---|
6 42 -5 3 -1 4 -22 1 62 1 21 2 52 1 6 | 6213 |
Kiểm tra: nhập
8 5-3 -1 -4 -1 -5 -9 -2 -62 1 81 5 102 1 82 1 42 6 8Bài 7. Dãy ngoặc đúng trên đoạn
Phần tiêu đề “Bài 7. Dãy ngoặc đúng trên đoạn”Dòng đầu có n, q (1 ≤ n, q ≤ 105), dòng thứ hai là xâu n ký tự ( và ), q dòng sau mỗi dòng là 1 i (đổi ký tự thứ i sang ngoặc ngược lại) hoặc 2 l r. Với câu hỏi loại 2, in ra YES nếu đoạn ký tự từ l đến r là dãy ngoặc đúng, ngược lại NO.
Ví dụ:
| Nhập | In ra |
|---|---|
6 4(()())2 1 62 2 51 32 1 6 | YESYESNO |
Kiểm tra: nhập
8 6(())()()2 1 42 2 31 42 1 41 12 1 2Nhóm 3. Tìm vị trí trên cây
Phần tiêu đề “Nhóm 3. Tìm vị trí trên cây”Bài 8. Vị trí đầu tiên không nhỏ hơn x
Phần tiêu đề “Bài 8. Vị trí đầu tiên không nhỏ hơn x”Dòng đầu có n, q (1 ≤ n, q ≤ 2 × 105), dòng thứ hai có n số nguyên (|ai| ≤ 109), q dòng sau mỗi dòng là 1 i v (gán ai = v) hoặc 2 x (hỏi vị trí nhỏ nhất i có ai ≥ x, hoặc -1 nếu không có). In ra đáp số các câu hỏi loại 2.
Ví dụ:
| Nhập | In ra |
|---|---|
6 51 4 2 7 3 52 32 81 2 02 32 -5 | 2-141 |
Kiểm tra: nhập
8 6-3 -3 5 0 9 2 9 12 62 101 5 -12 61 1 62 6Bài 9. Số 1 thứ k
Phần tiêu đề “Bài 9. Số 1 thứ k”Dãy n bit (0 hoặc 1). Dòng đầu có n, q (1 ≤ n, q ≤ 2 × 105), dòng thứ hai là xâu n ký tự 0/1, q dòng sau mỗi dòng là 1 i (đổi bit thứ i) hoặc 2 k (hỏi vị trí của bit 1 thứ k tính từ trái, hoặc -1 nếu có ít hơn k bit 1). In ra đáp số các câu hỏi loại 2.
Ví dụ:
| Nhập | In ra |
|---|---|
8 5101101002 32 51 22 22 4 | 4-124 |
Kiểm tra: nhập
10 600000000012 11 32 12 21 102 2Nhóm 4. Cập nhật lười
Phần tiêu đề “Nhóm 4. Cập nhật lười”Bài 10. Cộng đoạn, tổng đoạn
Phần tiêu đề “Bài 10. Cộng đoạn, tổng đoạn”Dòng đầu có n, q (1 ≤ n, q ≤ 105), dòng thứ hai có n số nguyên (|ai| ≤ 109), q dòng sau mỗi dòng là 1 l r v (cộng v vào mọi al, …, ar, |v| ≤ 109) hoặc 2 l r (hỏi tổng al + … + ar). In ra đáp số các câu hỏi loại 2.
Ví dụ:
| Nhập | In ra |
|---|---|
5 41 2 3 4 51 2 4 102 1 51 1 5 -12 3 3 | 4512 |
Kiểm tra: nhập
6 60 0 0 0 0 01 1 6 31 2 3 -52 1 61 6 6 1002 3 62 1 2Bài 11. Cộng đoạn, nhỏ nhất trên đoạn
Phần tiêu đề “Bài 11. Cộng đoạn, nhỏ nhất trên đoạn”Dòng đầu có n, q (1 ≤ n, q ≤ 105), dòng thứ hai có n số nguyên (|ai| ≤ 109), q dòng sau mỗi dòng là 1 l r v (cộng v vào mọi al, …, ar, |v| ≤ 109) hoặc 2 l r (hỏi số nhỏ nhất từ al đến ar). In ra đáp số các câu hỏi loại 2.
Ví dụ:
| Nhập | In ra |
|---|---|
5 43 1 4 1 52 1 51 2 4 52 1 52 2 4 | 136 |
Kiểm tra: nhập
6 610 20 30 40 50 601 4 6 -352 1 61 1 1 1002 1 31 1 6 12 4 4Bài 12. Gán đoạn, tổng đoạn
Phần tiêu đề “Bài 12. Gán đoạn, tổng đoạn”Dòng đầu có n, q (1 ≤ n, q ≤ 105), dòng thứ hai có n số nguyên (|ai| ≤ 109), q dòng sau mỗi dòng là 1 l r v (gán mọi al, …, ar bằng v, |v| ≤ 109) hoặc 2 l r (hỏi tổng al + … + ar). In ra đáp số các câu hỏi loại 2.
Ví dụ:
| Nhập | In ra |
|---|---|
5 41 2 3 4 51 2 4 02 1 51 3 5 72 2 4 | 614 |
Kiểm tra: nhập
6 65 5 5 5 5 51 1 3 -12 1 61 2 5 102 1 61 6 6 02 4 6Bài 13. Lật bit trên đoạn
Phần tiêu đề “Bài 13. Lật bit trên đoạn”Dãy n bit ban đầu toàn 0. Dòng đầu có n, q (1 ≤ n, q ≤ 105), q dòng sau mỗi dòng là 1 l r (lật mọi bit từ l đến r: 0 thành 1, 1 thành 0) hoặc 2 l r (hỏi số bit 1 từ l đến r). In ra đáp số các câu hỏi loại 2.
Ví dụ:
| Nhập | In ra |
|---|---|
6 51 2 52 1 61 4 62 1 62 4 4 | 430 |
Kiểm tra: nhập
8 61 1 81 3 42 1 81 4 72 3 62 8 8Bài 14. Nhân và cộng trên đoạn
Phần tiêu đề “Bài 14. Nhân và cộng trên đoạn”Dòng đầu có n, q (1 ≤ n, q ≤ 105), dòng thứ hai có n số nguyên (0 ≤ ai nhỏ hơn 109 + 7), q dòng sau mỗi dòng là 1 l r v (nhân mọi al, …, ar với v), 2 l r v (cộng v vào mọi al, …, ar) hoặc 3 l r (hỏi tổng al + … + ar chia lấy dư 109 + 7), với 0 ≤ v nhỏ hơn 109 + 7. In ra đáp số các câu hỏi loại 3.
Ví dụ:
| Nhập | In ra |
|---|---|
4 41 2 3 41 1 4 22 2 3 53 1 43 3 3 | 3011 |
Kiểm tra: nhập
5 51 1 1 1 12 1 5 31 2 4 103 1 52 3 3 10000000063 2 4Nhóm 5. Kỹ thuật khác
Phần tiêu đề “Nhóm 5. Kỹ thuật khác”Bài 15. Lấy căn trên đoạn
Phần tiêu đề “Bài 15. Lấy căn trên đoạn”Dòng đầu có n, q (1 ≤ n, q ≤ 105), dòng thứ hai có n số nguyên (0 ≤ ai ≤ 1012), q dòng sau mỗi dòng là 1 l r (thay mọi ai với l ≤ i ≤ r bằng phần nguyên căn bậc hai của nó) hoặc 2 l r (hỏi tổng al + … + ar). In ra đáp số các câu hỏi loại 2.
Ví dụ:
| Nhập | In ra |
|---|---|
5 41 100 5 64 02 1 51 2 42 1 51 1 5 | 17021 |
Kiểm tra: nhập
6 61000000000000 2 3 16 81 71 1 62 1 61 1 32 1 31 1 62 1 6Bài 16. Đếm số nhỏ hơn x trong đoạn
Phần tiêu đề “Bài 16. Đếm số nhỏ hơn x trong đoạn”Dòng đầu có n, q (1 ≤ n, q ≤ 105), dòng thứ hai có n số nguyên (|ai| ≤ 109), q dòng sau mỗi dòng có l, r, x. Với mỗi câu hỏi, in ra số phần tử ai với l ≤ i ≤ r và ai nhỏ hơn x. Phải trả lời theo đúng thứ tự (coi như không biết trước các câu hỏi).
Ví dụ:
| Nhập | In ra |
|---|---|
6 35 1 4 2 8 31 6 42 4 35 6 1 | 320 |
Kiểm tra: nhập
8 47 -2 7 0 9 3 3 -51 8 32 5 86 7 41 1 7