Простой вопрос интервью усложнился: по номерам 1..100 найдите пропущенные числа, по которым точно k отсутствуют

1146

Некоторое время назад у меня был интересный опыт собеседования. Вопрос начался очень просто:

Q1 : У нас есть пакет , содержащий номера 1, 2, 3..., 100. Каждый номер появляется ровно один раз, поэтому есть 100 номеров. Теперь один номер случайно выбирается из сумки. Найдите пропущенный номер.

Конечно, я слышал этот вопрос раньше, поэтому очень быстро ответил:

A1 : Ну, сумма чисел 1 + 2 + 3 + … + Nравна (N+1)(N/2)(см. Википедия: сумма арифметических рядов ). Ибо N = 100сумма есть 5050.

Таким образом, если в сумке присутствуют все числа, сумма будет точно 5050. Так как отсутствует одно число, сумма будет меньше этой, а разница в этом числе. Таким образом, мы можем найти это недостающее число во O(N)времени и O(1)пространстве.

В этот момент я думал, что у меня все хорошо, но внезапно вопрос принял неожиданный оборот:

Q2 : Это правильно, но как бы вы это сделали, если ДВЕ номера не хватает?

Я никогда раньше не видел / не слышал / не рассматривал эту вариацию, поэтому запаниковал и не смог ответить на вопрос. Интервьюер настаивал на том, чтобы знать мой мыслительный процесс, поэтому я упомянул, что, возможно, мы можем получить больше информации, сравнивая с ожидаемым продуктом, или, возможно, сделав второй проход после сбора некоторой информации с первого прохода и т. Д., Но я действительно просто снимал в темноте, а не на самом деле иметь четкий путь к решению.

Интервьюер попытался меня ободрить, сказав, что наличие второго уравнения действительно является одним из способов решения проблемы. В этот момент я был немного расстроен (из-за того, что не знал ответа заранее) и спросил, является ли это общей (читай: «полезной») техникой программирования или это просто ответ с подвохом.

Ответ интервьюера удивил меня: вы можете обобщить технику, чтобы найти 3 пропущенных числа. Фактически, вы можете обобщить это, чтобы найти k пропущенных чисел.

Qk : Если в сумке нет ровно k чисел, как бы вы нашли это эффективно?

Это было несколько месяцев назад, и я до сих пор не могу понять, что это за техника. Очевидно , что это Ω(N)время нижней границы , так как мы должны сканировать все номера , по крайней мере , один раз, но интервьюер настаивает , что ВРЕМЯ и ПРОСТРАНСТВО сложность методы решения задачи (минус O(N)входном сканирование времени) определяются в к не N .

Так что вопрос здесь прост:

  • Как бы вы решили Q2 ?
  • Как бы вы решили Q3 ?
  • Как бы вы решили Qk ?

Разъяснения

  • Обычно есть N чисел от 1 .. N , а не только 1..100.
  • Я не ищу очевидное основанное на множестве решение, например, используя набор битов , кодируя присутствие / отсутствие каждого числа значением назначенного бита, поэтому используя O(N)биты в дополнительном пространстве. Мы не можем позволить себе любое дополнительное пространство , пропорциональное N .
  • Я также не ищу очевидного подхода первого порядка. Этот и основанный на множестве подходы заслуживают упоминания в интервью (они просты в реализации и в зависимости от N могут быть очень практичными). Я ищу решение Святого Грааля (которое может или не может быть практичным для реализации, но, тем не менее, имеет желаемые асимптотические характеристики).

Итак, еще раз, конечно, вы должны сканировать входные данные O(N), но вы можете захватить только небольшое количество информации (определяемой в терминах k, а не N ), а затем должны как-то найти k пропущенных чисел.

polygenelubricants
источник
7
@polygenelubricants Спасибо за разъяснения. «Я ищу алгоритм, который использует время O (N) и пространство O (K), где K - число отсутствующих чисел», было бы ясно с самого начала ;-)
Дейв О.
7
Вы должны уточнить, в заявлении Q1, что вы не можете получить доступ к номерам по порядку. Это, вероятно, кажется очевидным для вас, но я никогда не слышал об этом вопросе, и термин «сумка» (что также означает «мультимножество») был своего рода путаницей.
Жереми
7
Пожалуйста, прочитайте следующее, так как ответы, представленные здесь, смешны: stackoverflow.com/questions/4406110/…
18
Решение суммирования чисел требует пространства log (N), если только вы не считаете, что для неограниченного целого числа требуется пространство O (1). Но если вы разрешите неограниченные целые числа, то у вас будет столько места, сколько вы хотите, только с одним целым числом.
Удо Кляйн
3
Кстати, довольно хорошим альтернативным решением для Q1 может быть вычисление XORвсех чисел от 1до n, а затем сохранение результата со всеми числами в данном массиве. В конце концов, у вас есть пропущенный номер. В этом решении вам не нужно заботиться о переполнении, как при суммировании.
сбеляков

Ответы:

590

Вот краткое изложение ссылки Димитриса Андреу .

Запомните сумму i-й степени, где i = 1,2, .., k. Это сводит проблему к решению системы уравнений

a 1 + a 2 + ... + a k = b 1

a 1 2 + a 2 2 + ... + a k 2 = b 2

...

a 1 k + a 2 k + ... + a k k = b k

Используя тождества Ньютона , зная, что b i позволяет вычислить

c 1 = a 1 + a 2 + ... a k

c 2 = a 1 a 2 + a 1 a 3 + ... + a k-1 a k

...

с к = а 1 а 2 ... a k

Если развернуть многочлен (xa 1 ) ... (xa k ), коэффициенты будут точно c 1 , ..., c k - см . Формулы Виета . Поскольку каждый полиномиальный фактор однозначно (кольцо полиномов является евклидовой областью ), это означает, что a i однозначно определены, с точностью до перестановки.

Это завершает доказательство того, что запоминание способностей достаточно для восстановления чисел. Для константы k это хороший подход.

Однако, когда k изменяется, прямой подход к вычислению c 1 , ..., c k является чрезмерно дорогим, поскольку, например, c k является произведением всех пропущенных чисел, величина n! / (Nk) !. Чтобы преодолеть это, выполните вычисления в поле Z q , где q такое простое число, что n <= q <2n - оно существует по постулату Бертрана . Доказательство менять не нужно, поскольку формулы все еще выполняются, а факторизация полиномов по-прежнему уникальна. Вам также нужен алгоритм для факторизации по конечным полям, например алгоритм Берлекампа или Кантора-Цассенхауза .

Псевдокод высокого уровня для константы k:

  • Вычислить i-й степени заданных чисел
  • Вычтите, чтобы получить суммы i-й степени неизвестных чисел. Назовите суммы b i .
  • Используйте тождества Ньютона для вычисления коэффициентов из b i ; называть их c i . В основном, c 1 = b 1 ; c 2 = (c 1, b 1 - b 2 ) / 2; см. Википедию для точных формул
  • Фактор многочлена x k -c 1 x k-1 + ... + c k .
  • Корнями многочлена являются нужные числа a 1 , ..., a k .

Для варьирования k найдите простое число n <= q <2n, используя, например, Миллера-Рабина, и выполните шаги с уменьшением всех чисел по модулю q.

РЕДАКТИРОВАТЬ: предыдущая версия этого ответа заявил, что вместо Z q , где q является простым, можно использовать конечное поле характеристики 2 (q = 2 ^ (log n)). Это не так, поскольку формулы Ньютона требуют деления на числа до k.

sdcvvc
источник
6
Вам не нужно использовать простое поле, вы также можете использовать q = 2^(log n). (Как ты сделал супер- и подписки ?!)
Генрих Апфельмус
49
+1 Это действительно очень умно. В то же время сомнительно, действительно ли оно того стоит, или можно ли (частично) использовать это решение для довольно искусственной проблемы другим способом. И даже если бы это была проблема реального мира, на многих платформах наиболее тривиальное O(N^2)решение, возможно, превзошло бы эту красоту даже на достаточно высоком уровне N. Заставляет меня думать об этом: tinyurl.com/c8fwgw Тем не менее, отличная работа! У меня не хватило бы терпения
пролистать
167
Я думаю, что это прекрасный ответ. Я думаю, что это также показывает, насколько плохим был бы вопрос об интервью, чтобы расширить пропущенные числа за единицу. Даже первое - это что-то вроде гоча, но достаточно распространено, что оно в основном показывает: «Вы подготовились к собеседованию». Но ожидать, что майор CS знает, что выходят за пределы k = 1 (особенно «на месте» в интервью), немного глупо.
CorsiKa
5
Это эффективно делает кодирование Рида Соломона на входе.
Дэвид Эрманн
78
Бьюсь об заклад, ввод всего числа в a hash setи итерация по 1...Nнабору с использованием поиска, чтобы определить, отсутствуют ли числа, будет самым общим, самым быстрым в среднем по kвариациям, наиболее отлаживаемым, наиболее приемлемым и понятным решением. Конечно, математика впечатляет, но где-то по пути вам нужно быть инженером, а не математиком. Особенно, когда дело касается бизнеса.
v.oddou
243

