Нотатка · Алгоритми
Дерево Фенвіка: що робить i & -i
У дереві Фенвіка кожна позиція зберігає суму блоку. Розмір цього блоку можна отримати так:
i & -iПершого разу цей вираз виглядає як чаклунство. Насправді він просто залишає наймолодший установлений біт числа — крайню праву одиницю в його двійковому записі.
Розберемо число 6:
6 = 00000110Щоб отримати -6, комп’ютер інвертує біти й додає одиницю:
00000110
↓ інвертуємо
11111001
↓ додаємо 1
11111010Тепер виконуємо побітове AND:
00000110
& 11111010
----------
00000010Отримали 2.
Отже:
6 & -6 = 2Саме два елементи входять до блоку, який закінчується на шостій позиції:
[5–6]Інший приклад — позиція 4:
4 = 00000100
4 & -4 = 4Тому четверта комірка зберігає блок із чотирьох чисел:
[1–4]Для позиції 7 результат дорівнює одиниці:
7 = 00000111
7 & -7 = 1Отже, сьома комірка відповідає лише за себе:
[7]Тепер можна знайти суму перших семи чисел.
Починаємо з позиції 7. Її блок має розмір 1, тому додаємо tree[7] і переходимо до позиції 6:
7 - 1 = 6Шоста позиція зберігає блок із двох чисел. Додаємо tree[6] і переходимо до четвертої:
6 - 2 = 4Четверта зберігає чотири числа:
4 - 4 = 0Дійшли до нуля — сума готова:
tree[7] + tree[6] + tree[4]У коді це виглядає так:
def prefix_sum(i):
result = 0
while i > 0:
result += tree[i]
i -= i & -i
return resultПід час оновлення рухаємося в інший бік. Якщо змінилася третя позиція, потрібно оновити всі більші блоки, які її містять:
3 → 4 → 8Для цього розмір блоку не віднімаємо, а додаємо:
def add(i, value):
while i <= n:
tree[i] += value
i += i & -iЗамість проходу по всьому масиву кожна операція відвідує лише кілька комірок. Для мільйона елементів це приблизно двадцять переходів.
Концепція справді проста: зберігаємо суми правильно підібраних блоків і пересуваємося між ними. Але щоб побачити цю простоту за двійковими числами та i & -i, довелося добряче посидіти.