Leftist Tree
Cây lệch trái là gì?
Cây lệch trái (Leftist Tree), giống như Pairing Heap, là một loại Heap hợp nhất được (Mergeable Heap), có các tính chất của heap và hỗ trợ thao tác hợp nhất nhanh chóng.
Định nghĩa và tính chất của Cây lệch trái
Đối với một cây nhị phân, ta định nghĩa nút ngoài (external node) là nút có ít hơn hai con. Định nghĩa \(\mathrm{dist}\) của một nút là số lượng cạnh đi qua trên đường đi ngắn nhất từ nút đó đến một nút ngoài trong cây con của nó. \(\mathrm{dist}\) của nút rỗng (null node) là \(0\).
Lưu ý
Một số tài liệu định nghĩa \(\mathrm{dist}\) nhỏ hơn định nghĩa trong bài viết này \(1\) đơn vị. Định nghĩa như vậy giúp việc viết code bỏ qua được một số bước kiểm tra rỗng, nhưng cần lưu ý phải khởi tạo \(\mathrm{dist}\) của nút rỗng là \(-1\). Tất cả code trong bài viết này đều sử dụng định nghĩa \(\mathrm{dist}\) của nút rỗng là \(-1\), hãy chú ý sự khác biệt này so với định nghĩa trong văn bản.
Cây lệch trái là một cây nhị phân, không chỉ có tính chất của heap mà còn có tính chất "lệch trái": với mọi nút, \(\mathrm{dist}\) của con trái luôn lớn hơn hoặc bằng \(\mathrm{dist}\) của con phải.
Do đó, \(\mathrm{dist}\) của mỗi nút trong Cây lệch trái luôn bằng \(\mathrm{dist}\) của con phải cộng thêm \(1\).
Cần lưu ý rằng, \(\mathrm{dist}\) không phải là độ sâu, độ sâu của Cây lệch trái không được đảm bảo, một chuỗi các nút chỉ có con trái (dạng đường thẳng) cũng thỏa mãn định nghĩa của Cây lệch trái.
Thao tác cốt lõi: Hợp nhất (merge)
Khi hợp nhất hai heap, để thỏa mãn tính chất heap, trước tiên ta chọn gốc có giá trị nhỏ hơn (để thuận tiện, bài viết này xét min-heap) làm gốc của heap sau khi hợp nhất. Sau đó, giữ nguyên con trái của gốc này, và đệ quy hợp nhất con phải của nó với heap còn lại để làm con phải mới. Để thỏa mãn tính chất lệch trái, sau khi hợp nhất, nếu \(\mathrm{dist}\) của con trái nhỏ hơn \(\mathrm{dist}\) của con phải, ta đổi chỗ hai con này.
Code tham khảo:
Hiện thực
1 2 3 4 5 6 7 8 9 | |
Do tính chất lệch trái, mỗi khi đệ quy xuống một tầng, \(\mathrm{dist}\) của gốc một trong hai heap sẽ giảm đi \(1\). Mà một cây nhị phân có \(n\) nút thì \(\mathrm{dist}\) của gốc không vượt quá \(\left\lceil\log (n+1)\right\rceil\), nên độ phức tạp để hợp nhất hai heap có kích thước lần lượt là \(n\) và \(m\) là \(O(\log n+\log m)\).
Chứng minh về tính chất của \(\mathrm{dist}\)
Một cây nhị phân có gốc với \(\mathrm{dist}\) bằng \(x\) sẽ có ít nhất \(x-1\) tầng là cây nhị phân đầy đủ (full binary tree), do đó nó có ít nhất \(2^x-1\) nút. Lưu ý rằng tính chất này đúng với mọi cây nhị phân, không chỉ riêng Cây lệch trái.
Cây lệch trái còn có một cách cài đặt không cần đổi chỗ hai con: coi con có \(\mathrm{dist}\) lớn hơn là con trái, con có \(\mathrm{dist}\) nhỏ hơn là con phải:
Hiện thực
1 2 3 4 5 6 7 8 9 10 | |
Các thao tác khác trên Cây lệch trái
Chèn nút
Một nút đơn lẻ cũng có thể được xem là một heap, ta chỉ cần hợp nhất nó vào heap chính.
Xóa gốc
Hợp nhất con trái và con phải của gốc.
Xóa một nút bất kỳ
Cách làm
Đầu tiên hợp nhất con trái và con phải của nút cần xóa, sau đó cập nhật \(\mathrm{dist}\) từ dưới lên trên (pushup). Nếu không thỏa mãn tính chất lệch trái thì đổi chỗ hai con. Khi \(\mathrm{dist}\) không thay đổi nữa thì dừng đệ quy:
Hiện thực
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | |
Chứng minh độ phức tạp
Đầu tiên xét quá trình merge, mỗi bước đều đi xuống một tầng của \(x\) hoặc \(y\). Trường hợp tệ nhất là luôn chọn đi về phía con phải của Cây lệch trái (nút có \(\mathrm{dist}\) nhỏ nhất), khi đó \(\mathrm{dist}\) giảm đi \(1\).
Tiếp theo xét quá trình pushup. Gọi nút hiện tại đang pushup là \(x\), cha của nó là \(y\). "\(\mathrm{dist}\) ban đầu" của một nút là \(\mathrm{dist}\) của nó trước khi thực hiện pushup. Bắt đầu đệ quy từ cha của nút bị xóa, có hai trường hợp:
- \(x\) là con phải của \(y\). Khi đó \(\mathrm{dist}\) ban đầu của \(y\) bằng \(\mathrm{dist}\) ban đầu của \(x\) cộng \(1\).
- \(x\) là con trái của \(y\). Do \(\mathrm{dist}\) của một nút chỉ giảm tối đa \(1\), nên đệ quy chỉ tiếp tục khi \(\mathrm{dist}\) ban đầu của con trái và con phải của \(y\) bằng nhau (khi đó việc con trái giảm \(\mathrm{dist}\) sẽ dẫn đến hoán đổi hai con hoặc cập nhật lại \(\mathrm{dist}\) của \(y\)). Do đó, \(\mathrm{dist}\) ban đầu của \(y\) vẫn bằng \(\mathrm{dist}\) ban đầu của \(x\) cộng \(1\).
Như vậy, mỗi khi đệ quy lên một tầng, \(\mathrm{dist}\) ban đầu của \(x\) sẽ tăng thêm \(1\). Do đó số tầng đệ quy tối đa là \(O(\log n)\).
Cộng/Trừ một giá trị, Nhân một số dương cho cả heap
Thực tế, mọi thao tác có thể dùng lazy tag (đánh dấu) và không làm thay đổi thứ tự tương đối giữa các phần tử đều có thể thực hiện được.
Ta đánh dấu (tag) tại gốc, và đẩy nhãn (pushdown) khi xóa gốc hoặc hợp nhất (truy cập vào con):
Hiện thực
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Các loại Heap hợp nhất được khác
Heap ngẫu nhiên (Randomized Heap)
Hiện thực
1 2 3 4 5 6 7 8 | |
Có thể thấy điểm khác biệt duy nhất của phương pháp này là sử dụng số ngẫu nhiên để quyết định việc hợp nhất, nhờ đó có thể bỏ qua việc tính toán \(\mathrm{dist}\). Độ phức tạp thời gian trung bình vẫn là \(O(\log n)\). Chứng minh chi tiết có thể tham khảo Randomized Heap.
Heap nghiêng (Skew Heap)
Skew Heap là dạng tự điều chỉnh của Cây lệch trái. Khi hợp nhất hai heap, nó vô điều kiện đổi chỗ tất cả các nút trên đường đi hợp nhất, nhằm mục đích duy trì sự cân bằng. Theo phân tích khấu hao (amortized analysis), độ phức tạp của các thao tác chèn, hợp nhất, xóa giá trị nhỏ nhất trên Skew Heap (top-down) là \(O(\log n)\)1.
Bài tập ví dụ
Bài tập mẫu
luogu P3377【Template】Leftist Tree (Mergeable Heap)
Cần lưu ý:
-
Trước khi hợp nhất phải kiểm tra xem hai nút đã nằm trong cùng một heap chưa.
-
Độ sâu của Cây lệch trái có thể lên tới \(O(n)\), do đó để tìm gốc của heap chứa một nút, ta cần dùng DSU (Cấu trúc dữ liệu các tập hợp không giao nhau), không thể nhảy cha (brute force) trực tiếp. (Mặc dù nhiều bài có dữ liệu yếu nên nhảy cha vẫn qua được...) (Khi dùng DSU để bảo trì gốc, cần đảm bảo gốc cũ trỏ vào gốc mới, và gốc mới trỏ vào chính nó.)
Code tham khảo bài Roman Game
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | |
Bài toán trên cây
Loại bài này thường yêu cầu mỗi nút bảo trì một heap, hợp nhất với heap của các con, sau đó thực hiện pop, sửa đổi, tính toán đáp án theo đề bài, hơi giống với các bài dùng Segment Tree Merge (Hợp nhất cây phân đoạn).
Code tham khảo bài City Capture
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | |
「SCOI2011」Tricky Operations
Đầu tiên, việc tìm gốc của heap chứa một nút phải dùng DSU, không thể nhảy cha trực tiếp.
Tiếp theo xét thao tác truy vấn đơn điểm. Nếu dùng cách đánh dấu (lazy tag) thông thường, ta phải tính tổng các tag trên đường đi từ nút đó đến gốc, trong trường hợp xấu nhất độ phức tạp có thể lên tới \(O(n)\). Nếu chỉ có tag ở gốc heap thì có thể truy vấn nhanh, nhưng làm sao để thực hiện điều này?
Ta có thể dùng phương pháp tương tự như hợp nhất theo quy tắc nhỏ-gộp-lớn (small-to-large merging). Mỗi khi hợp nhất, ta đẩy tag (pushdown) một cách thủ công (brute force) xuống từng nút của heap nhỏ hơn, sau đó lấy tag của heap lớn hơn làm tag chung cho heap sau khi hợp nhất. Vì heap sau khi hợp nhất có chứa tag của heap lớn, nên khi đẩy tag cho heap nhỏ, ta phải trừ đi tag của heap lớn. Do mỗi lần hợp nhất, kích thước của heap chứa một nút bất kỳ sẽ tăng ít nhất gấp đôi, nên mỗi nút chỉ bị đẩy tag thủ công tối đa \(O(\log n)\) lần. Tổng độ phức tạp của việc đẩy tag thủ công là \(O(n\log n)\).
Đối với thao tác cộng đơn điểm: xóa nút, cập nhật giá trị, sau đó chèn lại.
Cuối cùng là thao tác tìm giá trị lớn nhất toàn cục. Ta có thể dùng một cây cân bằng / heap hỗ trợ xóa nút bất kỳ (như Cây lệch trái) / multiset để bảo trì giá trị lớn nhất của các heap (tức là giá trị tại gốc của mỗi heap).
Tóm lại, các thao tác được xử lý như sau:
- Đẩy tag thủ công cho heap nhỏ hơn, hợp nhất hai heap, cập nhật size, tag. Trong
multiset, xóa gốc cũ không còn là gốc sau khi hợp nhất. - Xóa nút, cập nhật giá trị, chèn lại, cập nhật
multiset. Cần phân trường hợp nếu nút bị xóa là gốc. - Đánh tag tại gốc heap, cập nhật
multiset. - Đánh tag toàn cục.
- Truy vấn giá trị + tag tại gốc heap + tag toàn cục.
- Truy vấn giá trị tại gốc + tag tại gốc heap + tag toàn cục.
- Truy vấn giá trị lớn nhất trong
multiset+ tag toàn cục.
Code tham khảo bài Tricky Operations
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | |
「BOI2004」Sequence
Đây là một bài toán từ bài báo khoa học, chi tiết xem tại 《Hoàng Nguyên Hà -- Đặc điểm và ứng dụng của Cây lệch trái》.
Tài liệu tham khảo
Last updated on this page:, Update history
Found an error? Want to help improve? Edit this page on GitHub!
Contributors to this page:JiZiQian, llleixx, firefly-zjyjoe
All content on this page is provided under the terms of the CC BY-SA 4.0 and SATA license, additional terms may apply