it-roy-ru.com

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

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

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

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

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

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

Спасибо!

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

29
fleix

Вы можете просто вернуть первое из 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.
 }

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

31
Walter

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

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

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

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

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

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

9
Damien

Вы можете найти наименьшее неиспользуемое целое число во времени 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_MAX, INT_MAX - INT_MIN) (вычитание математическое, не в соответствии с семантикой C, которая не позволяет представить результат.) В дополнении 2, это та же самая картина битов. Конечно, это меняет порядок чисел, что влияет на семантику «наименьшего неиспользованного целого числа»; можно также использовать сохраняющее порядок отображение.
6
rici

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

4
Alexey Birukov

Шаг 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)), извините.

2
dquijada

Для случая, когда целые числа представлены в 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;
   }
}

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

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

0
El Profesor