XOR базис з видаленнями
Передмова
Ця стаття використовує терміни лінійної алгебри. Якщо ви їх розумієте, можете пропустити цей розділ.
Вступ
Згадаємо таку відому задачу:
Детальніше про те як розв'язувати цю задачу можна дізнатися за посиланням.
В цій статті будемо розглядати більш складну задачу, де ще є запити видалення числа з множини . Крім того, ми отримуємо наступний запит, тільки після того, як відповіли на попередній (тобто задачу треба розв'язувати "в онлайні").
Початкову задачу можна розв'язувати за на запит, а от задачу з видаленнями будемо навчатися розв'язувати за O(m^2).
Структура
У задачі з видаленнями в нас починаються невизначеності на моменті, коли число, що ми додаємо до XOR базису представляється -ами наявних чисел.
Ми хочемо ефективно зберегти кожне число, що додається до , тому будемо підтримувати послідовність X_n (n \ge 1) XOR базисів (кожен з яких є початково порожнім), замість одного базису.
Додавання числа до
Розглянемо як саме будемо додавати нове число v \neq 0 до множини :
Будемо поступово обходити по нашим базисами починаючи з першого. Нехай — номер поточного базису, який розглядаємо;
Якщо v \notin \operatorname{span}(X_i), то просто додамо до X_i і закінчимо процедуру;
Інакше, v \in \operatorname{span}(X_i), то так додати не виходить, але ми прагнемо обов'язково десь зберегти це число. Тому збільшуємо на та повертаємося до кроку .
Іншими словами, ми знаходимо таке мінімальне , що v \notin \operatorname{span}(X_i) та додаємо до X_i. Якщо в лоб реалізувати цей алгоритм, то в гіршому випадку він буде працювати за , що досить неефективно.
Будемо покращувати цей алгоритм. Спершу розглянемо деякі властивості даної послідовності базисів X_n:
Використовуючи ці дві властивості можна покращити алгоритм, щоб додавання працювало за O(m^2) або навіть O(m \log{m}).
Нагадаємо, що ми знаходимо таке мінімальне , що v \notin \operatorname{span}(X_i) та додаємо до X_i. Оскільки унікальних \operatorname{span}(X_i) не більше m+1, то треба лише перебирати ті , для яких i=1 або \operatorname{span}(X_i) \neq \operatorname{span}(X_{i-1}). Тоді досягнемо складності O(m^2). Якщо використати бінарний пошук по цим індексам , то досягнемо складності O(m \log m).
Отже, ми навчилися додавати число v \neq 0 до множини .
Видалення числа v \in S
Оберемо , так що v \in X_j. Якщо просто видалити з X_j, то порушаться властивості, що зазначили попередньо. Ми будемо прагнути, щоб ці властивості виконувались після видалення.
Будемо видаляти таким чином:
Поки \operatorname{span}(X_j)=\operatorname{span}(X_{j+1}), то міняємо місцями базиси X_j та X_{j+1} (\operatorname{swap}(X_j, X_{j+1})) та збільшуємо на . Оскільки \operatorname{span}(X_j)=\operatorname{span}(X_{j+1}), то неважливо в якому порядку розташовані ці базиси у послідовності, тому можна довільно їх перемішувати;
Тепер \operatorname{span}(X_j) \neq \operatorname{span}(X_{j+1}). Маємо два випадки:
\operatorname{span}(X_j \setminus \{v\}) \supseteq \operatorname{span}(X_{j+1}). В цьому випадку можемо просто видалити число з X_j та закінчити процедуру;
\operatorname{span}(X_j \setminus \{v\}) \nsupseteq \operatorname{span}(X_{j+1}). Розглянемо одне твердження:
Знайдемо цей y \in X_{j+1} з твердження. Тоді можемо замінити у X_j на і тепер продовжити процедуру з кроку , але тепер ми будемо видаляти вже число з множини X_{j+1}.
Проаналізуємо часову складність процедури видалення. Скільки разів ми будемо попадати у випадок ? Нескладно зрозуміти, що не більше разів, оскільки кожен раз коли повертаємося на крок , то розмір нашого поточного базису X_j зменшується хоча б на .
Кроки 2.1-2.2 можна робити в лоб за O(m^2):
Повністю перебудуємо XOR базис X_j видаливши з нього елемент за O(m^2);
Додаємо елементи одного базису X_{j+1} в X_j \setminus \{v\} за O(m^2), щоб знайти або впевнитися, що такого немає.
Оскільки ці кроки можемо робити в гіршому випадку разів, то видалення буде працювати за O(m^3).
Насправді кроки 2.1-2.2 можемо робити за .
Перед тим як перейти до оптимізації, розглянемо як саме ми представляємо XOR базиси X_i, щоб з ними ефективно працювати.
Нехай та — XOR базиси, що побудовані алгоритмом вище. XOR базис, що представлений у такому вигляді має декілька важливих властивостей та наслідків з неї:
Розглянемо як саме видаляти число з базису та перебудовувати його за :
Кожне число базису також має маску , яка позначає лінійну комбінацію початкових елементів базису, за допомогою яких ми отримали цей елемент базису .
Нам треба зробити так, щоб числа вже не було у лінійних комбінаціях та при чому базис залишився правильно побудованим (збереглася властивість, яку наводили вище).
Виявляється, що це зробити досить просто. А саме знаходимо таке число базису , що містить у лінійній комбінації та серед таких чисел має найменший старший біт.
Далі видаляємо це знайдене число з базису та модифікуємо інші числа базису, що містили у лінійній комбінації, проXOR-ривши відповідне число з . Таким чином старший біт у цих числах не зміниться, отже, і властивість збережеться.
Паралельно рахуємо число , що містить всі старші біти чисел базису, які містили у лінійній комбінації число .
int rebuild_and_delete (int id) {
int pos = 0, mn = inf, p2 = 0;
for (int i = 0; i < basis.size(); i++) {
if (basis[i].id == id) {
pos = i;
}
}
int bits = 0;
for (int i = 0; i < basis.size(); i++) {
if (basis[i].mask & (1 << pos)) {
if (mn > basis[i].high) {
mn = basis[i].high;
p2 = i;
}
bits ^= 1 << basis[i].high;
}
}
if (p2 != pos) {
swap(basis[p2].id, basis[pos].id);
for (auto &i : basis) {
swap_bits(i.mask, pos, p2); // just swaps pos-th and p2-th bit in i.mask
}
pos = p2;
}
for (int i = 0; i < basis.size(); i++) {
if (i != pos) {
if (basis[i].mask & (1 << pos)) {
basis[i].val ^= basis[pos].val;
basis[i].mask ^= basis[pos].mask;
}
}
}
int good = (1 << pos) - 1;
int other = ((1 << len(basis)) - 1) ^ (good | (1 << pos));
basis.erase(basis.begin() + pos);
for (auto &i : basis) {
i.mask = ((i.mask & other) >> 1) | (i.mask & good);
}
return bits;
}Далі треба знайти з твердження за або сказати, що його не існує. Для має виконуватись така умова: \operatorname{span}((X_j \setminus \{v\}) \cup y)=\operatorname{span}(X_j). Ми знаємо який старший біт зник, при видаленні числа з X_j. Тому при додавання y \in X_{j+1} до X_j \setminus \{v\}, має «відповідати» саме за цей зниклий біт.
Тоді можемо за для кожного x \in X_{j+1} за перевірити чи підходить воно в якості . Нескладно переконатися, що підходить тоді та тільки, коли \operatorname{cnt}(bits \ \& \ x) \ \operatorname{mod} \ 2 = 1, де — функція, що повертає кількість одиничних бітів у числі.
int get_the_same_high_bit (int bits, vector <int> &val) {
for (auto &i : basis) {
if (__builtin_popcount(val[i.id] & bits) & 1) {
return i.id;
}
}
return -1;
}Тоді ми навчилися видаляти за O(m^2).
З повною версією кода можна ознайомитись тут:
P. S. Подібно до додавання, ми можемо спробувати використати бінарний пошук для видалення: знайти максимальне , що \operatorname{span}(X_j \setminus \{v \}) \nsupseteq \operatorname{span}(X_r). Але після переміщення цілого числа від X_r до X_j властивість послідовності базисів все ще може не виконуватися. І тому використання бінарного пошуку не покращує часову складність.
Деякий час я думав, що ми можемо зробити краще, ніж O(m^2), але не вдалося покращити. Справа в тому, що за допомогою цієї властивості \operatorname{span}(X_i) \supseteq \operatorname{span}(X_{i+1}) ми можемо побудувати послідовність цілих чисел a_i так, що \operatorname{ span}(X_i)=\operatorname{span}(\{a_1, a_2, \ldots, a_{\dim(X_i)} \}) для всіх . Маючи її, ми фактично можемо додавати за , але все ще не можу зрозуміти, як оптимізувати видалення.
Коментарі2