Ця стаття використовує терміни лінійної алгебри. Якщо ви їх розумієте, можете пропустити цей розділ.
Вступ
Згадаємо таку відому задачу:
Детальніше про те як розв'язувати цю задачу можна дізнатися за посиланням.
В цій статті будемо розглядати більш складну задачу, де ще є запити видалення числа з множини S. Крім того, ми отримуємо наступний запит, тільки після того, як відповіли на попередній (тобто задачу треба розв'язувати "в онлайні").
Початкову задачу можна розв'язувати за O(m) на запит, а от задачу з видаленнями будемо навчатися розв'язувати за O(m2).
Структура
У задачі з видаленнями в нас починаються невизначеності на моменті, коли число, що ми додаємо до XOR базису представляється xor-ами наявних чисел.
Ми хочемо ефективно зберегти кожне число, що додається до S, тому будемо підтримувати послідовність Xn(n≥1) XOR базисів (кожен з яких є початково порожнім), замість одного базису.
Додавання числа v до S
Розглянемо як саме будемо додавати нове число v=0 до множини S:
Будемо поступово обходити по нашим базисами починаючи з першого. Нехай i — номер поточного базису, який розглядаємо;
Якщо v∈/span(Xi), то просто додамо v до Xi і закінчимо процедуру;
Інакше, v∈span(Xi), то так додати не виходить, але ми прагнемо обов'язково десь зберегти це число. Тому збільшуємо i на 1 та повертаємося до кроку 2.
Іншими словами, ми знаходимо таке мінімальне i, що v∈/span(Xi) та додаємо v до Xi. Якщо в лоб реалізувати цей алгоритм, то в гіршому випадку він буде працювати за O(∣S∣), що досить неефективно.
Будемо покращувати цей алгоритм. Спершу розглянемо деякі властивості даної послідовності базисів Xn:
Використовуючи ці дві властивості можна покращити алгоритм, щоб додавання працювало за O(m2) або навіть O(mlogm).
Нагадаємо, що ми знаходимо таке мінімальне i, що v∈/span(Xi) та додаємо v до Xi. Оскільки унікальних span(Xi) не більше m+1, то треба лише перебирати ті i, для яких i=1 або span(Xi)=span(Xi−1). Тоді досягнемо складності O(m2). Якщо використати бінарний пошук по цим індексам i, то досягнемо складності O(mlogm).
Отже, ми навчилися додавати число v=0 до множини S.
Видалення числа v∈S
Оберемо j, так що v∈Xj. Якщо просто видалити v з Xj, то порушаться властивості, що зазначили попередньо. Ми будемо прагнути, щоб ці властивості виконувались після видалення.
Будемо видаляти таким чином:
Поки span(Xj)=span(Xj+1), то міняємо місцями базиси Xj та Xj+1 (swap(Xj,Xj+1)) та збільшуємо j на 1. Оскільки span(Xj)=span(Xj+1), то неважливо в якому порядку розташовані ці базиси у послідовності, тому можна довільно їх перемішувати;
Тепер span(Xj)=span(Xj+1). Маємо два випадки:
span(Xj∖{v})⊇span(Xj+1). В цьому випадку можемо просто видалити число v з Xj та закінчити процедуру;
span(Xj∖{v})⊉span(Xj+1). Розглянемо одне твердження:
Знайдемо цей y∈Xj+1 з твердження. Тоді можемо замінити v у Xj на y і тепер продовжити процедуру з кроку 1, але тепер ми будемо видаляти вже число y з множини Xj+1.
Проаналізуємо часову складність процедури видалення. Скільки разів ми будемо попадати у випадок 2.2? Нескладно зрозуміти, що не більше m разів, оскільки кожен раз коли повертаємося на крок 1, то розмір нашого поточного базису Xj зменшується хоча б на 1.
Кроки 2.1−2.2 можна робити в лоб за O(m2):
Повністю перебудуємо XOR базис Xj видаливши з нього елемент v за O(m2);
Додаємо елементи одного базису Xj+1 в Xj∖{v} за O(m2), щоб знайти y або впевнитися, що такого немає.
Оскільки ці кроки можемо робити в гіршому випадку m разів, то видалення буде працювати за O(m3).
Насправді кроки 2.1−2.2 можемо робити за O(m).
Перед тим як перейти до оптимізації, розглянемо як саме ми представляємо XOR базиси Xi, щоб з ними ефективно працювати.
Нехай A та B — XOR базиси, що побудовані алгоритмом вище. XOR базис, що представлений у такому вигляді має декілька важливих властивостей та наслідків з неї:
Розглянемо як саме видаляти число v з базису та перебудовувати його за O(m):
Кожне число базису також має маску mask, яка позначає лінійну комбінацію початкових елементів базису, за допомогою яких ми отримали цей елемент базису val.
Нам треба зробити так, щоб числа v вже не було у лінійних комбінаціях та при чому базис залишився правильно побудованим (збереглася властивість, яку наводили вище).
Виявляється, що це зробити досить просто. А саме знаходимо таке число базису x, що містить v у лінійній комбінації та серед таких чисел має найменший старший біт.
Далі видаляємо це знайдене число з базису та модифікуємо інші числа базису, що містили v у лінійній комбінації, проXOR-ривши відповідне число з x. Таким чином старший біт у цих числах не зміниться, отже, і властивість збережеться.
Паралельно рахуємо число bits, що містить всі старші біти чисел базису, які містили у лінійній комбінації число v.
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;
}
Далі треба знайти y з твердження 1 за O(m) або сказати, що його не існує. Для y має виконуватись така умова: span((Xj∖{v})∪y)=span(Xj). Ми знаємо який старший біт зник, при видаленні числа v з Xj. Тому при додавання y∈Xj+1 до Xj∖{v}, y має «відповідати» саме за цей зниклий біт.
Тоді можемо за для кожного x∈Xj+1 за O(1) перевірити чи підходить воно в якості y. Нескладно переконатися, що x підходить тоді та тільки, коли cnt(bits&x)mod2=1, де cnt(t) — функція, що повертає кількість одиничних бітів у числі.
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(m2).
З повною версією кода можна ознайомитись тут:
P. S. Подібно до додавання, ми можемо спробувати використати бінарний пошук для видалення: знайти максимальне r, що span(Xj∖{v})⊉span(Xr). Але після переміщення цілого числа від Xr до Xj властивість послідовності базисів все ще може не виконуватися. І тому використання бінарного пошуку не покращує часову складність.
Деякий час я думав, що ми можемо зробити краще, ніж O(m2), але не вдалося покращити. Справа в тому, що за допомогою цієї властивості span(Xi)⊇span(Xi+1) ми можемо побудувати послідовність цілих чисел ai так, що span(Xi)=span({a1,a2,…,adim(Xi)}) для всіх i. Маючи її, ми фактично можемо додавати за O(m), але все ще не можу зрозуміти, як оптимізувати видалення.