Вы найдете это, прочитав пару страниц алгоритмов Muthukrishnan - Data Stream: Головоломка 1: Поиск пропущенных чисел . Он показывает именно то обобщение, которое вы ищете . Возможно, именно это прочитал ваш интервьюер и почему он задал эти вопросы.

Теперь, если бы только люди начали удалять ответы, которые включены или заменены обработкой Мутукришнана, и облегчить поиск этого текста. :)


Также смотрите прямой ответ sdcvvc , который также включает псевдокод (ура! Нет необходимости читать эти хитрые математические формулировки :)) (спасибо, отличная работа!).

Димитрис Андреу
источник
Ооо ... Это интересно. Я должен признать, что я немного запутался в математике, но я только просматривал это. Могу оставить это открытым, чтобы посмотреть на более позже. :) И +1, чтобы получить эту ссылку более доступной. ;-)
Крис
2
Ссылка на книги Google не работает для меня. Здесь лучшая версия [PostScript File].
Генрих Апфельмус
9
Ух ты. Я не ожидал, что за это проголосуют! В прошлый раз, когда я публиковал ссылку на решение (в данном случае Кнута) вместо того, чтобы пытаться решить его самостоятельно, оно фактически было отвергнуто: stackoverflow.com/questions/3060104/… Библиотекарь внутри меня радуется, спасибо :)
Dimitris Andreou
@Apfelmus, обратите внимание, что это черновик. (Я, конечно, не виню вас, я почти год путал черновик с реальными вещами, прежде чем нашел книгу). Кстати, если ссылка не работает, вы можете перейти на books.google.com и найти «Алгоритмы потока данных Muthukrishnan» (без кавычек), он появляется первым.
Димитрис Андреу
2
Пожалуйста, прочитайте следующее, так как ответы, представленные здесь, смешны: stackoverflow.com/questions/4406110/…
174

Мы можем решить Q2, суммируя как сами числа, так и квадраты чисел.

Затем мы можем уменьшить проблему до

k1 + k2 = x
k1^2 + k2^2 = y

Где xи yкак далеко суммы ниже ожидаемых значений.

Подставляя дает нам:

(x-k2)^2 + k2^2 = y

Что мы можем затем решить, чтобы определить наши пропущенные числа.

Anon.
источник
7
+1; Я попробовал формулу в Maple для выбора чисел, и она работает. Я все еще не мог убедить себя, ПОЧЕМУ это работает, все же.
полигенасмазочные материалы
4
@polygenelubricants: Если вы хотите , чтобы доказать правильность, вы бы первым показать , что она всегда обеспечивает более правильное решение (то есть, она всегда дает пару чисел , которые при удалении их из набора, приведут к остальной части набора , имеющим наблюдаемая сумма и сумма квадратов). Оттуда доказательство уникальности так же просто, как показ, что она производит только одну такую ​​пару чисел.
Анон.
5
Природа уравнений означает, что вы получите два значения k2 из этого уравнения. Тем не менее, из первого уравнения, которое вы используете для генерации k1, вы можете видеть, что эти два значения k2 будут означать, что k1 - это другое значение, поэтому у вас есть два решения с одинаковыми числами в противоположных направлениях. Если вы ошибочно заявили, что k1> k2, то у вас будет только одно решение квадратного уравнения и, следовательно, одно решение в целом. И ясно, что по природе вопроса ответ всегда существует, поэтому он всегда работает.
Крис
3
Для данной суммы k1 + k2 существует много пар. Мы можем записать эти пары как K1 = a + b и K2 = ab, где a = (K1 + k2 / 2). а является уникальным для данной суммы. Сумма квадратов (a + b) ** 2 + (ab) ** 2 = 2 * (a 2 + b 2). Для данной суммы K1 + K2 член a 2 является фиксированным, и мы видим, что сумма квадратов будет уникальной благодаря члену b 2. Следовательно, значения x и y уникальны для пары целых чисел.
phkahler
8
Это круто. @ user3281743 вот пример. Пусть пропущенные числа (k1 и k2) равны 4 и 6. Sum (1 -> 10) = 55 и Sum (1 ^ 2 -> 10 ^ 2) = 385. Теперь пусть x = 55 - (Sum (Все оставшиеся числа) )) и y = 385 - (сумма (квадраты всех оставшихся чисел)), таким образом, x = 10 и y = 52. Замените, как показано, что оставляет нас с: (10 - k2) ^ 2 + k2 ^ 2 = 52, которые вы можете упростим до: 2k ^ 2 - 20k + 48 = 0. Решение квадратного уравнения дает вам 4 и 6 в качестве ответа.
AlexKoren
137

Как указал @j_random_hacker, это очень похоже на поиск дубликатов в O (n) времени и O (1) пространстве , и адаптация моего ответа там тоже работает.

Предполагая, что «мешок» представлен массивом A[]размером 1 N - k, мы можем решить Qk во O(N)времени и O(k)дополнительном пространстве.

Во- первых, мы расширим наш массив с A[]помощью kэлементов, так что теперь размер N. Это O(k)дополнительное пространство. Затем мы запускаем следующий алгоритм псевдокода:

for i := n - k + 1 to n
    A[i] := A[1]
end for

for i := 1 to n - k
    while A[A[i]] != A[i] 
        swap(A[i], A[A[i]])
    end while
end for

for i := 1 to n
    if A[i] != i then 
        print i
    end if
end for

Первый цикл инициализирует kдополнительные записи так же, как и первая запись в массиве (это просто удобное значение, которое, как мы знаем, уже присутствует в массиве - после этого шага любые записи, отсутствующие в исходном массиве размера N-k, все еще отсутствует в расширенном массиве).

Второй цикл переставляет расширенный массив, так что если элемент xприсутствует хотя бы один раз, то одна из этих записей будет в позицииA[x] .

Обратите внимание, что, хотя у него есть вложенный цикл, он все еще выполняется во O(N)времени - своп происходит только в том случае, если есть iтакой A[i] != i, и каждый своп устанавливает как минимум один элемент, такой A[i] == i, где это не было ранее. Это означает, что общее количество перестановок (и, следовательно, общее количество выполнений whileтела цикла) не более N-1.

Третий цикл печатает те индексы массива i, которые не заняты значением i- это означает, что, iдолжно быть, отсутствовал.

кафе
источник
4
Интересно, почему так мало людей голосуют за этот ответ и даже не отметили его как правильный ответ? Вот код на Python. Он работает за O (n) и требует дополнительного пространства O (k). pastebin.com/9jZqnTzV
wall-e
3
@caf это очень похоже на установку битов и подсчет мест, где бит равен 0. И я думаю, что когда вы создаете целочисленный массив, больше памяти занято.
Fox
5
«Установка битов и подсчет мест, где бит равен 0», требует O (n) дополнительного пространства, это решение показывает, как использовать O (k) дополнительное пространство.
Кафе
7
Не работает с потоками в качестве входных данных и изменяет входной массив (хотя мне это очень нравится, и идея плодотворна).
Comco
3
@ v.oddou: Нет, все в порядке. Обмен изменится A[i], что означает, что следующая итерация не будет сравнивать те же два значения, что и предыдущая. Новое A[i]будет таким же, как последний цикл A[A[i]], но новое A[A[i]]будет новым значением. Попробуйте и посмотрите.
Кафе,
128

Я попросил 4-летнего ребенка решить эту проблему. Он отсортировал числа и затем сосчитал. Это требует пространства O (пол на кухне) и работает так же просто, как и многие шары.

Полковник паника
источник
20
;) Ваш 4-летний должен быть около 5 или / и является гением. моя 4-летняя дочь еще не может считать до 4. ну, если честно, скажем, она едва наконец-то интегрировала существование «4». иначе до сих пор она всегда пропускала это. «1,2,3,5,6,7» была ее обычной последовательностью подсчета. Я попросил ее сложить вместе карандаши, и она справится с 1 + 2 = 3, заново оцифровав все заново. Я волнуюсь на самом деле ...: '(
Мех
простой, но эффективный подход.
PabTorre
6
O (кухонный пол) хаха - но разве это не O (n ^ 2)?
13
O (м²) я думаю :)
Виктор Меллгрен
1
@phuclv: в ответе говорится, что «для этого требуется пространство O (пол на кухне)». Но в любом случае, это случай, когда сортировка может быть достигнута в O (n) - смотрите это обсуждение .
Энтони Лабарр,
36

Не уверен, что это наиболее эффективное решение, но я бы перебрал все записи и использовал набор битов, чтобы запомнить, какие числа установлены, а затем проверил наличие 0 битов.

Мне нравятся простые решения - и я даже считаю, что это может быть быстрее, чем вычисление суммы или суммы квадратов и т. Д.

