Эффективно найти целое число, не входящее в набор размером 40, 400 или 4000

В связи с классической задачей найти целое число не среди четырех миллиардов заданных, но не совсем то же самое.

Чтобы прояснить, под целыми числами я имею в виду лишь подмножество своего математического определения. То есть предположим, что существует только конечное число целых чисел. Скажем, в C++, они int в диапазоне [INT_MIN, INT_MAX].

Теперь, используя std::vector<int> (без дубликатов) или std::unordered_set<int>, чей размер может быть 40, 400, 4000 или около того, но не слишком большой, как эффективно генерировать число, которое гарантированно не быть среди данных?

Если нет проблем с переполнением, тогда я мог бы умножить все ненулевые единицы и добавить продукт на 1. Но есть. Тестовые случаи противника могут намеренно содержать INT_MAX.

Я больше за простые, неслучайные подходы. Есть ли?

Спасибо!

Обновление: чтобы убрать неоднозначность, скажем, несортированный std::vector<int> который гарантированно не будет иметь дубликатов. Поэтому я спрашиваю, есть ли что-нибудь лучше, чем O (n log (n)). Также обратите внимание, что контрольные примеры могут содержать как INT_MIN и INT_MAX.

Ответы

Ответ 1

Вы можете просто вернуть первое из N+1 кандидатов целых чисел, не содержащихся в ваших входных данных. Простейшими кандидатами являются числа от 0 до N Это требует O(N) пространства и времени.

 int find_not_contained(container<int> const&data)
 {
     const int N=data.size();
     std::vector<char> known(N+1, 0);   // one more candidates than data
     for(int i=0; i< N; ++i)
         if(data[i]>=0 && data[i]<=N)
             known[data[i]]=1;
     for(int i=0; i<=N; ++i)
         if(!known[i])
             return i;
     assert(false);                     // should never be reached.
 }

Случайные методы могут быть более компактными, но в худшем случае могут потребовать больше проходов по данным.

Ответ 2

Случайные методы действительно очень эффективны.

Если мы хотим использовать детерминированный метод и, предполагая, что размер n не слишком большой, например 4000, то мы можем создать вектор x размером m = n + 1 (или немного больше, например 4096, чтобы облегчить вычисление).), инициализируется 0.

Для каждого i в диапазоне мы просто устанавливаем x [array [i] по модулю m] = 1.

Тогда простой поиск O (n) по x даст значение, которого нет в массиве.

Примечание: операция по модулю не совсем операция "%"

Изменение: я упомянул, что вычисления упрощаются, выбрав здесь размер 4096. Чтобы быть более конкретным, это означает, что операция по модулю выполняется с простой & операции

Ответ 3

Вы можете найти наименьшее неиспользуемое целое число в O (N) времени, используя O (1) вспомогательное пространство, если вам разрешено изменить порядок входного вектора, используя следующий алгоритм. [Примечание 1] (Алгоритм также работает, если вектор содержит повторяющиеся данные.)

size_t smallest_unused(std::vector<unsigned>& data) {
  size_t N = data.size(), scan = 0;
  while (scan < N) {
    auto other = data[scan];
    if (other < scan && data[other] != other) {
      data[scan] = data[other];
      data[other] = other;
    }
    else
      ++scan;
  }
  for (scan = 0; scan < N && data[scan] == scan; ++scan) { }
  return scan;
}

Первый проход гарантирует, что если некоторое k в диапазоне [0, N) было найдено после позиции k, то оно теперь присутствует в позиции k. Эта перестановка выполняется путем замены, чтобы избежать потери данных. Когда сканирование завершено, первая запись, значение которой не совпадает с индексом, нигде не упоминается в массиве.

Это утверждение не может быть на 100% очевидным, поскольку на запись можно ссылаться из более раннего индекса. Однако в этом случае запись не может быть первой записью, не равной ее индексу, поскольку более ранняя запись будет соответствовать этому критерию.

