Інтуїтивно можна думати про двійкове індексоване дерево як стиснене зображення бінарного дерева, яке саме по собі є оптимізацією стандартного представлення масиву. Ця відповідь переходить до одного можливого виведення.
Припустимо, наприклад, що ви хочете зберігати кумулятивні частоти в цілому 7 різних елементів. Ви можете почати, виписавши сім відер, в які будуть розподілені цифри:
[ ] [ ] [ ] [ ] [ ] [ ] [ ]
1 2 3 4 5 6 7
Тепер припустимо, що кумулятивні частоти виглядають приблизно так:
[ 5 ] [ 6 ] [14 ] [25 ] [77 ] [105] [105]
1 2 3 4 5 6 7
Використовуючи цю версію масиву, ви можете збільшити кумулятивну частоту будь-якого елемента, збільшивши значення числа, збереженого на цьому місці, а потім збільшивши частоти всього, що приходить згодом. Наприклад, щоб збільшити кумулятивну частоту 3 на 7, ми могли б додати 7 до кожного елемента в масиві в позиції 3 або після нього, як показано тут:
[ 5 ] [ 6 ] [21 ] [32 ] [84 ] [112] [112]
1 2 3 4 5 6 7
Проблема в цьому полягає в тому, що для цього потрібно O (n) час, який досить повільний, якщо n великий.
Один із способів, що ми можемо подумати над покращенням цієї операції, - це змінити те, що ми зберігаємо у відрах. Замість того, щоб зберігати сукупну частоту до заданої точки, ви можете замість цього просто думати про збереження суми, що поточна частота зросла відносно попереднього відра. Наприклад, у нашому випадку ми б переписали вищезазначені відра таким чином:
Before:
[ 5 ] [ 6 ] [21 ] [32 ] [84 ] [112] [112]
1 2 3 4 5 6 7
After:
[ +5] [ +1] [+15] [+11] [+52] [+28] [ +0]
1 2 3 4 5 6 7
Тепер ми можемо збільшити частоту у відрі за час O (1), просто додавши до цього відра відповідну кількість. Однак загальна вартість здійснення пошуку тепер стає O (n), оскільки нам доведеться перерахувати загальну суму у відрі шляхом підсумовування значень у всіх менших відрах.
Перше основне розуміння, яке нам потрібно дістати звідси до двійкового індексованого дерева, полягає в наступному: замість того, щоб постійно перераховувати суму елементів масиву, які передують конкретному елементу, що робити, якби ми попередньо обчислили загальну суму всіх елементів перед конкретними крапки в послідовності? Якби ми могли це зробити, то ми могли б визначити сукупну суму в точці, просто підсумувавши правильну комбінацію цих попередньо обчислених сум.
Один із способів зробити це - змінити представлення з масиву відро до бінарного дерева вузлів. Кожен вузол буде анотований із значенням, яке представляє сукупну суму всіх вузлів зліва від даного вузла. Наприклад, припустимо, що ми побудуємо з цих вузлів таке бінарне дерево:
4
/ \
2 6
/ \ / \
1 3 5 7
Тепер ми можемо збільшити кожен вузол, зберігаючи сукупну суму всіх значень, включаючи цей вузол та його ліве піддерево. Наприклад, враховуючи наші значення, ми зберігаємо наступне:
Before:
[ +5] [ +1] [+15] [+11] [+52] [+28] [ +0]
1 2 3 4 5 6 7
After:
4
[+32]
/ \
2 6
[ +6] [+80]
/ \ / \
1 3 5 7
[ +5] [+15] [+52] [ +0]
Враховуючи цю структуру дерева, визначити сукупну суму до очка легко. Ідея така: ми підтримуємо лічильник, спочатку 0, потім робимо звичайний бінарний пошук вгору, поки не знайдемо відповідний вузол. Так само ми робимо наступне: щоразу, коли рухаємось правильно, ми також додаємо до лічильника поточне значення.
Наприклад, припустимо, що ми хочемо шукати суму за 3. Для цього робимо наступне:
- Почніть з кореня (4). Лічильник 0.
- Перехід ліворуч до вузла (2). Лічильник 0.
- Переходьте праворуч до вузла (3). Лічильник 0 + 6 = 6.
- Знайдіть вузол (3). Лічильник 6 + 15 = 21.
Ви можете собі уявити, що цей процес також працює в зворотному порядку: починаючи з заданого вузла, ініціалізуйте лічильник до значення цього вузла, а потім підняти дерево до кореня. Кожен раз, коли ви переходите на правильне дочірнє посилання вгору, додайте значення у вузлі, до якого ви приїжджаєте. Наприклад, щоб знайти частоту для 3, ми могли б зробити наступне:
- Почніть з вузла (3). Лічильник 15.
- Перехід вгору до вузла (2). Лічильник 15 + 6 = 21.
- Перехід вгору до вузла (4). Лічильник 21.
Щоб збільшити частоту вузла (і, мається на увазі, частоту всіх вузлів, які приходять після нього), нам потрібно оновити набір дерев, які включають цей вузол у його лівому піддіреві. Для цього робимо наступне: збільшуємо частоту для цього вузла, а потім починаємо підходити до кореня дерева. Кожен раз, коли ви переходите за посиланням, яке сприймає вас як дочірню дитину, збільшуйте частоту вузла, з яким ви стикаєтесь, додаючи поточне значення.
Наприклад, щоб збільшити частоту вузла 1 на п'ять, ми зробимо наступне:
4
[+32]
/ \
2 6
[ +6] [+80]
/ \ / \
> 1 3 5 7
[ +5] [+15] [+52] [ +0]
Починаючи з вузла 1, збільшуйте його частоту на 5, щоб отримати
4
[+32]
/ \
2 6
[ +6] [+80]
/ \ / \
> 1 3 5 7
[+10] [+15] [+52] [ +0]
Тепер перейдіть до його батька:
4
[+32]
/ \
> 2 6
[ +6] [+80]
/ \ / \
1 3 5 7
[+10] [+15] [+52] [ +0]
Ми йшли лівою дочірньою лінією вгору, тому збільшуємо також частоту цього вузла:
4
[+32]
/ \
> 2 6
[+11] [+80]
/ \ / \
1 3 5 7
[+10] [+15] [+52] [ +0]
Тепер ми переходимо до його батьків:
> 4
[+32]
/ \
2 6
[+11] [+80]
/ \ / \
1 3 5 7
[+10] [+15] [+52] [ +0]
Це було ліве дочірнє посилання, тому ми також збільшуємо цей вузол:
4
[+37]
/ \
2 6
[+11] [+80]
/ \ / \
1 3 5 7
[+10] [+15] [+52] [ +0]
І ось ми закінчили!
Останнім кроком є перетворення від цього до бінарного індексованого дерева, і саме тут ми отримуємо цікаві речі з двійковими числами. Давайте перепишемо кожен індекс відра у цьому дереві у двійковій формі:
100
[+37]
/ \
010 110
[+11] [+80]
/ \ / \
001 011 101 111
[+10] [+15] [+52] [ +0]
Тут ми можемо зробити дуже-дуже класне спостереження. Візьміть будь-яке з цих двійкових чисел і знайдіть останнє 1, яке було встановлено в номері, а потім відкиньте цей біт разом з усіма бітами, що надходять після нього. Тепер вам залишилось:
(empty)
[+37]
/ \
0 1
[+11] [+80]
/ \ / \
00 01 10 11
[+10] [+15] [+52] [ +0]
Ось справді дуже цікаве спостереження: якщо ви вважаєте, що 0 означає «ліворуч», а 1 - «праворуч», решта бітів на кожному номері чітко визначають, як починати з кореня, а потім переходити до цього числа. Наприклад, вузол 5 має двійковий візерунок 101. Останній 1 - це заключний біт, тому ми відкидаємо його, щоб отримати 10. Дійсно, якщо ви починаєте в корені, ідіть праворуч (1), потім переходите вліво (0), ви закінчуєте вгору у вузлі 5!
Причина, що це важливо, полягає в тому, що наші операції пошуку та оновлення залежать від шляху доступу від вузла назад до кореня та від того, чи слідкуємо ми за лівими чи правими посиланнями дитини. Наприклад, під час пошуку ми просто дбаємо про правильні посилання, які ми переходимо. Під час оновлення ми просто дбаємо про ліві посилання, які ми переходимо. Це двійкове індексоване дерево робить все це дуже ефективно, просто використовуючи біти в індексі.
Основна хитрість полягає в наступній властивості цього ідеального бінарного дерева:
З огляду на вузол n, наступний вузол на шляху доступу назад до кореня, в якому ми йдемо праворуч, задається, взявши двійкове представлення n та видаливши останній 1.
Наприклад, погляньте на шлях доступу до вузла 7, який становить 111. Вузли на шляху доступу до кореня, який ми беремо, що включають наступний правий вказівник вгору, це
- Вузол 7: 111
- Вузол 6: 110
- Вузол 4: 100
Все це - правильні посилання. Якщо ми візьмемо шлях доступу до вузла 3, який є 011, і подивимось на вузли, куди ми йдемо правильно, ми отримаємо
- Вузол 3: 011
- Вузол 2: 010
- (Вузол 4: 100, за яким слід ліве посилання)
Це означає, що ми можемо дуже, дуже ефективно обчислити сукупну суму до вузла наступним чином:
- Випишіть вузол n у двійковій формі.
- Встановіть лічильник на 0.
- Повторіть наступне, коли n ≠ 0:
- Додайте значення у вузлі n.
- Очистіть крайній правий біт від n.
Аналогічно давайте подумаємо про те, як би ми зробили крок оновлення. Для цього ми хотіли б слідувати шлях доступу до кореня, оновлюючи всі вузли, де ми йшли лівою ланкою вгору. Ми можемо це зробити, по суті, виконавши вищевказаний алгоритм, але переключивши всі 1 на 0 і 0 на 1.
Останнім кроком у двійковому індексованому дереві є зауваження, що через цю побітну хитрість нам навіть більше не потрібно зберігати дерево. Ми можемо просто зберегти всі вузли в масиві довжиною n, а потім використовувати побітові методи перекручування для навігації по дереву неявно. Насправді це саме те, що робить побитове індексоване дерево - воно зберігає вузли в масиві, а потім використовує ці побізні трюки, щоб ефективно імітувати ходьбу вгору по цьому дереву.
Сподіваюся, це допомагає!