Ddenisovkostya

XOR базис з видаленнями

Передмова

Ця стаття використовує терміни лінійної алгебри. Якщо ви їх розумієте, можете пропустити цей розділ.

Вступ

Згадаємо таку відому задачу:

Детальніше про те як розв'язувати цю задачу можна дізнатися за посиланням.

В цій статті будемо розглядати більш складну задачу, де ще є запити видалення числа з множини . Крім того, ми отримуємо наступний запит, тільки після того, як відповіли на попередній (тобто задачу треба розв'язувати "в онлайні").

Початкову задачу можна розв'язувати за на запит, а от задачу з видаленнями будемо навчатися розв'язувати за O(m^2).

Структура

У задачі з видаленнями в нас починаються невизначеності на моменті, коли число, що ми додаємо до XOR базису представляється -ами наявних чисел.

Ми хочемо ефективно зберегти кожне число, що додається до , тому будемо підтримувати послідовність X_n (n \ge 1) XOR базисів (кожен з яких є початково порожнім), замість одного базису.

Додавання числа до

Розглянемо як саме будемо додавати нове число v \neq 0 до множини :

  1. Будемо поступово обходити по нашим базисами починаючи з першого. Нехай — номер поточного базису, який розглядаємо;

  2. Якщо v \notin \operatorname{span}(X_i), то просто додамо до X_i і закінчимо процедуру;

  3. Інакше, 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, то порушаться властивості, що зазначили попередньо. Ми будемо прагнути, щоб ці властивості виконувались після видалення.

Будемо видаляти таким чином:

  1. Поки \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}), то неважливо в якому порядку розташовані ці базиси у послідовності, тому можна довільно їх перемішувати;

  2. Тепер \operatorname{span}(X_j) \neq \operatorname{span}(X_{j+1}). Маємо два випадки:

    1. \operatorname{span}(X_j \setminus \{v\}) \supseteq \operatorname{span}(X_{j+1}). В цьому випадку можемо просто видалити число з X_j та закінчити процедуру;

    2. \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):

  1. Повністю перебудуємо XOR базис X_j видаливши з нього елемент за O(m^2);

  2. Додаємо елементи одного базису X_{j+1} в X_j \setminus \{v\} за O(m^2), щоб знайти або впевнитися, що такого немає.

Оскільки ці кроки можемо робити в гіршому випадку разів, то видалення буде працювати за O(m^3).

Насправді кроки 2.1-2.2 можемо робити за .

Перед тим як перейти до оптимізації, розглянемо як саме ми представляємо XOR базиси X_i, щоб з ними ефективно працювати.

Нехай та — XOR базиси, що побудовані алгоритмом вище. XOR базис, що представлений у такому вигляді має декілька важливих властивостей та наслідків з неї:

Розглянемо як саме видаляти число з базису та перебудовувати його за :

  1. Кожне число базису також має маску , яка позначає лінійну комбінацію початкових елементів базису, за допомогою яких ми отримали цей елемент базису .

  2. Нам треба зробити так, щоб числа вже не було у лінійних комбінаціях та при чому базис залишився правильно побудованим (збереглася властивість, яку наводили вище).

  3. Виявляється, що це зробити досить просто. А саме знаходимо таке число базису , що містить у лінійній комбінації та серед таких чисел має найменший старший біт.

  4. Далі видаляємо це знайдене число з базису та модифікуємо інші числа базису, що містили у лінійній комбінації, проXOR-ривши відповідне число з . Таким чином старший біт у цих числах не зміниться, отже, і властивість збережеться.

  5. Паралельно рахуємо число , що містить всі старші біти чисел базису, які містили у лінійній комбінації число .

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)} \}) для всіх . Маючи її, ми фактично можемо додавати за , але все ще не можу зрозуміти, як оптимізувати видалення.

40

Коментарі2