Перейти до основного вмісту

MEX (мінімальне виключене значення) послідовності

Дано масив AA розміру NN. Потрібно знайти найменший невід'ємний елемент, якого немає в масиві. Це число зазвичай називають MEX (мінімальне виключене значення).

mex({0,1,2,4,5})=3mex({0,1,2,3,4})=5mex({1,2,3,4,5})=0\begin{align} \text{mex}(\{0, 1, 2, 4, 5\}) &= 3 \\ \text{mex}(\{0, 1, 2, 3, 4\}) &= 5 \\ \text{mex}(\{1, 2, 3, 4, 5\}) &= 0 \\ \end{align}

Зауважимо, що MEX масиву розміру NN ніколи не може бути більшим за саме NN.

Найпростіший підхід — створити множину всіх елементів масиву AA, щоб ми могли швидко перевіряти, чи входить число до масиву, чи ні. Тоді ми можемо перебрати всі числа від 00 до NN і повернути перше, якого немає в множині.

Коли підходить цей алгоритм?
  • Чи шукаєш саме найменше невід'ємне відсутнє число (а не, скажімо, KK-й елемент за порядком — тоді → KK-та порядкова статистика)?
  • Чи достатньо обчислити MEX один раз — тоді вистачить булевого масиву за O(N)O(N)?
  • Чи масив змінюється між запитами — тоді потрібна структура (set / дерево відрізків) для O(logN)O(\log N) на оновлення?

Реалізація

Наведений нижче алгоритм працює за час O(NlogN)O(N \log N).

int mex(vector<int> const& A) {
set<int> b(A.begin(), A.end());

int result = 0;
while (b.count(result))
++result;
return result;
}

Якщо алгоритму потрібне обчислення MEX за O(N)O(N), цього можна досягти, використавши булевий вектор замість множини. Зауважимо, що масив має бути таким великим, як максимально можливий розмір масиву.

int mex(vector<int> const& A) {
static bool used[MAX_N+1] = { 0 };

// позначаємо задані числа
for (int x : A) {
if (x <= MAX_N)
used[x] = true;
}

// знаходимо mex
int result = 0;
while (used[result])
++result;

// знову очищаємо масив
for (int x : A) {
if (x <= MAX_N)
used[x] = false;
}

return result;
}

Цей підхід швидкий, але добре працює лише тоді, коли MEX потрібно обчислити один раз. Якщо ж його потрібно обчислювати знову й знову, наприклад тому, що масив постійно змінюється, то він неефективний. Для цього нам потрібно щось краще.

MEX з оновленнями масиву

У цій задачі потрібно змінювати окремі числа в масиві й обчислювати новий MEX масиву після кожного такого оновлення.

Тут потрібна краща структура даних, яка ефективно обробляє такі запити.

Один із підходів — узяти частоту кожного числа від 00 до NN і побудувати над нею деревоподібну структуру даних. Наприклад, дерево відрізків або декартове дерево. Кожна вершина представляє діапазон чисел, і разом із загальною частотою на цьому діапазоні ми додатково зберігаємо кількість різних чисел у цьому діапазоні. Таку структуру даних можна оновлювати за час O(logN)O(\log N), а також знаходити MEX за час O(logN)O(\log N), виконуючи бінарний пошук MEX. Якщо вершина, що представляє діапазон [0,N/2)[0, \lfloor N/2 \rfloor), не містить N/2\lfloor N/2 \rfloor різних чисел, то одного бракує, MEX менший за N/2\lfloor N/2 \rfloor і можна рекурсивно перейти в ліву гілку дерева. Інакше він не менший за N/2\lfloor N/2 \rfloor і можна рекурсивно перейти в праву гілку дерева.

Також можна скористатися структурами даних map і set зі стандартної бібліотеки (на основі підходу, поясненого тут). За допомогою map ми будемо запам'ятовувати частоту кожного числа, а за допомогою set представлятимемо числа, яких наразі немає в масиві. Оскільки set упорядкований, *set.begin() буде MEX. Загалом нам потрібно O(NlogN)O(N \log N) на попередні обчислення, після чого MEX можна обчислити за O(1)O(1), а оновлення можна виконати за O(logN)O(\log N).

class Mex {
private:
map<int, int> frequency;
set<int> missing_numbers;
vector<int> A;

public:
Mex(vector<int> const& A) : A(A) {
for (int i = 0; i <= A.size(); i++)
missing_numbers.insert(i);

for (int x : A) {
++frequency[x];
missing_numbers.erase(x);
}
}

int mex() {
return *missing_numbers.begin();
}

void update(int idx, int new_value) {
if (--frequency[A[idx]] == 0)
missing_numbers.insert(A[idx]);
A[idx] = new_value;
++frequency[new_value];
missing_numbers.erase(new_value);
}
};

Задачі для практики

Відеоматеріали