Питання про інтерв'ю стало складніше: задані числа 1..100, знайдіть числа, які відсутні, вказані саме k, відсутні


1146

Я мав цікавий досвід співбесіди. Питання почалося дуже просто:

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

Звичайно, я чула це запитання про інтерв'ю, тому дуже швидко відповіла:

А1 : Ну, сума чисел 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 пропущені числа.


7
@polygenelubricants Дякую за роз'яснення. "Я шукаю алгоритм, який використовує час O (N) та простір O (K), де K - кількість відсутніх чисел", було б зрозуміло з самого початку ;-)
Дейв О.

7
У заяві Q1 ви повинні точно вказати, що ви не можете отримати доступ до номерів за порядком. Це, мабуть, здається вам очевидним, але я жодного разу не чув про це питання, і термін "сумка" (що означає "мультисети") був дещо заплутаним.
Jérémie

7
Будь ласка, прочитайте наступне, оскільки відповіді, подані тут, смішні: stackoverflow.com/questions/4406110/…

18
Рішення підсумовування чисел вимагає простору журналу (N), якщо ви не вважаєте, що вимога простору для необмеженого цілого числа є O (1). Але якщо ви допускаєте необмежені цілі числа, у вас є стільки місця, скільки ви хочете, лише з одним цілим числом.
Udo Klein

3
До речі, досить приємним альтернативним рішенням Q1 може бути обчислення XORвсіх чисел від 1до n, а потім xoring результат з усіма числами в даному масиві. Зрештою, у вас є ваш номер, який не вистачає. У цьому рішенні вам не потрібно дбати про переповнення, як при підбитті підсумків.
sbeliakov

Відповіді:


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

...

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

Якщо розширити многочлен (xa 1 ) ... (xa k ), коефіцієнти будуть рівно c 1 , ..., c k - див . Формули Віте . Оскільки всі поліноміальні фактори однозначно (кільце многочленів є евклідовим доменом ), це означає, що i 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.

EDIT: У попередній версії цієї відповіді було зазначено, що замість Z q , де q є простим, можна використовувати скінченне поле характеристики 2 (q = 2 ^ (log n)). Це не так, оскільки формули Ньютона вимагають ділення на числа до k.


6
Вам не потрібно використовувати просте поле, ви також можете використовувати q = 2^(log n). (Як ви зробили супер- та підписки ?!)
Генріх Апфельмус

49
+1 Це дійсно, дуже розумно. У той же час сумнівно, чи варто докладати зусиль, чи можна (частину) вирішення цілком штучної проблеми використовувати повторно. І навіть якби це була реальна світова проблема, на багатьох платформах найтривітніше O(N^2)рішення, ймовірно, перевершить цю красу навіть на досить високу N. Змушує задуматися над цим: tinyurl.com/c8fwgw Тим не менш, чудова робота! Я б не мав терпіння
пролазити

167
Я думаю, що це чудова відповідь. Я думаю, це також ілюструє, наскільки бідним питання інтерв'ю було б розширити пропущені цифри понад один. Навіть перший - це готчя, але це досить поширено, що в основному видно, що "ви зробили попередню підготовку до інтерв'ю". Але сподіватися на те, що КС мажор знає, що виходить за рамки k = 1 (особливо "на місці" в інтерв'ю) - трохи дурно.
corsiKa

5
Це ефективно робить кодування Ріда Соломона на вході.
Девід Ерман

78
Я ставлю на облік, що введення всього числа в а hash setта повторення 1...Nнабору за допомогою пошуку, щоб визначити, чи відсутні цифри, було б найбільш загальним, найшвидшим у середньому щодо kваріацій, найбільш налагоджуваним, найбільш рентабельним та зрозумілим рішенням. Звичайно, математичний шлях вражає, але десь на цьому шляху ти повинен бути інженером, а не математиком. Особливо, коли бере участь бізнес.
v.oddou

243

Ви знайдете це, прочитавши пару сторінок Мутукришнан - Алгоритми потоку даних: Пазл 1: Пошук відсутніх чисел . Він показує саме те узагальнення, яке ви шукаєте . Напевно, саме це читав ваш інтерв'ю і чому він ставив ці питання.

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


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


Ооо ... Це цікаво. Я мушу визнати, що мене трохи збентежили математики, але я jsut його пропустив. Можна залишити його відкритим, щоб подивитися на це пізніше. :) І +1, щоб покращити це посилання. ;-)
Кріс

2
Посилання на книги Google не працює для мене. Тут краща версія [PostScript File].
Генріх Апфельмус

9
Ого. Я не очікував, що це стане схвалено! Останній раз я розмістив посилання на рішення (Кнут, в цьому випадку) , а не намагатися вирішити це сам, він був на самому ділі downvoted: stackoverflow.com/questions/3060104 / ... бібліарія в мені радіє, спасибі :)
Дімітріс 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

