Чому неадекватні підходи зазнають невдачі
Розглянемо бітову масу довжиною n та дві операції, які нас цікавлять. По-перше, порахуйте кількість одиниць, що зустрічаються серед позицій від 0 до i, яка називається rank(i). По-друге, знайдіть позицію k-тої одиничної біти, яка називається select(k). Неадекватний підхід до обчислення rank сканує від позиції нуля та підраховує встановлені біти один за одним, що займає час пропорційний i в найгіршому випадку; неадекватний підхід до вибору також виконує такий же лінійний пошук, що займає час пропорційний n. Для окремого запиту це нормально, але багато застосувань генерують мільйони таких запитів проти однієї й тієї ж фіксованої бітової маси, тому відповіді лінійного часу стають вузьким місцем. Очевидне рішення – попередньо обчислювати та зберігати відповідь для rank у кожній окремій позиції, створюючи таблицю пошуку такої ж довжини, як і оригінальна бітова маса, але з записами достатнього розміру для підрахунку до n. Ця таблиця зазвичай потребує набагато більше місця, ніж самі біти, оскільки кожен збережений лічильник вимагає повного обсягу бітів, тоді як основні дані займали лише один біт на позицію. Це нівелює всю мету використання компактної бітової маси, яка часто обирається спеціально тому, щоб стиснути інформацію в кількість бітів, близьку до теоретичного мінімуму. Потрібен золотий компроміс: допоміжну структуру, яка значно менша за повну таблицю попереднього обчислення у всіх позиціях, але все ще дозволяє кожному запиту відповідати без перегляду маси з початку. Це саме те, що робить структуру лаконічною, а не просто компактною: вона використовує простір, близький до інформаційної межі нижньої межі для представлення даних, плюс додатковий індекс, розмір якого асимптотично незначний у порівнянні, і все ще відповідає на запити за константною швидкістю. Суперблок та схема блоків досягають цього балансу і є класичним початком, викладеним у кожному курсі лаконічних структур даних перед переходом до більш складних конструкцій.
Дворівневий Блок і Індекс Блоків
Класична конструкція розділяє бітовий вектор на великі, рівномірно розміщені частини, які називаються суперблоками, а кожен суперблок, в свою чергу, ділиться на менші частини, що називаються блоками. На межі кожного суперблока структура зберігає одне число: загальну кількість встановлених бітів у всіх позиціях строго перед цим суперблоком, тобто поточний підсумок від початку масиву. На межі кожного блоку зберігається друге, менше число: кількість встановлених бітів всередині поточного суперблока, від початку цього суперблока до початку поточного блоку. Це двошарове розділення є ключем до підтримки індексу невеликим, але при цьому швидким. Лічильники на рівні суперблоків повинні мати достатньо біт для представлення рахунків до n, тому їх небагато (приблизно n поділене на розмір суперблока), і кожен з них є відносно широким цілим числом. Лічильники на рівні блоків, навпаки, ніколи не потребують підрахунку більше, ніж розмір суперблока, який обирається як малий, тому лічильник кожного блоку можна зберігати за допомогою значно меншої кількості біт, хоча блоків більше, ніж суперблоків. Вибір розміру суперблока близько до квадрата розміру блоку або, точніше, зв’язування обох з логарифмічними функціями від n, робить зменшення загального розміру індексу до мізерної частини початкових n бітів, коли n зростає. Після консультації з обома збереженими лічильниками залишається невеликий залишковий проміжок: кількість позицій бітів між початком відповідного блоку та фактичною позицією запиту i. Цей проміжок обмежений розміром блоку, який свідомо тримається малим, зазвичай на порядку розміру машинного слова або менше. Підрахунок встановлених біт у цьому короткому залишку є остаточною частиною головоломки, і вона обробляється швидким кроком постійного часу popcount, описаним у наступному розділі, а не будь-якою подальшою попередньо обчисленою ходою масиву.
Постійна часова оцінка кількості встановлених бітів на залишку
Після додавання збереженого загального значення суперблоку та збереженого часткового значення блоку, залишається порахувати кількість встановлених біт у короткому залишку, не більше ніж один блок, що закінчується точно в запитаному положенні i. Цей остаточний підрахунок також повинен бути постійною часом, інакше вся схема все ще буде лінійною в найгіршому випадку для цього останнього відрізка. Двоє стандартних методів досягають цього. Перший спирається на інструкцію повної оцінки (popcount) апаратного забезпечення, доступну на практично всіх сучасних процесорах, яка підраховує кількість встановлених біт у машинному слові в одній інструкції. Якщо розмір блоку обрано таким чином, щоб він поміщався в один або два машинні слова, маскування бітів за межами позиції i та видання однієї або двох інструкцій повної оцінки миттєво завершують роботу, без будь-якого доступу до пам'яті за межами самого слова. Другий метод, який корисний, коли інструкція апаратного забезпечення недоступна або при роботі з програмною моделлю структури, попередньо обчислює невелику таблицю пошуку, індексовану кожним можливим шаблоном біт-ширини обраної підблокової ширини, зазвичай восьми або шістнадцять бітів, і зберігає кількість встановлених біт для кожного шаблону. Оскільки кількість різних шаблонів шириною w дорівнює 2 в степені w, ця таблиця мізерна, наприклад, таблиця для восьмибітових шаблонів має лише 256 записів. Пошук кількості встановлених біт у короткому залишку стає одним або декількома читаннями таблиці плюс додавання, все ще постійна часом і незначний з точки зору простору. Об'єднуючи ці три частини - загальну суму суперблоку, часткову суму блоку та швидку повну оцінку кінцевого короткого залишку, отримуємо відповідь на rank(i) за допомогою фіксованої, невеликої кількості арифметичних операцій незалежно від того, наскільки великий є загальний бітовий вектор. Select(k) відповідає аналогічній структурі, зазвичай використовуючи той самий суперблок і межі блоків разом з або бінарним пошуком збережених рахунків, або паралельною структурою семплювання, яка записує позицію кожного m-го встановленого біта безпосередньо, дозволяючи пошуку звузитися до невеликого діапазону перед лінійним або табличним сканом, який точно визначає точне положення.
Точне вимірювання просторового заповнення
Слово «succinct» має конкретне технічне значення: структура даних є лаконічною, коли її загальне використання простору дорівнює мінімальному інформаційному об’єму, необхідному для представлення об’єкта, плюс нижній порядок член, який стає незначним відносно цього мінімуму зі збільшенням вхідних даних. Для будь-якого бітового вектора довжиною n без особливої структури, мінімальний інформаційний об’єм, необхідний для його зберігання, становить просто n біт, оскільки існує 2 в степені n можливих бітових векторів цієї довжини і кожен з них потребує окремого коду. Суперблок та індекс, описані тут, додають приблизно n поділених на log n біт поверх цих оригінальних n біт, використовуючи типові параметри, де розмір блоку пропорційний log n, а розмір суперблока пропорційний квадрату log n. Зі збільшенням n, співвідношення розміру індексу до обсягу даних наближається до нуля, що є визначальною властивістю лаконічної допоміжної структури: це асимптотично незначний оверхед в обмін на константний час ранжування та вибору. Це контрастує з двома іншими поширеними категоріями. Імплицитна структура використовує точно n біт із жодним допоміжним оверхедом, але ранжування та вибір зазвичай потребують лінійного сканування, оскільки немає місця для зберігання будь-яких попередньо обчислених підказок. Протилежна лаконічна або проста компактна структура, з іншого боку, може зберігати повний допоміжний масив з одним записом на кожну позицію біта, використовуючи оверхед того ж асимптотичного порядку, що й дані самі по собі, часто кілька разів n біт, що унеможливлює мету вибору компактного представлення. Схема суперблоку та індексу займає золоту середину між цими екстремумами: близька до імплицитності в просторі, близька до повністю попередньо обчисленої таблиці щодо швидкості запитів. Реальні реалізації налаштовують точні розміри суперблоків і блоків на основі розмірів кешу та ширини слова, торгуючи невеликим постійним множником оверхеду для кращої практичної продуктивності, зберігаючи при цьому асимптотичну гарантію лаконічності.
Фундамент під хвильовими деревами та стислими спробами
Ранжування та вибір на основі бітового вектора можуть виглядати як вузький, спеціалізований інструмент, але насправді це первинна структура, що лежить в основі великої родини більш складних стислих структур. Хвильове дерево, яке використовується для представлення послідовності символів у стислому вигляді, зберігаючи при цьому можливість ранжування, вибору та доступу до будь-яких символів, працює шляхом рекурсивного розділення алфавіту навпіл і записує на кожному рівні бінарного дерева, в який півколор потрапили кожен символ у вигляді бітового вектора. Будь-яке запитання щодо хвильового дерева як цілого розкладається на послідовність запитів ранжування або вибору до цих бітових векторів рівня, тому загальний час виконання запиту хвильового дерева повністю залежить від швидкості ранжування та вибору в цих бітових векторах. Якщо зробити швидкість ранжування/вибору кожного бітового вектора постійною, то час виконання всіх запитів у дереві буде логарифмічним відносно розміру алфавіту, без будь-якого лінійного сканування ніде. Стислі представлення дерев та трієв розповідають подібну історію. Поширений метод кодує форму дерева за допомогою послідовності збалансованих дужок або подібного бітного кодування, де операції, такі як пошук батьківського вузла, його дітей або розміру піддерева, виражаються як запити ранжування та вибору, іноді разом із пов'язаною операцією для пошуку відповідних позицій дужок. Стислі трієві структури, побудовані на основі цих кодів дерев, які використовуються в таких застосунках, як стиснені словники рядків та індекси геномних даних, успадковують свою швидкість запитів безпосередньо від продуктивності ранжування/вибору базового бітового вектора. Оскільки так багато вищих рівнів стислих структур зменшують свої основні операції до ранжування та вибору на бітовому векторі, оптимізація цієї однієї фундаментальної структури, вибір хороших суперблоків і розмірів блоків, вибір ефективної стратегії підрахунку кількості одиниць, має значний вплив на всю галузь. У цьому симуляторі лабораторії ізолює цей базовий шар, щоб можна було безпосередньо перевіряти його поведінку, перш ніж він зникне в рекурсивній машині хвильового дерева або стислого трієвого дерева.
Часті запитання
Чому не просто попередньо обчислювати ранг на кожній позиції біту?
Зберігання повної пробільної суми рахунку на кожній позиції використало б значно більше місця, ніж оригінальний бітове вектор, оскільки кожен збережений лічильник потребував би достатньо бітів для представлення значень до n, тоді як підлеглий об’єкт використовував лише один біт на позицію. Це суперечить меті вибору компактного бітового вектора в першу чергу. Суперблок і індекс блоку зберігають менше, добре обраних попередньо обчислених лічильників, зберігаючи додане місце близько до інформаційно-теоретичного мінімуму, але все ще відповідаючи на будь-яке запитання за постійний час.
Яка різниця між суперблоком і блоком?
Суперблок — це великий шматок бітового вектора, а його межа зберігає загальну пробільну суму з самого початку масиву. Блок — це менший шматок, занурений у суперблок, і його межа зберігає лише кількість встановлених бітів з початку свого власного суперблоку. Об’єднання широкого загального суперблоку із локальною частковою сумою блоку дозволяє запитанню майже повністю пропустити масив, використовуючи набагато менше місця, ніж один лічильник на позицію.
Як працює select(k), якщо лише ранг попередньо обчислено?
Select зазвичай відповідає на запит, спочатку звужуючи діапазон суперблоків і блоків, які містять k-те встановлене бітове значення, використовуючи або бінарний пошук по збережених лічильниках суперблоків і блоків, або окрему структуру зразків, яка безпосередньо записує положення кожного m-го встановленого біта. Після того, як пошук звужується до невеликого діапазону, який не перевищує блок, швидкий перегляд або табличний пошук у цьому короткому діапазоні визначає точне положення, зберігаючи при цьому всю операцію за постійний час.
Що таке «succinct» насправді означає в цьому контексті?
Структура вважається лаконічною, коли її загальний обсяг є мінімальним теоретично необхідним для представлення даних, плюс додатковий член, який стає незначним пропорційно до росту даних. Для бітового вектора довжиною n мінімум становить n біт, а індекс суперблоку/блоку зазвичай додає лише порядок n поділеного на log n бітів, відношення, яке зменшується до нуля для великих n, при цьому все ще відповідаючи на ранг і select за постійний час.
Чому дерева вейвлетів і лаконічні спроби залежать від цієї структури?
Обидві структури розкладають свої вищі рівневі запити, такі як пошук k-го вoccurrence символу або навігація до батьківського вузла, на послідовність викликів rank і select проти внутрішніх бітових векторів. Якщо ці бітові вектори відповідають на rank і select за постійний час, вся вища рівнева структура успадковує швидкий, часто логарифмічний загальний час запиту. Таким чином, бітовий вектор суперблоку/блоку є основним будівельним блоком, з якого будуються ці більш складні лаконічні структури.
Спробуйте наживо
Усе, що вище, працює прямо у вашому браузері — відкрийте Succinct Rank/Select Bitvector і змінюйте параметри під час роботи. Нічого не встановлюється, нічого не завантажується на сервер, уся модель живе в одній вкладці.
▶ Відкрити симуляцію Succinct Rank/Select Bitvector