Крис Лерчер
источник
11
Я предложил этот очевидный ответ, но это не то, что хотел интервьюер. Я прямо сказал в вопросе, что это не тот ответ, который я ищу. Еще один очевидный ответ: сначала сортируй. Ни O(N)подсчет сортировка , ни O(N log N)сравнение рода не то , что я ищу, хотя они оба очень простых решений.
полигенасмазочные материалы
@polygenelubricants: я не могу найти, где вы сказали это в своем вопросе. Если вы считаете, что это результат, то второго прохода не будет. Сложность состоит в том (если мы считаем N постоянным, как предполагает интервьюер, говоря, что сложность «определена в k, а не N»), O (1), и если вам нужно построить более «чистый» результат, вы получить O (K), который является лучшим, что вы можете получить, потому что вам всегда нужно O (K), чтобы создать чистый результат.
Крис Лерчер
«Обратите внимание, что я не ищу очевидное основанное на множестве решение (например, используя набор битов». Второй последний абзац из исходного вопроса.
2010 г.,
9
@hmt: Да, вопрос был отредактирован несколько минут назад. Я просто даю ответ, который я ожидаю от собеседника ... Искусственное построение неоптимального решения (вы не можете превзойти время O (n) + O (k), независимо от того, что вы делаете), не ' Это не имеет смысла для меня - за исключением случаев, когда вы не можете позволить себе O (n) дополнительного места, но вопрос по этому вопросу не ясен.
Крис Лерчер
3
Я снова отредактировал вопрос, чтобы уточнить. Я ценю обратную связь / ответ.
полигенасмазочные материалы
33

Я не проверял математику, но подозреваю, что вычисления Σ(n^2)на одном и том же проходе, когда мы вычисляем Σ(n), предоставят достаточно информации для получения двух пропущенных чисел, Σ(n^3)а также, если их три, и так далее.

AakashM
источник
15

Проблема с решениями, основанными на суммах чисел, состоит в том, что они не учитывают стоимость хранения и работы с числами с большими показателями ... на практике, для того, чтобы он работал для очень больших n, использовалась бы библиотека больших чисел , Мы можем проанализировать использование пространства для этих алгоритмов.

Мы можем проанализировать временную и пространственную сложность алгоритмов sdcvvc и Dimitris Andreou.

Место хранения:

l_j = ceil (log_2 (sum_{i=1}^n i^j))
l_j > log_2 n^j  (assuming n >= 0, k >= 0)
l_j > j log_2 n \in \Omega(j log n)

l_j < log_2 ((sum_{i=1}^n i)^j) + 1
l_j < j log_2 (n) + j log_2 (n + 1) - j log_2 (2) + 1
l_j < j log_2 n + j + c \in O(j log n)`

Так l_j \in \Theta(j log n)

Общее количество использованного хранилища: \sum_{j=1}^k l_j \in \Theta(k^2 log n)

Используемое пространство: при условии, что вычисления a^jзанимают ceil(log_2 j)время, общее время:

t = k ceil(\sum_i=1^n log_2 (i)) = k ceil(log_2 (\prod_i=1^n (i)))
t > k log_2 (n^n + O(n^(n-1)))
t > k log_2 (n^n) = kn log_2 (n)  \in \Omega(kn log n)
t < k log_2 (\prod_i=1^n i^i) + 1
t < kn log_2 (n) + 1 \in O(kn log n)

Общее использованное время: \Theta(kn log n)

Если это время и пространство удовлетворительное, вы можете использовать простой рекурсивный алгоритм. Пусть b! I будет i-й записью в сумке, n числом чисел перед удалением и k количеством изъятий. В синтаксисе Haskell ...

let
  -- O(1)
  isInRange low high v = (v >= low) && (v <= high)
  -- O(n - k)
  countInRange low high = sum $ map (fromEnum . isInRange low high . (!)b) [1..(n-k)]
  findMissing l low high krange
    -- O(1) if there is nothing to find.
    | krange=0 = l
    -- O(1) if there is only one possibility.
    | low=high = low:l
    -- Otherwise total of O(knlog(n)) time
    | otherwise =
       let
         mid = (low + high) `div` 2
         klow = countInRange low mid
         khigh = krange - klow
       in
         findMissing (findMissing low mid klow) (mid + 1) high khigh
in
  findMising 1 (n - k) k

Используемое хранилище: O(k)для списка, O(log(n))для стека: O(k + log(n)) этот алгоритм более интуитивен, имеет ту же сложность времени и использует меньше места.

a1kmm
источник
1
+1, выглядит хорошо, но вы потеряли меня, переходя от строки 4 к строке 5 во фрагменте № 1 - не могли бы вы объяснить это дальше? Спасибо!
j_random_hacker
isInRangeэто O (log n) , а не O (1) : он сравнивает числа в диапазоне 1..n, поэтому он должен сравнивать O (log n) бит. Я не знаю, в какой степени эта ошибка влияет на остальную часть анализа.
jcsahnwaldt говорит GoFundMonica
14

Подожди минуту. Поскольку вопрос установлен, в сумке есть 100 чисел. Независимо от того, насколько велико k, проблему можно решить за постоянное время, потому что вы можете использовать набор и удалять числа из набора в самое большее 100 - k итераций цикла. 100 постоянна. Набор оставшихся чисел - ваш ответ.

Если мы обобщим решение для чисел от 1 до N, ничего не изменится, за исключением того, что N не является константой, поэтому мы находимся в O (N - k) = O (N) времени. Например, если мы используем набор битов, мы устанавливаем биты в 1 за O (N), перебираем числа, устанавливая биты в 0 по мере продвижения (O (Nk) = O (N)), а затем есть ответ.

Мне кажется, что интервьюер спрашивал вас, как распечатать содержимое окончательного набора за O (k), а не O (N). Ясно, что с установленным битом вам нужно пройти через все N битов, чтобы определить, следует ли вам печатать число или нет. Однако, если вы измените способ реализации набора, вы можете распечатать числа за k итераций. Это делается путем помещения чисел в объект, который будет храниться как в хэш-наборе, так и в двусвязном списке. Когда вы удаляете объект из хеш-набора, вы также удаляете его из списка. Ответы останутся в списке, который теперь имеет длину k.

JeremyP
источник
9
Этот ответ слишком прост, и мы все знаем, что простые ответы не работают! ;) Если серьезно, оригинальный вопрос, вероятно, должен подчеркивать O (k) пространство требования.
ДК.
Проблема не в том, что это просто, а в том, что вам придется использовать O (n) дополнительной памяти для карты. Проблема бюста у меня решена в постоянном времени и постоянной памяти
Mojo Risin
3
Могу поспорить, что вы можете доказать, что минимальное решение по крайней мере O (N). потому что меньше, будет означать, что вы даже не СМОТРИТЕ на некоторые числа, и так как порядок не указан, просмотр ВСЕХ номеров является обязательным.
v.oddou
Если мы посмотрим на входные данные как поток, а n слишком велико, чтобы держать его в памяти, то требование O (k) памяти имеет смысл. Мы все еще можем использовать хеширование: просто соберите k ^ 2 сегментов и используйте простой алгоритм суммирования для каждого из них. Это всего лишь k ^ 2 памяти, и можно использовать еще несколько блоков, чтобы получить высокую вероятность успеха.
Томас Але
8

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

  1. Разбейте набор относительно случайного центра pна разделы l, которые содержат числа, меньшие, чем центр, и rкоторые содержат числа, большие, чем центр.

  2. Определите, в каких разделах находятся 2 пропущенных числа, сравнив значение сводки с размером каждого раздела ( p - 1 - count(l) = count of missing numbers in lи n - count(r) - p = count of missing numbers in r)

  3. a) Если в каждом разделе пропущено одно число, используйте подход разностей сумм, чтобы найти каждое пропущенное число.

    (1 + 2 + ... + (p-1)) - sum(l) = missing #1 а также ((p+1) + (p+2) ... + n) - sum(r) = missing #2

    б) если один раздел отсутствует как число и раздел пуст, то недостающие цифры либо (p-1,p-2)или в (p+1,p+2) зависимости от того, какой из разделов отсутствуют число.

    Если в одном разделе пропущены 2 числа, но он не пуст, вернитесь к этому разделу.

При наличии только 2 пропущенных чисел этот алгоритм всегда отбрасывает хотя бы один раздел, поэтому он сохраняет O(n)среднюю сложность быстрого выбора. Точно так же с 3 пропущенными числами этот алгоритм также отбрасывает по крайней мере один раздел с каждым проходом (потому что как с 2 пропущенными числами, самое большее только 1 раздел будет содержать несколько пропущенных чисел). Тем не менее, я не уверен, насколько снижается производительность, когда добавляется больше пропущенных чисел.

Вот реализация, которая не использует разделение на месте, поэтому этот пример не соответствует требованию к пространству, но он иллюстрирует шаги алгоритма:

<?php

  $list = range(1,100);
  unset($list[3]);
  unset($list[31]);

  findMissing($list,1,100);

  function findMissing($list, $min, $max) {
    if(empty($list)) {
      print_r(range($min, $max));
      return;
    }

    $l = $r = [];
    $pivot = array_pop($list);

    foreach($list as $number) {
      if($number < $pivot) {
        $l[] = $number;
      }
      else {
        $r[] = $number;
      }
    }

    if(count($l) == $pivot - $min - 1) {
      // only 1 missing number use difference of sums
      print array_sum(range($min, $pivot-1)) - array_sum($l) . "\n";
    }
    else if(count($l) < $pivot - $min) {
      // more than 1 missing number, recurse
      findMissing($l, $min, $pivot-1);
    }

    if(count($r) == $max - $pivot - 1) {
      // only 1 missing number use difference of sums
      print array_sum(range($pivot + 1, $max)) - array_sum($r) . "\n";
    } else if(count($r) < $max - $pivot) {
      // mroe than 1 missing number recurse
      findMissing($r, $pivot+1, $max);
    }
  }

демонстрация

FuzzyTree
источник
Разделение множества похоже на использование линейного пространства. По крайней мере, это не будет работать в потоковом режиме.
Томас Але
@ThomasAhle см en.wikipedia.org/wiki/Selection_algorithm#Space_complexity . разбиение набора на место требует только O (1) дополнительного пространства, а не линейного пространства. В настройках потоковой передачи это будет O (k) дополнительное пространство, однако в первоначальном вопросе не упоминается потоковая передача.
FuzzyTree
Не напрямую, но он пишет: «Вы должны просканировать ввод в O (N), но вы можете захватить только небольшое количество информации (определяемой в терминах k, а не N)», что обычно является определением потоковой передачи. Перемещение всех чисел для разбиения на самом деле невозможно, если у вас нет массива размера N. Просто вопрос имеет много ответов, которые, кажется, игнорируют это ограничение.
Томас Але
1
Но, как вы говорите, производительность может уменьшиться по мере добавления новых чисел? Мы также можем использовать алгоритм линейного медианного времени, чтобы всегда получать идеальный разрез, но если числа k хорошо распределены в 1, ..., n, вам не придется проходить уровни логка "глубоко", прежде чем вы сможете сократить какие-то ветви?
Томас Але
2
Наихудшее время выполнения - это, действительно, nlogk, потому что вам нужно обрабатывать весь ввод в большинстве случаев, а затем это геометрическая последовательность (которая начинается не более чем с n элементами). Требования к пространству регистрируются, когда реализованы с простой рекурсией, но они могут быть сделаны O (1) путем запуска фактического быстрого выбора и обеспечения правильной длины каждого раздела.
Эму
7

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

void puzzle (int* data, int n, bool* extra, int k)
{
    // data contains n distinct numbers from 1 to n + k, extra provides
    // space for k extra bits. 

    // Rearrange the array so there are (even) even numbers at the start
    // and (odd) odd numbers at the end.
    int even = 0, odd = 0;
    while (even + odd < n)
    {
        if (data [even] % 2 == 0) ++even;
        else if (data [n - 1 - odd] % 2 == 1) ++odd;
        else { int tmp = data [even]; data [even] = data [n - 1 - odd]; 
               data [n - 1 - odd] = tmp; ++even; ++odd; }
    }

    // Erase the lowest bits of all numbers and set the extra bits to 0.
    for (int i = even; i < n; ++i) data [i] -= 1;
    for (int i = 0; i < k; ++i) extra [i] = false;

    // Set a bit for every number that is present
    for (int i = 0; i < n; ++i)
    {
        int tmp = data [i];
        tmp -= (tmp % 2);
        if (i >= even) ++tmp;
        if (tmp <= n) data [tmp - 1] += 1; else extra [tmp - n - 1] = true;
    }

    // Print out the missing ones
    for (int i = 1; i <= n; ++i)
        if (data [i - 1] % 2 == 0) printf ("Number %d is missing\n", i);
    for (int i = n + 1; i <= n + k; ++i)
        if (! extra [i - n - 1]) printf ("Number %d is missing\n", i);

    // Restore the lowest bits again.
    for (int i = 0; i < n; ++i) {
        if (i < even) { if (data [i] % 2 != 0) data [i] -= 1; }
        else { if (data [i] % 2 == 0) data [i] += 1; }
    }
}
gnasher729
источник
Ты хотел (data [n - 1 - odd] % 2 == 1) ++odd;?
Чарльз
2
Не могли бы вы объяснить, как это работает? Я не понимаю
Teepeemm
Решение было бы очень и очень простым, если бы я мог использовать массив (n + k) логических значений для временного хранения, но это недопустимо. Поэтому я переставляю данные, помещая четные числа в начало, а нечетные числа в конец массива. Теперь младшие биты из этих n чисел можно использовать для временного хранения, потому что я знаю, сколько существует четных и нечетных чисел, и могу восстановить младшие биты! Эти n битов и k дополнительных битов являются именно теми (n + k) логическими значениями, которые мне были нужны.
gnasher729
2
Это не сработало бы, если бы данные были слишком велики для хранения в памяти, а вы рассматривали их только как поток.
Очень вкусно,
Сложность пространства может быть O (1). В первом проходе вы обрабатываете все числа <(n - k) точно по этому алгоритму, без использования «extra». Во втором проходе вы снова очищаете биты четности и используете первые k позиций для индексации чисел (nk) .. (n).
Эму
5

Можете ли вы проверить, существует ли каждый номер? Если да, вы можете попробовать это:

S = сумма всех чисел в сумке (S <5050)
Z = сумма пропущенных чисел 5050 - S

если пропущенные числа, xа yзатем:

x = Z - y и
max (x) = Z - 1

Итак, вы проверяете диапазон от 1до max(x)и находите число

Илиан Илиев
источник
1
Что max(x)значит, когда xчисло?
Томас Але
2
он, вероятно, имеет в виду макс из набора чисел
JavaHopper
если бы у нас было более 2 чисел, это решение было бы
нарушено
4

Может быть, этот алгоритм может работать на вопрос 1:

  1. Предварительно вычислить xor первых 100 целых чисел (val = 1 ^ 2 ^ 3 ^ 4 .... 100)
  2. xor элементы, поскольку они продолжают поступать из входного потока (val1 = val1 ^ next_input)
  3. окончательный ответ = val ^ val1

Или даже лучше:

def GetValue(A)
  val=0
  for i=1 to 100
    do
      val=val^i
    done
  for value in A:
    do
      val=val^value 
    done
  return val

Этот алгоритм может быть фактически расширен для двух пропущенных чисел. Первый шаг остается прежним. Когда мы вызываем GetValue с двумя пропущенными числами, результатом будет a1^a2два пропущенных числа. Допустим

val = a1^a2

Теперь, чтобы отсеять a1 и a2 от val, мы берем любой установленный бит в val. Допустим, ithбит установлен в val. Это означает, что a1 и a2 имеют разную четность в ithпозиции бита. Теперь мы делаем еще одну итерацию для исходного массива и сохраняем два значения xor. Один для чисел, у которых установлен i-й бит, а другой - без установленного i-го бита. Теперь у нас есть две группы чисел, и она гарантированно a1 and a2будет лежать в разных группах. Теперь повторите то же самое, что мы сделали для нахождения одного пропущенного элемента в каждом ведре.

Bashrc
источник
Это только решает проблему k=1, верно? Но мне нравится использовать xorсверх суммы, кажется, немного быстрее.
Томас Але
@ThomasAhle Да. Я назвал это в своем ответе.
Bashrc
Правильно. У вас есть представление о том, каким может быть xor «второго порядка» для k = 2? Подобно использованию квадратов для суммы, мы могли бы "квадрат" для xor?
Томас Але
1
@ThomasAhle Модифицировано, чтобы работать на 2 недостающих номера.
Bashrc
это мой любимый способ :)
Роберт Кинг
3

Вы можете решить Q2, если у вас есть сумма обоих списков и произведение обоих списков.

(l1 - оригинал, l2 - модифицированный список)

d = sum(l1) - sum(l2)
m = mul(l1) / mul(l2)

Мы можем оптимизировать это, поскольку сумма арифметического ряда в n раз больше среднего первого и последнего членов:

n = len(l1)
d = (n/2)*(n+1) - sum(l2)

Теперь мы знаем, что (если a и b - удаленные числа):

a + b = d
a * b = m

Таким образом, мы можем изменить:

a = s - b
b * (s - b) = m

И умножить:

-b^2 + s*b = m

И переставить так, чтобы правая сторона была равна нулю

-b^2 + s*b - m = 0

Тогда мы можем решить с помощью квадратной формулы:

b = (-s + sqrt(s^2 - (4*-1*-m)))/-2
a = s - b

Пример кода Python 3:

from functools import reduce
import operator
import math
x = list(range(1,21))
sx = (len(x)/2)*(len(x)+1)
x.remove(15)
x.remove(5)
mul = lambda l: reduce(operator.mul,l)
s = sx - sum(x)
m = mul(range(1,21)) / mul(x)
b = (-s + math.sqrt(s**2 - (-4*(-m))))/-2
a = s - b
print(a,b) #15,5

Я не знаю сложности функций sqrt, Reduce и Sum, поэтому я не могу решить сложность этого решения (если кто-то знает, пожалуйста, прокомментируйте ниже.)

Туомас Лаакконен
источник
Сколько времени и памяти он использует для расчета x1*x2*x3*...?
Томас Але
@ThomasAhle Это O (n) -время и O (1) -пространство по длине списка, но на самом деле это больше, поскольку умножение (по крайней мере в Python) O (n ^ 1.6) -время по длине число и числа являются O (log n) -пространством по их длине.
Туомас Лаакконен
@ThomasAhle Нет, log (a ^ n) = n * log (a), поэтому у вас будет O (l log k) -пространство для хранения номера. Таким образом, учитывая список длины l и исходных чисел длины k, у вас будет O (l) -пространство, но постоянный коэффициент (log k) будет меньше, чем просто выписывает их все. (Я не думаю, что мой метод - это особенно хороший способ ответить на вопрос.)
Туомас Лаакконен,
3

Для Q2 это решение, которое немного более неэффективно, чем другие, но все еще имеет время выполнения O (N) и занимает пространство O (k).

Идея состоит в том, чтобы запустить оригинальный алгоритм два раза. В первом вы получите общее число, которое отсутствует, что дает вам верхнюю границу пропущенных чисел. Давайте назовем этот номер N. Вы знаете, что пропущенные два числа будут суммироваться до N, поэтому первое число может быть только в интервале, [1, floor((N-1)/2)]пока второе будет [floor(N/2)+1,N-1].

Таким образом, вы снова зацикливаетесь на всех числах, отбрасывая все числа, которые не включены в первый интервал. Те, которые, вы отслеживаете их сумму. Наконец, вы узнаете одно из двух пропущенных чисел и, в дополнение, второе.

У меня есть ощущение, что этот метод может быть обобщен, и, возможно, несколько запросов выполняются "параллельно" в течение одного прохода по входу, но я еще не выяснил, как.

Svalorzen
источник
Ахаха, да, это то же самое решение, которое я придумал для Q2, просто для вычисления суммы снова с учетом отрицательных значений для всех чисел ниже N / 2, но это даже лучше!
xjcl
2

Я думаю, что это можно сделать без каких-либо сложных математических уравнений и теорий. Ниже приведено предложение по решению проблемы сложности на месте и времени O (2n):

Предположения формы ввода:

Количество чисел в сумке = n

Количество пропущенных номеров = k

Числа в сумке представлены массивом длины n

Длина входного массива для алгоритма = n

Недостающие записи в массиве (числа, извлеченные из пакета) заменяются значением первого элемента в массиве.

Например. Изначально сумка выглядит как [2,9,3,7,8,6,4,5,1,10]. Если выбрано 4, значение 4 станет 2 (первый элемент массива). Таким образом, после того, как вынуть 4, сумка будет выглядеть как [2,9,3,7,8,6,2,5,1,10]

Ключ к этому решению состоит в том, чтобы пометить INDEX посещенного номера, отрицая значение в этом INDEX, когда массив пересекается.

    IEnumerable<int> GetMissingNumbers(int[] arrayOfNumbers)
    {
        List<int> missingNumbers = new List<int>();
        int arrayLength = arrayOfNumbers.Length;

        //First Pass
        for (int i = 0; i < arrayLength; i++)
        {
            int index = Math.Abs(arrayOfNumbers[i]) - 1;
            if (index > -1)
            {
                arrayOfNumbers[index] = Math.Abs(arrayOfNumbers[index]) * -1; //Marking the visited indexes
            }
        }

        //Second Pass to get missing numbers
        for (int i = 0; i < arrayLength; i++)
        {                
            //If this index is unvisited, means this is a missing number
            if (arrayOfNumbers[i] > 0)
            {
                missingNumbers.Add(i + 1);
            }
        }

        return missingNumbers;
    }
pickhunter
источник
Это использует слишком много памяти.
Томас Але
2

Существует общий способ обобщения потоковых алгоритмов, подобных этому. Идея состоит в том, чтобы использовать немного рандомизации, чтобы, как мы надеемся, «распределить» kэлементы по независимым подзадачам, где наш оригинальный алгоритм решает эту проблему для нас. Этот метод используется, среди прочего, при восстановлении разреженных сигналов.

Если все пропущенные числа были хэшированы в разные сегменты, ненулевые элементы массива теперь будут содержать пропущенные числа.

Вероятность того, что конкретная пара отправляется в одно и то же ведро, меньше, чем 1/uпо определению универсальной хеш-функции. Поскольку речь k^2/2идет о парах, мы имеем вероятность ошибки не более k^2/2/u=1/2. То есть мы добиваемся успеха с вероятностью не менее 50%, и если мы увеличиваем, uмы увеличиваем наши шансы.

Обратите внимание, что этот алгоритм занимает k^2 lognбиты пространства (нам нужно lognбитов на массив). Это соответствует пространству, требуемому для ответа @Dimitris Andreou (В частности, требование к пространству для полиномиальной факторизации, которая также бывает рандомизированной.) Этот алгоритм также имеет константу время на обновление, а не время kв случае сумм мощности.

Фактически, мы можем быть даже более эффективными, чем метод суммирования мощности, используя прием, описанный в комментариях.

Томас Ахле
источник
Примечание: мы также можем использовать xorв каждом контейнере, а не sum, если это быстрее на нашей машине.
Томас Але
Интересно, но я думаю, что это касается только ограничения пространства, когда k <= sqrt(n)- по крайней мере, если u=k^2? Предположим, что k = 11 и n = 100, тогда у вас будет 121 сегмент, и алгоритм в конечном итоге будет похож на массив из 100 битов, который вы проверяете при чтении каждого # из потока. Увеличение uувеличивает шансы на успех, но есть предел того, насколько вы можете увеличить его, прежде чем вы превысите ограничение пространства.
FuzzyTree
1
Проблема имеет больше смысла для nгораздо большего, чемk , я думаю, но на самом деле вы можете сэкономить место k lognс помощью метода, очень похожего на описанный хеширование, при этом сохраняя постоянные обновления времени. Он описан в gnunet.org/eppstein-set-reconciliation , как и метод суммы мощностей, но в основном вы хэшируете «два из k» сегментов с помощью сильной хэш-функции, такой как табулирование, которое гарантирует, что в некоторых сегментах будет только один элемент , Для декодирования вы идентифицируете этот
сегмент и удаляете
2

Очень простое решение Q2, на которое я удивляюсь, никто уже не ответил. Используйте метод из Q1, чтобы найти сумму двух пропущенных чисел. Обозначим его через S, тогда одно из пропущенных чисел меньше, чем S / 2, а другое больше, чем S / 2 (дух). Суммируйте все числа от 1 до S / 2 и сравните его с результатом формулы (аналогично методу в Q1), чтобы найти меньшее из пропущенных чисел. Вычтите это из S, чтобы найти большее пропущенное число.

Гилад Дойч
источник
Я думаю, что это то же самое, что и ответ Свалорзена , но вы объяснили это более хорошими словами. Есть идеи, как обобщить это на Qk?
Джон Макклейн
Извините, что пропустил другой ответ. Я не уверен, возможно ли обобщить его на $ Q_k $, поскольку в этом случае вы не можете привязать наименьший отсутствующий элемент к некоторому диапазону. Вы знаете, что некоторый элемент должен быть меньше, чем $ S / k $, но это может быть верно для нескольких элементов
Gilad Deutsch
1

Очень хорошая проблема. Я бы пошел за использование разницы набора для Qk. Многие языки программирования даже поддерживают его, как в Ruby:

missing = (1..100).to_a - bag

Возможно, это не самое эффективное решение, но я бы использовал его в реальной жизни, если бы столкнулся с такой задачей в этом случае (известные границы, низкие границы). Если бы набор чисел был очень большим, я бы, конечно, рассмотрел более эффективный алгоритм, но до этого мне было бы достаточно простого решения.

DarkDust
источник
1
Это занимает слишком много места.
Томас Але
@ThomasAhle: Почему вы добавляете бесполезные комментарии к каждому второму ответу? Что вы имеете в виду, что он использует слишком много места?
DarkDust
Потому что вопрос говорит, что «Мы не можем позволить себе дополнительное пространство, пропорциональное N.» Это решение делает именно это.
Томас Але
1

Вы можете попробовать использовать фильтр Блума . Вставьте каждый номер в сумке в цвету, затем итерируйте полный набор 1-k, пока не сообщите о каждом не найденном. Это может не найти ответ во всех сценариях, но может быть достаточно хорошим решением.

jdizzle
источник
Существует также фильтр подсчета Блума, который позволяет удалять. Затем вы можете просто добавить все числа и удалить те, которые вы видите в потоке.
Томас Але
Ха-ха, это, наверное, один из наиболее практичных ответов, но привлекает мало внимания.
ldog
1

Я бы использовал другой подход к этому вопросу и исследовал интервьюера для получения более подробной информации о более крупной проблеме, которую он пытается решить. В зависимости от проблемы и требований, окружающих ее, очевидное решение на основе множеств может быть правильным, а подход «генерировать список и выбирать через него» может и не быть.

Например, может случиться так, что интервьюер собирается отправлять nсообщения и ему нужно знать, kчто не привело к ответу, и ему нужно знать это за минимальное время после настенного времениn-k ответа. Давайте также скажем, что характер канала сообщений таков, что даже при работе на полную длину у вас есть достаточно времени для некоторой обработки между сообщениями без какого-либо влияния на время, необходимое для получения конечного результата после получения последнего ответа. Это время можно использовать для вставки некоторого идентифицирующего аспекта каждого отправленного сообщения в набор и удаления его по мере поступления каждого соответствующего ответа. Как только последний ответ получен, единственное, что нужно сделать, это удалить его идентификатор из набора, который в типичных реализациях принимаетO(log k+1), После этого набор содержит список kотсутствующих элементов, и дополнительная обработка не требуется.

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

Blrfl
источник
Будет ли это работать, если у вас есть только O (k ^ 2) дополнительной памяти?
Томас Але
1

Вы можете мотивировать решение, думая об этом с точки зрения симметрии (группы, на языке математики). Независимо от порядка набора чисел, ответ должен быть одинаковым. Если вы собираетесь использовать kфункции для определения отсутствующих элементов, вам следует подумать о том, какие функции имеют это свойство: симметричное. Функция s_1(x) = x_1 + x_2 + ... + x_nявляется примером симметричной функции, но есть и другие, более высокой степени. В частности, рассмотрим элементарные симметричные функции . Элементарная симметричная функция степени 2 является s_2(x) = x_1 x_2 + x_1 x_3 + ... + x_1 x_n + x_2 x_3 + ... + x_(n-1) x_nсуммой всех произведений двух элементов. Аналогично для элементарных симметрических функций степени 3 и выше. Они явно симметричны. Кроме того, оказывается, что они являются строительными блоками для всех симметричных функций.

Вы можете построить элементарные симметричные функции по ходу, отметив это s_2(x,x_(n+1)) = s_2(x) + s_1(x)(x_(n+1)). Дальнейшая мысль должна убедить вас в этом s_3(x,x_(n+1)) = s_3(x) + s_2(x)(x_(n+1))и так далее, чтобы их можно было вычислить за один проход.

Как нам определить, какие элементы отсутствовали в массиве? Подумайте о полиноме (z-x_1)(z-x_2)...(z-x_n). Он оценивает, 0если вы введете любое из чисел x_i. Расширяя полином, вы получаете z^n-s_1(x)z^(n-1)+ ... + (-1)^n s_n. Здесь также появляются элементарные симметричные функции, что неудивительно, поскольку многочлен должен оставаться прежним, если мы применим любую перестановку к корням.

Таким образом, мы можем построить многочлен и попытаться разложить его, чтобы выяснить, какие числа не входят в набор, как уже упоминали другие.

Наконец, если нас беспокоит переполнение памяти большими числами (n-й симметричный многочлен будет порядка 100!), мы можем выполнить эти вычисления, mod pгде pпростое число больше 100. В этом случае мы оцениваем многочлен mod pи находим, что оно снова вычисляет чтобы , 0когда вход представляет собой число в наборе, и она вычисляется в ненулевое значение , когда вход числа , не в наборе. Однако, как уже отмечалось, для получения значений из полинома во времени , которое зависит от k, не N, мы должны фактор многочлен mod p.

Эдвард Дулиттл
источник
1

Еще одним способом является использование остаточной фильтрации графов.

Предположим, у нас есть номера от 1 до 4, а 3 отсутствует. Бинарное представление следующее,

1 = 001b, 2 = 010b, 3 = 011b, 4 = 100b

И я могу создать потоковую диаграмму, как показано ниже.

                   1
             1 -------------> 1
             |                | 
      2      |     1          |
0 ---------> 1 ----------> 0  |
|                          |  |
|     1            1       |  |
0 ---------> 0 ----------> 0  |
             |                |
      1      |      1         |
1 ---------> 0 -------------> 1

Обратите внимание, что потоковый граф содержит х узлов, а х - количество битов. А максимальное количество ребер равно (2 * х) -2.

Таким образом, для 32-разрядного целого числа потребуется O (32) пробел или O (1) пробел.

Теперь, если я уберу емкость для каждого числа, начиная с 1,2,4, то у меня останется остаточный график.

0 ----------> 1 ---------> 1

Наконец, я запустите цикл, как показано ниже,

 result = []
 for x in range(1,n):
     exists_path_in_residual_graph(x)
     result.append(x)

Теперь результат resultсодержит числа, которые также не пропущены (ложное срабатывание). Но k <= (размер результата) <= n, когда kотсутствуют элементы.

Я пройду по данному списку в последний раз, чтобы отметить пропущенный результат или нет.

Таким образом, сложность времени будет O (n).

Наконец, можно уменьшить число ложноположительных (и пространство , необходимое), принимая узлы 00, 01, 11, 10вместо того , чтобы просто 0и 1.

Шува
источник
Я не понимаю вашу диаграмму. Что представляют собой узлы, ребра и числа? Почему одни края направлены, а другие нет?
Дейн
На самом деле я не совсем понимаю ваш ответ, можете уточнить еще?
Дейн
1

Вам, вероятно, нужно уточнить, что означает O (k).

Вот тривиальное решение для произвольного k: для каждого v в вашем наборе чисел накапливайте сумму 2 ^ v. В конце, цикл i от 1 до N. Если сумма поразрядно ANDed с 2 ^ i равна нулю, то i отсутствует. (Или численно, если пол суммы, деленной на 2 ^ i четен. Или sum modulo 2^(i+1)) < 2^i.)

Легко, правда? O (N) время, O (1) память, и это поддерживает произвольное k.

За исключением того, что вы вычисляете огромные числа, которые на реальном компьютере требуют пространства O (N). Фактически это решение идентично битовому вектору.

Таким образом, вы можете быть умным и вычислить сумму и сумму квадратов и сумму кубов ... вплоть до суммы v ^ k, и сделать причудливую математику, чтобы извлечь результат. Но это тоже большие цифры, поэтому возникает вопрос: о какой абстрактной модели работы идет речь? Сколько места в O (1) пространстве, и сколько времени нужно, чтобы сложить числа любого размера вам нужно?

sfink
источник
Хороший ответ! Одна маленькая вещь: «Если сумма по модулю 2 ^ я равна нулю, то я отсутствует» неверно. Но понятно, что предназначено. Я думаю, что "если сумма по модулю 2 ^ (i + 1) меньше, чем 2 ^ i, то я отсутствует" будет правильным. (Конечно, в большинстве языков программирования мы использовали бы сдвиг битов вместо вычисления по модулю. Иногда языки программирования немного более выразительны, чем обычные математические обозначения. :-))
jcsahnwaldt говорит GoFundMonica
1
Спасибо, вы абсолютно правы! Исправлено, хотя я был ленив и отклонился от математической записи ... о, и я тоже все испортил. Исправление снова ...
sfink
1

Вот решение, которое не полагается на сложную математику, как это делают ответы sdcvvc / Dimitris Andreou, не меняет входной массив, как это сделали caf и полковник Panic, и не использует набор огромных размеров, как Chris Lercher, JeremyP и многие другие сделали. По сути, я начал с идеи Свалорцена / Гилада Дойча для Q2, обобщил ее для общего случая Qk и реализовал в Java, чтобы доказать, что алгоритм работает.

Идея

Предположим, у нас есть произвольный интервал I, из которого мы знаем только то, что он содержит хотя бы одно из пропущенных чисел. После того, как один проход через входной массив, только глядя на цифры от I , можно получить как сумму S и величину Q недостающих чисел от I . Мы делаем это, просто уменьшая длину I каждый раз, когда мы встречаем число от I (для получения Q ), и уменьшая предварительно вычисленную сумму всех чисел в I на это число каждый раз (для получения S ).

Теперь мы смотрим на S и Q . Если Q = 1 , то это означает , что тогда я содержит только один из недостающих чисел, и это число явно S . Мы помечаем I как завершенный (в программе это называется «однозначным») и оставляем его для дальнейшего рассмотрения. С другой стороны, если Q> 1 , мы можем вычислить среднее A = S / Q недостающих чисел содержится в I . Поскольку все числа различны, по крайней мере , один из таких чисел строго меньше , чем А и по меньшей мере один строго больше , чем A . Теперь мы разобьем I в Ана два меньших интервала, каждый из которых содержит хотя бы одно пропущенное число. Обратите внимание, что не имеет значения, какому из интервалов мы назначаем A, если это целое число.

Мы делаем следующий проход массива, вычисляя S и Q для каждого из интервалов отдельно (но в том же проходе), и после этого отмечаем интервалы с Q = 1 и разделяем интервалы с Q> 1 . Мы продолжаем этот процесс до тех пор, пока не появятся новые «неоднозначные» интервалы, то есть нам нечего делить, потому что каждый интервал содержит ровно одно пропущенное число (и мы всегда знаем это число, потому что мы знаем S ). Мы начинаем с единственного интервала «весь диапазон», содержащего все возможные числа (как [1..N] в вопросе).

Анализ сложности времени и пространства

Общее количество проходов p, которое мы должны сделать до тех пор, пока процесс не остановится, никогда не превысит число пропущенных чисел k . Неравенство p <= k может быть строго доказано. С другой стороны, существует также эмпирическая верхняя граница p <log 2 N + 3, которая полезна для больших значений k . Нам нужно выполнить бинарный поиск для каждого номера входного массива, чтобы определить интервал, к которому он принадлежит. Это добавляет множитель log k к сложности времени.

В общей сложности сложность по времени составляет O (N ≤ min (k, log N) ᛫ log k) . Обратите внимание, что для больших k это значительно лучше, чем для метода sdcvvc / Dimitris Andreou, который является O (N ᛫ k) .

Для своей работы алгоритм требует O (k) дополнительного пространства для хранения в большинстве k интервалов, что значительно лучше, чем O (N) в «битовых» решениях.

Реализация Java

Вот класс Java, который реализует вышеупомянутый алгоритм. Он всегда возвращает отсортированный массив пропущенных чисел. Кроме того, он не требует подсчета пропущенных чисел k, потому что вычисляет его при первом проходе. Весь диапазон чисел задается minNumberи maxNumberпараметры (например , 1 и 100 для первого примера в вопросе).

public class MissingNumbers {
    private static class Interval {
        boolean ambiguous = true;
        final int begin;
        int quantity;
        long sum;

        Interval(int begin, int end) { // begin inclusive, end exclusive
            this.begin = begin;
            quantity = end - begin;
            sum = quantity * ((long)end - 1 + begin) / 2;
        }

        void exclude(int x) {
            quantity--;
            sum -= x;
        }
    }

    public static int[] find(int minNumber, int maxNumber, NumberBag inputBag) {
        Interval full = new Interval(minNumber, ++maxNumber);
        for (inputBag.startOver(); inputBag.hasNext();)
            full.exclude(inputBag.next());
        int missingCount = full.quantity;
        if (missingCount == 0)
            return new int[0];
        Interval[] intervals = new Interval[missingCount];
        intervals[0] = full;
        int[] dividers = new int[missingCount];
        dividers[0] = minNumber;
        int intervalCount = 1;
        while (true) {
            int oldCount = intervalCount;
            for (int i = 0; i < oldCount; i++) {
                Interval itv = intervals[i];
                if (itv.ambiguous)
                    if (itv.quantity == 1) // number inside itv uniquely identified
                        itv.ambiguous = false;
                    else
                        intervalCount++; // itv will be split into two intervals
            }
            if (oldCount == intervalCount)
                break;
            int newIndex = intervalCount - 1;
            int end = maxNumber;
            for (int oldIndex = oldCount - 1; oldIndex >= 0; oldIndex--) {
                // newIndex always >= oldIndex
                Interval itv = intervals[oldIndex];
                int begin = itv.begin;
                if (itv.ambiguous) {
                    // split interval itv
                    // use floorDiv instead of / because input numbers can be negative
                    int mean = (int)Math.floorDiv(itv.sum, itv.quantity) + 1;
                    intervals[newIndex--] = new Interval(mean, end);
                    intervals[newIndex--] = new Interval(begin, mean);
                } else
                    intervals[newIndex--] = itv;
                end = begin;
            }
            for (int i = 0; i < intervalCount; i++)
                dividers[i] = intervals[i].begin;
            for (inputBag.startOver(); inputBag.hasNext();) {
                int x = inputBag.next();
                // find the interval to which x belongs
                int i = java.util.Arrays.binarySearch(dividers, 0, intervalCount, x);
                if (i < 0)
                    i = -i - 2;
                Interval itv = intervals[i];
                if (itv.ambiguous)
                    itv.exclude(x);
            }
        }
        assert intervalCount == missingCount;
        for (int i = 0; i < intervalCount; i++)
            dividers[i] = (int)intervals[i].sum;
        return dividers;
    }
}

Справедливости ради, этот класс получает входные данные в виде NumberBagобъектов. NumberBagне разрешает изменение массива и произвольный доступ, а также подсчитывает, сколько раз массив был запрошен для последовательного обхода. Он также больше подходит для тестирования большого массива, чем Iterable<Integer>потому, что он позволяет избежать intупаковки примитивных значений и позволяет обернуть часть большого int[]для удобной подготовки теста. Это не трудно заменить, если это необходимо, с NumberBagпомощью int[]или Iterable<Integer>введите в findподписи, изменив два для лупов в нем в Foreach из них.

import java.util.*;

public abstract class NumberBag {
    private int passCount;

    public void startOver() {
        passCount++;
    }

    public final int getPassCount() {
        return passCount;
    }

    public abstract boolean hasNext();

    public abstract int next();

    // A lightweight version of Iterable<Integer> to avoid boxing of int
    public static NumberBag fromArray(int[] base, int fromIndex, int toIndex) {
        return new NumberBag() {
            int index = toIndex;

            public void startOver() {
                super.startOver();
                index = fromIndex;
            }

            public boolean hasNext() {
                return index < toIndex;
            }

            public int next() {
                if (index >= toIndex)
                    throw new NoSuchElementException();
                return base[index++];
            }
        };
    }

    public static NumberBag fromArray(int[] base) {
        return fromArray(base, 0, base.length);
    }

    public static NumberBag fromIterable(Iterable<Integer> base) {
        return new NumberBag() {
            Iterator<Integer> it;

            public void startOver() {
                super.startOver();
                it = base.iterator();
            }

            public boolean hasNext() {
                return it.hasNext();
            }

            public int next() {
                return it.next();
            }
        };
    }
}

тесты

Простые примеры, демонстрирующие использование этих классов, приведены ниже.

import java.util.*;

public class SimpleTest {
    public static void main(String[] args) {
        int[] input = { 7, 1, 4, 9, 6, 2 };
        NumberBag bag = NumberBag.fromArray(input);
        int[] output = MissingNumbers.find(1, 10, bag);
        System.out.format("Input: %s%nMissing numbers: %s%nPass count: %d%n",
                Arrays.toString(input), Arrays.toString(output), bag.getPassCount());

        List<Integer> inputList = new ArrayList<>();
        for (int i = 0; i < 10; i++)
            inputList.add(2 * i);
        Collections.shuffle(inputList);
        bag = NumberBag.fromIterable(inputList);
        output = MissingNumbers.find(0, 19, bag);
        System.out.format("%nInput: %s%nMissing numbers: %s%nPass count: %d%n",
                inputList, Arrays.toString(output), bag.getPassCount());

        // Sieve of Eratosthenes
        final int MAXN = 1_000;
        List<Integer> nonPrimes = new ArrayList<>();
        nonPrimes.add(1);
        int[] primes;
        int lastPrimeIndex = 0;
        while (true) {
            primes = MissingNumbers.find(1, MAXN, NumberBag.fromIterable(nonPrimes));
            int p = primes[lastPrimeIndex]; // guaranteed to be prime
            int q = p;
            for (int i = lastPrimeIndex++; i < primes.length; i++) {
                q = primes[i]; // not necessarily prime
                int pq = p * q;
                if (pq > MAXN)
                    break;
                nonPrimes.add(pq);
            }
            if (q == p)
                break;
        }
        System.out.format("%nSieve of Eratosthenes. %d primes up to %d found:%n",
                primes.length, MAXN);
        for (int i = 0; i < primes.length; i++)
            System.out.format(" %4d%s", primes[i], (i % 10) < 9 ? "" : "\n");
    }
}

Тестирование большого массива может быть выполнено следующим образом:

import java.util.*;

public class BatchTest {
    private static final Random rand = new Random();
    public static int MIN_NUMBER = 1;
    private final int minNumber = MIN_NUMBER;
    private final int numberCount;
    private final int[] numbers;
    private int missingCount;
    public long finderTime;

    public BatchTest(int numberCount) {
        this.numberCount = numberCount;
        numbers = new int[numberCount];
        for (int i = 0; i < numberCount; i++)
            numbers[i] = minNumber + i;
    }

    private int passBound() {
        int mBound = missingCount > 0 ? missingCount : 1;
        int nBound = 34 - Integer.numberOfLeadingZeros(numberCount - 1); // ceil(log_2(numberCount)) + 2
        return Math.min(mBound, nBound);
    }

    private void error(String cause) {
        throw new RuntimeException("Error on '" + missingCount + " from " + numberCount + "' test, " + cause);
    }

    // returns the number of times the input array was traversed in this test
    public int makeTest(int missingCount) {
        this.missingCount = missingCount;
        // numbers array is reused when numberCount stays the same,
        // just Fisher–Yates shuffle it for each test
        for (int i = numberCount - 1; i > 0; i--) {
            int j = rand.nextInt(i + 1);
            if (i != j) {
                int t = numbers[i];
                numbers[i] = numbers[j];
                numbers[j] = t;
            }
        }
        final int bagSize = numberCount - missingCount;
        NumberBag inputBag = NumberBag.fromArray(numbers, 0, bagSize);
        finderTime -= System.nanoTime();
        int[] found = MissingNumbers.find(minNumber, minNumber + numberCount - 1, inputBag);
        finderTime += System.nanoTime();
        if (inputBag.getPassCount() > passBound())
            error("too many passes (" + inputBag.getPassCount() + " while only " + passBound() + " allowed)");
        if (found.length != missingCount)
            error("wrong result length");
        int j = bagSize; // "missing" part beginning in numbers
        Arrays.sort(numbers, bagSize, numberCount);
        for (int i = 0; i < missingCount; i++)
            if (found[i] != numbers[j++])
                error("wrong result array, " + i + "-th element differs");
        return inputBag.getPassCount();
    }

    public static void strideCheck(int numberCount, int minMissing, int maxMissing, int step, int repeats) {
        BatchTest t = new BatchTest(numberCount);
        System.out.println("╠═══════════════════════╬═════════════════╬═════════════════╣");
        for (int missingCount = minMissing; missingCount <= maxMissing; missingCount += step) {
            int minPass = Integer.MAX_VALUE;
            int passSum = 0;
            int maxPass = 0;
            t.finderTime = 0;
            for (int j = 1; j <= repeats; j++) {
                int pCount = t.makeTest(missingCount);
                if (pCount < minPass)
                    minPass = pCount;
                passSum += pCount;
                if (pCount > maxPass)
                    maxPass = pCount;
            }
            System.out.format("║ %9d  %9d  ║  %2d  %5.2f  %2d  ║  %11.3f    ║%n", missingCount, numberCount, minPass,
                    (double)passSum / repeats, maxPass, t.finderTime * 1e-6 / repeats);
        }
    }

    public static void main(String[] args) {
        System.out.println("╔═══════════════════════╦═════════════════╦═════════════════╗");
        System.out.println("║      Number count     ║      Passes     ║  Average time   ║");
        System.out.println("║   missimg     total   ║  min  avg   max ║ per search (ms) ║");
        long time = System.nanoTime();
        strideCheck(100, 0, 100, 1, 20_000);
        strideCheck(100_000, 2, 99_998, 1_282, 15);
        MIN_NUMBER = -2_000_000_000;
        strideCheck(300_000_000, 1, 10, 1, 1);
        time = System.nanoTime() - time;
        System.out.println("╚═══════════════════════╩═════════════════╩═════════════════╝");
        System.out.format("%nSuccess. Total time: %.2f s.%n", time * 1e-9);
    }
}

Попробуйте их на Ideone

Джон Макклейн
источник
0

Я считаю , что у меня есть O(k)время и O(log(k))алгоритм пространства, учитывая , что у вас есть floor(x)и log2(x)функция для произвольно больших целых доступных:

У вас есть kдлинное целое число -бит (отсюда и log8(k)пробел), куда вы добавляете x^2, где x - это следующее число, которое вы найдете в сумке: s=1^2+2^2+...это занимает O(N)время (что не является проблемой для интервьюера). В конце вы получите j=floor(log2(s))самый большой номер, который вы ищете. Затем s=s-jи вы делаете снова выше:

for (i = 0 ; i < k ; i++)
{
  j = floor(log2(s));
  missing[i] = j;
  s -= j;
}

Теперь у вас обычно нет функций floor и log2 для 2756целых -битных чисел, а вместо этого для удвоений. Так? Проще говоря, для каждых 2 байтов (или 1, или 3, или 4) вы можете использовать эти функции, чтобы получить желаемые числа, но это добавляет O(N)фактор к временной сложности

CostasGR43
источник
0

Это может звучать глупо, но в первой представленной вам проблеме вам нужно будет увидеть все оставшиеся числа в сумке, чтобы фактически сложить их, чтобы найти пропущенное число, используя это уравнение.

Так что, поскольку вы видите все числа, просто найдите пропавший номер. То же самое касается двух чисел. Довольно просто, я думаю. Нет смысла использовать уравнение, когда вы видите числа, оставшиеся в сумке.

Стефан М
источник
2
Я думаю, что преимущество их суммирования состоит в том, что вам не нужно запоминать, какие цифры вы уже видели (например, нет необходимости в дополнительной памяти). В противном случае единственный вариант - сохранить набор всех увиденных значений, а затем выполнить итерацию по этому набору еще раз, чтобы найти тот, который отсутствует.
Дэн Тао
3
Этот вопрос обычно задают с оговоркой о сложности пространства O (1).
Сумма первых N чисел равна N (N + 1) / 2. Для N = 100 сумма = 100 * (101) / 2 = 5050;
28
0

Я думаю, что это можно обобщить так:

Обозначим S, M в качестве начальных значений для суммы арифметических рядов и умножения.

S = 1 + 2 + 3 + 4 + ... n=(n+1)*n/2
M = 1 * 2 * 3 * 4 * .... * n 

Я должен подумать о формуле для расчета этого, но это не главное. В любом случае, если отсутствует один номер, вы уже предоставили решение. Однако, если два числа отсутствуют, давайте обозначим новую сумму и общее кратное через S1 и M1, которые будут следующими:

S1 = S - (a + b)....................(1)

Where a and b are the missing numbers.

M1 = M - (a * b)....................(2)

Поскольку вы знаете S1, M1, M и S, вышеприведенное уравнение разрешимо найти a и b, пропущенные числа.

Теперь для трех пропавших чисел:

S2 = S - ( a + b + c)....................(1)

Where a and b are the missing numbers.

M2 = M - (a * b * c)....................(2)

Теперь ваше неизвестное равно 3, а у вас есть только два уравнения, из которых вы можете решить.

Jack_of_All_Trades
источник
Хотя умножение становится довольно большим. Кроме того, как вы обобщаете более 2 пропущенных чисел?
Томас Але
Я попробовал эти формулы на очень простой последовательности с N = 3 и пропущенными числами = {1, 2}. Я не работал, так как считаю, что ошибка в формулах (2), которые следует читать M1 = M / (a * b)(см. Этот ответ ). Тогда все работает нормально.
dma_k
0

Я не знаю, является ли это эффективным или нет, но я хотел бы предложить это решение.

  1. Вычислить xor из 100 элементов
  2. Вычислите xor из 98 элементов (после удаления 2 элементов)
  3. Теперь (результат 1) XOR (результат 2) дает вам xor двух пропущенных nos i..ea XOR b, если a и b являются пропущенными элементами
    4. Получите сумму пропущенных Nos с вашим обычным приближением формула суммы diff и скажем, что разница равна d.

Теперь запустите цикл, чтобы получить возможные пары (p, q), обе из которых лежат в [1, 100] и составляют сумму d.

Когда пара получена, проверьте, является ли (результат 3) XOR p = q и если да, то все готово.

Пожалуйста, поправьте меня, если я ошибаюсь, а также прокомментируйте сложность времени, если это правильно

user2221214
источник
2
Я не думаю, что сумма и xor однозначно определяют два числа. Выполнение цикла для получения всех возможных k-кортежей, суммирующих с d, занимает время O (C (n, k-1)) = O (n <sup> k-1 </ sup>), что для k> 2 плохо.
Teepeemm
0

Мы можем делать Q1 и Q2 в O (log n) большую часть времени.

Предположим, что наш memory chipсостоит из массива nчисла test tubes. А число xв пробирке представлено x milliliterхимически-жидким.

Предположим, наш процессор является laser light. Когда мы зажигаем лазер, он проходит все трубки перпендикулярно его длине. Каждый раз, когда он проходит через химическую жидкость, светимость уменьшается на 1. И прохождение света на определенной миллилитровой отметке является операцией O(1).

Теперь, если мы зажигаем наш лазер в середине пробирки и получаем светимость

  • равно предварительно рассчитанному значению (рассчитывается, когда не пропущено ни одного числа), тогда пропущенные числа больше, чем n/2.
  • Если наш вывод меньше, то по крайней мере одно пропущенное число меньше, чем n/2. Мы также можем проверить, уменьшается ли светимость 1или 2. если оно уменьшается, 1то одно пропущенное число меньше, n/2а другое больше n/2. Если оно уменьшается, 2то оба числа меньше, чем n/2.

Мы можем повторить вышеописанный процесс снова и снова, сузив нашу проблемную область. На каждом этапе мы уменьшаем домен вдвое. И, наконец, мы можем добраться до нашего результата.

Параллельные алгоритмы, которые стоит упомянуть (потому что они интересны),

  • сортировка по некоторому параллельному алгоритму, например, параллельное слияние может быть сделано во O(log^3 n)времени. И тогда пропущенный номер можно найти с помощью бинарного поиска по O(log n)времени.
  • Теоретически, если у нас есть nпроцессоры, то каждый процесс может проверить один из входов и установить некоторый флаг, который идентифицирует число (удобно в массиве). И на следующем шаге каждый процесс может проверить каждый флаг и, наконец, вывести номер, который не помечен. Весь процесс займет O(1)время. Это O(n)требует дополнительного места / памяти.

Обратите внимание, что двум параллельным алгоритмам, представленным выше, может потребоваться дополнительное пространство, как указано в комментарии .

Шува
источник
Хотя метод пробирки-лазера действительно интересен, я надеюсь, вы согласитесь, что он не очень хорошо подходит для инструкций по аппаратному обеспечению и поэтому вряд ли будет O(logn)на компьютере.
SirGuy
1
Что касается вашего метода сортировки, он потребует дополнительное пространство, которое зависит от времени Nи больше O(N)(с точки зрения его зависимости N), которое мы намерены сделать лучше, чем.
SirGuy
@SirGuy Я ценю ваше беспокойство по поводу концепции пробирки и стоимости параллельной обработки памяти. Мой пост - поделиться своими мыслями о проблеме. Процессоры GPU теперь делают возможной параллельную обработку. Кто знает, будет ли концепция пробирки доступной в будущем.
Шува