Що ми можемо потім вирішити, щоб визначити наші відсутні числа.


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 є унікальним для даної суми. Сума квадратів (a + b) ** 2 + (ab) ** 2 = 2 * (a 2 + b 2). Для заданої суми K1 + K2 доданок 2 є фіксованим, і ми бачимо, що сума квадратів буде унікальною завдяки b b члена. Тому значення x і y є унікальними для пари цілих чисел.
phkahler

8
Це круто. @ user3281743 ось приклад. Нехай пропущені числа (k1 і k2) дорівнюють 4 і 6. Сума (1 -> 10) = 55 та сума (1 ^ 2 -> 10 ^ 2) = 385. Тепер нехай x = 55 - (Сума (Усі числа, що залишилися) )) і 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. І я думаю, що ви створюєте цілий масив, більше пам'яті зайнято.
Фокс

5
"Встановлення бітів і підрахунок місць, де біт дорівнює 0" вимагає O (n) додаткового простору, це рішення показує, як використовувати O (k) додатковий простір.
caf

7
Не працює з потоками в якості вхідних даних і не змінює вхідний масив (хоча це мені дуже подобається, і ідея є плідною).
comco

3
@ v.oddou: Ні, це добре. Зміна зміниться A[i], це означає, що наступна ітерація не буде порівнювати ті самі два значення, що і попередня. Новий A[i]буде таким же, як і останній цикл A[A[i]], але новий A[A[i]]буде новим значенням. Спробуйте і подивіться.
caf

128

Я попросив 4-річного віку вирішити цю проблему. Він відсортував числа, а потім порахував. У ній є необхідний простір (кухонний підлогу), і він працює так само просто, проте багато куль не вистачає.


20
;) твій 4-річний чоловік повинен наближатися до 5 років та / і є генієм. моя 4-річна дочка ще не може правильно порахувати до 4-х. Ну, щоб бути справедливим, скажімо, що вона ледве остаточно інтегрувала існування "4". інакше до цих пір вона завжди пропускала це. "1,2,3,5,6,7" була її звичайною послідовністю підрахунку. Я попросив її додати олівці разом, і вона керуватиме 1 + 2 = 3 шляхом повторного числення з нуля. Я переживаю насправді ...: '(
мех

простий, але ефективний підхід.
PabTorre

6
О (підлога кухні) ха-ха - але хіба це не буде 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).
Кріс Лерчер

"Зауважте, що я не шукаю очевидного рішення на основі набору (наприклад, використання набору бітів". Другий останній абзац з оригінального питання.
hrnt

9
@hmt: Так, питання було відредаговано кілька хвилин тому. Я просто даю відповідь, що я б очікував від опитуваного ... Штучне конструювання неоптимального рішення (ви не можете перемогти час O (n) + O (k), незалежно від того, що ви робите) я не маю сенсу для мене - за винятком випадків, коли ви не можете дозволити додатковому простору O (n), але питання не є явним.
Кріс Лерчер

3
Я ще раз відредагував це запитання для подальшого уточнення. Я вдячний за відгук / відповідь.
полігенмастильні речовини

33

Я не перевіряв математику, але підозрюю, що обчислення Σ(n^2)в тому ж проході, що й ми обчислюємо, Σ(n)дало б достатньо інформації, щоб отримати два пропущені числа. Зробіть Σ(n^3)добре, якщо їх є три тощо.


15

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

Ми можемо проаналізувати часову та просторову складність алгоритмів sdcvvc та Димитріса Андреу.

Зберігання:

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)) Цей алгоритм більш інтуїтивний, має однакову складність у часі та використовує менше місця.


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.


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. Визначте, у яких розділах знаходяться два пропущені числа, порівнявши значення зведення з розміром кожного розділу ( p - 1 - count(l) = count of missing numbers in lі n - count(r) - p = count of missing numbers in r)

  3. а) Якщо в кожному розділі відсутнє одне число, тоді використовуйте підхід різниці сум, щоб знайти кожне пропущене число.

    (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) залежать від того, в якому розділі відсутні цифри.

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

Маючи лише два пропущені числа, цей алгоритм завжди відкидає принаймні один розділ, тому він зберігає O(n)середню часову складність швидкого вибору. Аналогічно, з 3 відсутніми номерами цей алгоритм також відкидає принаймні один розділ з кожним проходом (оскільки, як і для двох пропущених номерів, щонайменше лише 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);
    }
  }

Демо


Розбиття набору подібно використанню лінійного простору. Принаймні, це не працюватиме в потоковому режимі.
Томас Ейл