Чтобы увидеть, что этот алгоритм равен O (N), следует заметить, что своп в строках 6 и 7 может произойти только в том случае, если целевая запись не равна его индексу, и что после свопа целевая запись равна его индексу, Таким образом, можно выполнить не более N перестановок, и условие if в строке 5 будет true не более N раз. С другой стороны, если условие if ложно, scan будет увеличиваться, что также может происходить только N раз. Таким образом, оператор if оценивается не более 2N раз (что равно O (N)).


Заметки:

  1. Я использовал здесь целые числа без знака, потому что это делает код более понятным. Алгоритм может быть легко скорректирован для целых чисел со [INT_MIN, 0) например, путем сопоставления целых чисел со [INT_MIN, 0) из [INT_MIN, 0) в целые числа без знака [INT_MAX, INT_MAX - INT_MIN) (Вычитание является математическим, не в соответствии с семантикой C, которая не позволяет получить результат быть представленным.) В дополнении 2-х, это тот же битовый шаблон. Это, конечно, меняет порядок чисел, что влияет на семантику "наименьшего неиспользованного целого числа"; можно также использовать сохраняющее порядок отображение.

Ответ 4

Сделайте случайный x (INT_MIN..INT_MAX) и протестируйте его против всех. Проверьте x++ на неудачу (очень редкий случай для 40/400/4000).

Ответ 5

Шаг 1: Сортировка вектора.

Это можно сделать в O (n log (n)), вы можете найти несколько различных алгоритмов онлайн, используйте тот, который вам нравится больше всего.

Шаг 2: Найти первый int не в векторе.

Легко итерируйте от INT_MIN до INT_MIN + 40/400/4000, проверяя, имеет ли вектор текущее значение int:

псевдокод:

SIZE = 40|400|4000 // The one you are using
for (int i = 0; i < SIZE; i++) {
    if (array[i] != INT_MIN + i)
        return INT_MIN + i;

Решение будет O (n log (n) + n), что означает: O (n log (n))


Редактировать: просто прочитайте ваши изменения, прося что-то лучше, чем O (n log (n)), извините.

Ответ 6

Для случая, когда целые числа представлены в std::unordered_set<int> (в отличие от std::vector<int>), вы можете просто пройти диапазон целочисленных значений, пока не встретите одно целочисленное значение, которое отсутствует в unordered_set<int>. Поиск целого числа в std::unordered_set<int> довольно прост, поскольку std::unodered_set обеспечивает поиск через функцию-член find().

Пространственная сложность этого подхода будет O (1).


Если вы начнете обходить самое низкое из возможных значений для типа int (то есть std::numeric_limits<int>::min()), вы получите самое низкое значение int не содержащееся в std::unordered_set<int>:

int find_lowest_not_contained(const std::unordered_set<int>& set) {
   for (auto i = std::numeric_limits<int>::min(); ; ++i) {
      auto it = set.find(i); // search in set
      if (it == set.end()) // integer not in set?
         return *it;
   }
}

Аналогично, если вы начнете обходить максимально возможное значение для типа int (то есть std::numeric_limits<int>::max()), вы получите самое низкое значение int не содержащееся в std::unordered_set<int>:

int find_greatest_not_contained(const std::unordered_set<int>& set) {
   for (auto i = std::numeric_limits<int>::max(); ; --i) {
      auto it = set.find(i); // search in set
      if (it == set.end()) // integer not in set?
         return *it;
   }
}

Предполагая, что int равномерно отображается хэш-функцией в сегменты unordered_set, операция поиска для unordered_set<int> может быть выполнена за постоянное время. В этом случае сложность во время выполнения будет равна O (M), где M - это размер целочисленного диапазона, который вы ищете для не содержащего значения. M ограничен сверху размером unordered_set<int> (т.е. В вашем случае M <= 4000).

Действительно, при таком подходе выбор любого целочисленного диапазона, размер которого больше размера unordered_set, гарантированно натолкнется на целочисленное значение, которого нет в unordered_set<int>.