@ThomasAhle см en.wikipedia.org/wiki/Selection_algorithm#Space_complexity . розділення набору на місце вимагає лише O (1) додаткового простору - не лінійного простору. У налаштуваннях потокової передачі це був би додатковий простір O (k), проте в початковому питанні потоку не згадується.
FuzzyTree

Не безпосередньо, але він пише "ви повинні сканувати вхід в O (N), але ви можете захоплювати лише невеликий об'єм інформації (визначений з точки зору k не N)", що зазвичай є визначенням потокового передачі. Переміщення всіх номерів для розділення насправді неможливо, якщо ви не маєте масив розміром N. Просто на запитання є багато відповідей, які, здається, ігнорують це обмеження.
Thomas Ahle

1
Але, як ви кажете, продуктивність може зменшуватися, коли додається більше чисел? Ми також можемо використовувати лінійний алгоритм часової медіани, щоб завжди отримати ідеальний розріз, але якщо k-числа добре розкладені через 1, ..., n, вам не доведеться робити рівні логіки "глибоко", перш ніж ви зможете підрізати будь-які гілки?
Thomas Ahle

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; }
    }
}

Ти хотів (data [n - 1 - odd] % 2 == 1) ++odd;?
Чарльз

2
Чи можете ви пояснити, як це працює? Я не розумію.
Teepeemm

Рішення було б дуже, дуже простим, якби я міг використовувати масив (n + k) булевих даних для тимчасового зберігання, але це не дозволено. Тому я переставляю дані, ставлячи парні числа на початку, і непарні числа в кінці масиву. Тепер найнижчі біти цих n чисел можна використовувати для тимчасового зберігання, бо я знаю, скільки парних і непарних чисел є і можу відновити найнижчі біти! Ці n біт і k додаткові біти - саме ті (n + k) булеви, які мені були потрібні.
gnasher729

2
Це не спрацювало, якби дані були занадто великими, щоб зберігати в пам'яті, і ви бачили їх лише як потік. Дуже смачно хакі :)
Thomas Ahle

Складність простору може бути 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число?
Thomas Ahle

2
він, мабуть, означає макс із набору чисел
JavaHopper

якщо у нас більше двох номерів, це рішення було б
розбито

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


Це вирішує лише проблему 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, функцій зменшення та суми, тому я не можу розробити складність цього рішення (якщо хтось знає, будь ласка, прокоментуйте його нижче.)


Скільки часу та пам'яті використовується для обчислення x1*x2*x3*...?
Thomas Ahle

@ThomasAhle Це O (n) -time та O (1) -простір у довжині списку, але насправді це більше, ніж множення (принаймні в Python) є O (n ^ 1.6) -time на довжину списку число і числа 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].

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

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


Ага, так, це те саме рішення, що я придумав і для Q2, просто з підрахунком суми знову з урахуванням негативів для всіх чисел нижче N / 2, але це ще краще!
xjcl

2

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

Припущення форми введення:

число чисел у мішку = n

число пропущених чисел = k

Цифри в сумці представлені масивом довжиною n

Довжина вхідного масиву для algo = 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;
    }

Для цього використовується занадто багато пам'яті.
Thomas Ahle

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-reconciasation , як метод суми повноважень, але в основному ви маєте хеш-вершину до двох з k із сильною функцією хеш-пам'яті, як хешування табуляції, що гарантує, що у деякому відрі буде лише один елемент . Щоб розшифрувати, ви ідентифікуєте це відро і вилучаєте елемент з обох його відер, який (ймовірно) звільняє ще одне відро і так далі
Thomas Ahle

2

Дуже просте рішення Q2, на яке я дивуюсь, ніхто вже не відповів. Використовуйте метод з Q1, щоб знайти суму двох пропущених чисел. Позначимо його через S, тоді одне з пропущених чисел менше S / 2, а інше більше S / 2 (duh). Підсумуйте всі числа від 1 до S / 2 і порівняйте їх з результатом формули (аналогічно методу в Q1), щоб знайти нижчі між пропущеними числами. Відніміть його від S, щоб знайти більше пропущеного числа.


Я думаю, що це те саме , що відповідь Свалорзена , але ви пояснили це кращими словами. У вас є ідея, як узагальнити його на Qk?
Джон Макклайн

Вибачте, що пропустили іншу відповідь. Я не впевнений, чи можливо узагальнити його до $ Q_k $, оскільки в цьому випадку ви не можете прив’язати найменший елемент, що відсутній, до деякого діапазону. Ви знаєте, що якийсь елемент повинен бути меншим за $ S / k $, але це може бути правдою для декількох елементів
Gilad Deutsch

1

Дуже приємна проблема. Я б хотів використовувати різницю наборів для Qk. Багато мов програмування навіть мають підтримку, як у Ruby:

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

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


1
Тут використовується занадто багато місця.
Томас Ейл

@ThomasAhle: Чому до кожної другої відповіді ви додаєте марні коментарі? Що ви маєте на увазі, якщо ви використовуєте занадто багато місця?
DarkDust

Тому що в запитанні сказано, що "ми не можемо дозволити собі додатковий простір, пропорційний Н." Це рішення робить саме це.
Томас Ейл

1

Ви можете спробувати використати фільтр Bloom . Вставте кожне число в сумку до цвітіння, потім повторіть повний набір 1-к до тих пір, поки кожен з них не знайдеться. Це не може знайти відповідь у всіх сценаріях, але може бути досить хорошим рішенням.


Є також фільтр підрахунку цвітіння, який дозволяє видаляти. Тоді ви можете просто додати всі числа та видалити ті, які ви бачите в потоці.
Томас Ейл

Ха-ха, це, мабуть, одна з більш практичних відповідей, але мало уваги отримує.
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(навіть якщо воно невідоме достроково), і в наведеному вище прикладі воно застосовувалося таким чином, що мінімізує найбільш критичний інтервал.


Це спрацювало б, якщо у вас є лише додаткова пам'ять 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

Зверніть увагу, що графік потоку містить x вузлів, а x - кількість бітів. А максимальна кількість ребер дорівнює (2 * x) -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) і скільки часу потрібно, щоб підбити підсумки чисел будь-якого розміру?


Гарна відповідь! Одна дрібниця: "Якщо сума по модулю 2 ^ i дорівнює нулю, то я відсутня" є невірною. Але зрозуміло, що призначено. Я думаю, "якщо сума модуля 2 ^ (i + 1) менше 2 ^ i, то я відсутня" було б правильним. (Звичайно, у більшості мов програмування ми б використовували бітове зміщення замість обчислення модуля. Іноді мови програмування дещо виразніші, ніж звичайні математичні позначення. :-))
jcsahnwaldt каже, що GoFundMonica

1
Спасибі, ви абсолютно праві! Виправлено, хоча я лінивий і відхилився від математичних позначень ... о, і я це теж зіпсував. Виправлення знову ...
sfink

1

Ось рішення, яке не покладається на складну математику, як це роблять відповіді sdcvvc / Димитріс Андреу, не змінює вхідний масив, як це робив caf і полковник Паніка, і не використовує біт величезних розмірів, як 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 . Тепер ми розділили І в Ана два менші інтервали, кожен з яких містить щонайменше одне відсутнє число. Зауважте, що не має значення, якому з інтервалів ми призначимо 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 / Димитріса Андреу, який є 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-bit цілих чисел, а замість парних. Тому? Просто для кожного 2 байта (або 1, або 3, або 4) ви можете використовувати ці функції, щоб отримати бажані числа, але це додає O(N)фактор часової складності


0

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

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


2
Я думаю, що користь їх підбиття полягає в тому, що вам не потрібно запам’ятовувати, які цифри ви вже бачили (наприклад, немає зайвої пам'яті). В іншому випадку єдиним варіантом є збереження набору всіх переглянутих значень, а потім повторне повторення цього набору, щоб знайти той, якого не вистачає.
Дан Дао

3
Зазвичай це питання задається з умовою складності простору O (1).

Сума перших N чисел дорівнює N (N + 1) / 2. Для N = 100, сума = 100 * (101) / 2 = 5050;
tmarthal

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, тоді як у вас просто два рівняння, з яких ви можете вирішити.


Хоча множення стає досить великим. Крім того, як ви узагальнюєте більш ніж 2 пропущені числа?
Thomas Ahle

Я спробував ці формули в дуже простій послідовності з N = 3 і відсутніми числами = {1, 2}. Я не працював, оскільки я вважаю, що помилка полягає у формулах (2), які слід прочитати M1 = M / (a * b)(див. Цю відповідь ). Тоді це прекрасно працює.
dma_k

0

Я не знаю, чи ефективно це чи ні, але я хотів би запропонувати це рішення.

  1. Обчисліть xor зі 100 елементів
  2. Обчисліть xor з 98 елементів (після видалення двох елементів)
  3. Тепер (результат 1) XOR (результат 2) дає вам xor двох відсутніх i.ea XOR b, якщо a і b є відсутніми елементами
    4. Отримайте суму відсутніх Nos при вашому звичному підході формула суми diff і дозволимо сказати, що diff є d.

Тепер запустіть цикл, щоб отримати можливі пари (p, q), які обидві лежать у [1, 100], і підсумовуйте d.

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

Будь ласка, виправте мене, якщо я помиляюся, а також прокоментуйте складність часу, якщо це правильно


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 роблять можливою паралельну обробку. Хто знає, якщо концепція пробірки не буде доступною в майбутньому.
shuva
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.