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

Эту проблему мне задали в интервью Amazon -

Учитывая массив положительных целых чисел, вам нужно найти наименьшее положительное целое число, которое не может быть образовано из суммы чисел из массива.

Пример:

Array:[4 13 2 3 1]
result= 11 { Since 11 was smallest positive number which can not be formed from the given array elements }


Я сделал следующее:

  1. отсортировал массив
  2. рассчитал сумму префикса
  3. Просмотрите массив суммы и проверьте, не превышает ли следующий элемент меньше чем сумма, то есть A [j] ‹= (сумма + 1). В противном случае ответ будет сумма + 1.

Но это было решение nlog (n).

Интервьюера это не удовлетворило, и он попросил решение менее чем за O (n log n) времени.


person user3187810    schedule 12.01.2014    source источник
comment
Вы хотите сказать, что интервьюер попросил решение O (logn)? Это, очевидно, невозможно, потому что вам нужно один раз просмотреть каждое значение массива, что займет как минимум O (n).   -  person interjay    schedule 12.01.2014
comment
Возможно, здесь нужно уточнить: возможно, наименьшее целое число больше нуля, которое не может быть создано путем суммирования любой комбинации элементов массива, возможно?   -  person DavidO    schedule 12.01.2014
comment
Он попросил решение меньше, чем nlog (n)   -  person user3187810    schedule 12.01.2014
comment
Да, это было наименьшее положительное число   -  person user3187810    schedule 12.01.2014
comment
Все ли элементы массива целые положительные? Может быть дубликаты?   -  person interjay    schedule 12.01.2014
comment
Да, все элементы массива являются положительными целыми числами   -  person user3187810    schedule 12.01.2014
comment
@interjay Да, элементы могут повторяться   -  person user3187810    schedule 12.01.2014
comment
Гарантирует ли спецификация задачи максимально возможное целочисленное значение, существенно меньшее, чем INT_MAX?   -  person DavidO    schedule 12.01.2014
comment
Набросок решения: начните с пустого красно-черного дерева. Для каждого элемента j входного массива: просмотрите дерево и для каждого узла n добавьте j + n к дереву, если он еще не находится в дереве, затем добавьте j к дереву. По завершении просмотрите дерево, проверяя, равен ли текущий узел последнему узлу +1. Если нет, вы нашли решение. Если вы дойдете до конца дерева, решением будет значение последнего листа + 1. Однако может быть лучшая структура данных, учитывая количество вставок.   -  person Conspicuous Compiler    schedule 12.01.2014
comment
@DavidO Да Значение будет соответствовать целочисленному диапазону, т.е. 10 ^ 9   -  person user3187810    schedule 12.01.2014
comment
Разве это не очень похоже на тот вопрос, который был задан вчера? stackoverflow .com / questions / 21060873 /   -  person Abhishek Bansal    schedule 12.01.2014
comment
@ConspicuousCompiler Будет ли ваше решение работать с повторяющимися элементами?   -  person user3187810    schedule 12.01.2014
comment
@ConspicuousCompiler Это решение - O (n ^ 2 * logn), вы добавляете O (n ^ 2) элементов в дерево, и каждое добавление - O (logn).   -  person interjay    schedule 12.01.2014
comment
Практически идентичный (закрытый) вопрос.   -  person Bernhard Barker    schedule 12.01.2014
comment
Может ли результат алгоритма быть 0? То есть вы считаете 0 положительным целым числом? Точно так же может 0 появиться в массиве?   -  person Apriori    schedule 12.01.2014
comment
@Rich Из примера, 0 - не наименьшее число, а 11, поэтому я думаю, что мы можем с уверенностью предположить, что это неверный ответ.   -  person Bernhard Barker    schedule 12.01.2014
comment
@Dukeling, спасибо, да, ты прав, я не рассмотрел этот пример достаточно долго.   -  person Apriori    schedule 13.01.2014
comment
@Rich Учитывая более подробное описание проблемы, которую я нашел в другом месте, 0 всегда включается в числа, которые могут быть сгенерированы, потому что законно не выбирать никаких элементов, что приводит к нулю. то есть: для заданного { 1 3 5 7 } выбор { } (пустой диапазон) возвращает 0. Так что это никогда не бывает невозможным числом.   -  person DavidO    schedule 13.01.2014
comment
@interjay Совершенно верно. Не очень хорошо оценил мое решение. Ваше здоровье!   -  person Conspicuous Compiler    schedule 15.01.2014


Ответы (4)


Рассмотрим все целые числа в интервале [2 i .. 2 i + 1 - 1]. И предположим, что все целые числа меньше 2 i могут быть образованы из суммы чисел из данного массива. Также предположим, что мы уже знаем C, который представляет собой сумму всех чисел ниже 2 i. Если C> = 2 i + 1 - 1, каждое число в этом интервале может быть представлено как сумма заданных чисел. В противном случае мы могли бы проверить, содержит ли interval [2 i .. C + 1] какое-либо число из заданного массива. А если такого числа нет, мы искали C + 1.

Вот набросок алгоритма:

  1. Для каждого входного числа определите, к какому интервалу он принадлежит, и обновите соответствующую сумму: S[int_log(x)] += x.
  2. Вычислить сумму префикса для массива S: foreach i: C[i] = C[i-1] + S[i].
  3. Отфильтруйте массив C, чтобы оставить только записи со значениями ниже следующей степени 2.
  4. Просканируйте входной массив еще раз и обратите внимание, какой из интервалов [2 i .. C + 1] содержит хотя бы одно входное число: i = int_log(x) - 1; B[i] |= (x <= C[i] + 1).
  5. Найдите первый интервал, который не отфильтрован на шаге №3 и соответствующий элемент B[] не установлен на шаге №4.

Если не очевидно, почему мы можем применить шаг 3, вот доказательство. Выберите любое число от 2 i до C, затем последовательно вычтите из него все числа ниже 2 i в порядке убывания. В конце концов мы получаем либо какое-то число меньше последнего вычтенного числа, либо ноль. Если результат равен нулю, просто сложите все вычтенные числа, и мы получим представление выбранного числа. Если результат не равен нулю и меньше последнего вычтенного числа, этот результат также меньше 2 i, поэтому он «представимый», и ни одно из вычтенных чисел не используется для его представления. Когда мы складываем эти вычтенные числа обратно, мы получаем представление выбранного числа. Это также предполагает, что вместо фильтрации интервалов по одному мы могли бы пропустить сразу несколько интервалов, перейдя непосредственно к int_log C.

Сложность времени определяется функцией int_log(), которая представляет собой целочисленный логарифм или индекс самого высокого установленного бита в числе. Если наш набор инструкций содержит целочисленный логарифм или любой его эквивалент (подсчет ведущих нулей или трюки с числами с плавающей запятой), то сложность равна O (n). В противном случае мы могли бы использовать некоторый битовый взлом, чтобы реализовать int_log() в O (log log U) и получить временную сложность O (n * log log U). (Здесь U - наибольшее число в массиве).

Если шаг 1 (помимо обновления суммы) также обновит минимальное значение в заданном диапазоне, шаг 4 больше не нужен. Мы могли бы просто сравнить C [i] с Min [i + 1]. Это означает, что нам нужен только один проход по входному массиву. Или мы могли бы применить этот алгоритм не к массиву, а к потоку чисел.

Несколько примеров:

Input:       [ 4 13  2  3  1]    [ 1  2  3  9]    [ 1  1  2  9]
int_log:       2  3  1  1  0       0  1  1  3       0  0  1  3

int_log:     0  1  2  3          0  1  2  3       0  1  2  3
S:           1  5  4 13          1  5  0  9       2  2  0  9
C:           1  6 10 23          1  6  6 15       2  4  4 13
filtered(C): n  n  n  n          n  n  n  n       n  n  n  n
number in
[2^i..C+1]:  2  4  -             2  -  -          2  -  -
C+1:              11                7                5

Для входных чисел с разной точностью этот подход требует времени O (n * log M) и пространства O (log M). Где M - наибольшее число в массиве. То же время нужно просто для чтения всех чисел (а в худшем случае нам понадобится каждый их бит).

Тем не менее, этот результат может быть улучшен до O (n * log R), где R - значение, найденное этим алгоритмом (фактически, его вариант, чувствительный к выходу). Единственная модификация, необходимая для этой оптимизации, состоит в том, чтобы вместо обработки целых чисел сразу обрабатывать их по цифрам: первый проход обрабатывает младшие биты каждого числа (например, биты 0..63), второй проход - следующие биты (например, 64..127) и т. Д. Мы можем игнорировать все старшие биты после нахождения результата. Также это уменьшает требования к пространству до O (K) чисел, где K - количество битов в машинном слове.

person Evgeny Kluev    schedule 12.01.2014
comment
Не могли бы вы объяснить, как это работает для {1 2 3 9} и {1 1 2 9} - person user3187810; 13.01.2014
comment
В ПОРЯДКЕ. Добавлено несколько примеров. - person Evgeny Kluev; 13.01.2014
comment
@EvgenyKluev Я смотрю на ваши примеры, я не могу понять, как рассчитывается ваша линия S :. В своем описании вы упоминаете сумму префикса, но это, конечно, не сумма префикса. - person Jonathan Mee; 09.01.2015
comment
@JonathanMee: на самом деле C - это сумма префикса, а не S. S [i] - это сумма значений из входного массива, имеющая целочисленный логарифм, равный i. C [i] - это сумма значений, имеющих целочисленный логарифм, меньший или равный i. - person Evgeny Kluev; 09.01.2015
comment
@EvgenyKluev Спасибо за объяснение, теперь я понимаю C и S. Но я снова застрял на шаге 3. Я не понимаю, что вы подразумеваете под следующей степенью двойки. - person Jonathan Mee; 09.01.2015
comment
@JonathanMee: каждый элемент массива C содержит сумму значений, имеющих целочисленный логарифм, меньший или равный некоторому значению. Другими словами, он рассматривает значения ниже некоторой степени 2. Следующая степень 2, очевидно, вдвое больше, и я имею в виду именно это значение. Например, если все значения ниже 16 представимы, а сумма всех значений ниже 16 находится между 16 и 32, мы должны искать входные значения между 16 и этой суммой + 1; но если эта сумма равна 32 или больше, все числа от 16 до 32 являются представимыми, поэтому не имеет значения, есть ли какие-либо входные данные от 16 до 32. - person Evgeny Kluev; 09.01.2015
comment
@EvgenyKluev Хорошо, теперь я понял алгоритм. Но мне трудно гарантировать, что это правда. Есть ли доказательство того, что мы уже знаем C, которое является суммой всех чисел ниже 2 * i *. Если C ›= 2 * i * + 1 - 1, каждое число в этом интервале может быть представлено как сумма заданных чисел. Почему мы так уверены в этом? - person Jonathan Mee; 10.01.2015
comment
@JonathanMee: Я также предполагаю (но забываю сказать это явно), что все числа ниже 2 ^ i представимы (потому что алгоритм проверяет эти интервалы последовательно, от наименьшего к наибольшему). Доказательство: выберите любое число от 2 ^ i до C, затем последовательно вычтите из него все числа ниже 2 ^ i в порядке убывания; в конце концов вы получите либо какое-то число меньше последнего вычтенного числа, либо ноль; в обоих случаях это означает, что выбранное число представимо. - person Evgeny Kluev; 10.01.2015
comment
@EvgenyKluev Если вы вычитаете из числа до тех пор, пока результат не станет отрицательным, как вы можете быть уверены, что сможете сформировать это число точно? Я знаю, что ругаю вас по этому поводу. Я задал здесь вопрос для пояснения: math.stackexchange.com/q/1099359/194115 - person Jonathan Mee; 11.01.2015
comment
@JonathanMee: Мы останавливаемся на шаг раньше, пока результат не станет отрицательным. Если результат равен нулю, просто сложите все вычтенные числа, и вы получите представление выбранного числа. Если результат не равен нулю и меньше последнего вычтенного числа, этот результат также меньше 2 ^ i, поэтому его можно представить, и ни одно из вычтенных чисел не используется для его представления. Когда вы добавляете эти вычтенные числа обратно, вы получаете представление выбранного числа. - person Evgeny Kluev; 11.01.2015
comment
@EvgenyKluev Вероятный вопрос по умолчанию к этому (здесь). Я реализовал ваш алгоритм здесь. Когда у вас будет время взглянуть на это, я прошу вашего благословения. Я считаю, что ваше решение правильное, и мне грустно, что вы не получили больше голосов. - person Jonathan Mee; 12.01.2015
comment
@JonathanMee: В последнее время я видел несколько похожих вопросов. Вероятно, это из-за недавнего конкурса на CodeChef. Что касается вашей реализации, я думаю, что копирование всех входных чисел в hashmap - не лучшая идея. Это значительно увеличивает пространство, необходимое для алгоритма. Обратите внимание, что для шага 4 нам не нужно обрабатывать числа в порядке int_log (это необходимо только для шага 5). Также unordered_multimap кажется слишком медленным для этой задачи; Я бы предпочел что-то вроде массива векторов. Кстати, найти минимальное значение для каждого интервала - хорошая идея. Только лучше делать на первом (и единственном) проходе. Смотрите мое обновление. - person Evgeny Kluev; 12.01.2015
comment
@EvgenyKluev Хотя я считаю, что найти минимальное число можно в одном цикле, однако вычисление суммы префиксов в одном цикле было бы ошибкой. Если бы ваши маленькие числа находились в конце вашего входного массива, вы могли бы в конечном итоге повторно обработать каждую сделанную вами сумму, что приведет вас к O (nlogn) времени. Что касается использования _1 _..., я считаю, что использование одного vector было бы трудным, требуя обновления всех индексов каждый раз при вставке элемента. Использование нескольких vector возможно, но вряд ли принесет большую экономию места по сравнению с multimap. - person Jonathan Mee; 12.01.2015
comment
@JonathanMee: Я бы предпочел массив векторов (один вектор для каждой битовой позиции) не из-за места (и для векторов, и для хэшей требуется слишком много места), а потому, что векторы (скорее всего) намного быстрее. Что касается суммы префикса, то здесь проблем нет. Массив S имеет один элемент для каждой битовой позиции (например, 64 элемента), он готов после обработки последнего числа из входного массива, только после этого вы вычисляете его префиксные суммы (вы даже можете повторно использовать тот же массив для префиксных сумм), без повторной обработки каждая сумма нужна. И прочий массив для минимумов. В конце вам понадобятся только эти 2 небольших массива, чтобы получить результат. - person Evgeny Kluev; 12.01.2015

Есть красивый алгоритм для решения этой проблемы за время O (n + Sort), где Sort - это количество времени, необходимое для сортировки входного массива.

Идея алгоритма состоит в том, чтобы отсортировать массив, а затем задать следующий вопрос: какое наименьшее положительное целое число вы не можете получить, используя первые k элементов массива? Затем вы просматриваете массив слева направо, обновляя свой ответ на этот вопрос, пока не найдете наименьшее число, которое вы не можете составить.

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

  • Если текущее число больше наименьшего числа, которое вы не можете составить до сих пор, тогда вы знаете наименьшее число, которое вы не можете составить - это то, которое вы записали, и все готово.
  • В противном случае текущее число меньше или равно наименьшему числу, которое вы не можете составить. Утверждают, что вы действительно можете сделать это число. Прямо сейчас вы знаете наименьшее число, которое вы не можете составить с первыми k элементами массива (назовите его candidate), и теперь смотрите на значение A[k]. Следовательно, число candidate - A[k] должно быть некоторым числом, которое вы действительно можете составить с первыми k элементами массива, поскольку в противном случае candidate - A[k] было бы меньшим числом, чем наименьшее число, которое вы якобы не можете составить с первыми k числами в массиве. Более того, вы можете создать любое число в диапазоне от candidate до candidate + A[k] включительно, потому что вы можете начать с любого числа в диапазоне от 1 до A[k] включительно, а затем добавить к нему candidate - 1. Поэтому установите candidate на candidate + A[k] и увеличьте k.

В псевдокоде:

Sort(A)
candidate = 1
for i from 1 to length(A):
   if A[i] > candidate: return candidate
   else: candidate = candidate + A[i]
return candidate

Вот тестовый прогон на [4, 13, 2, 1, 3]. Отсортируйте массив, чтобы получить [1, 2, 3, 4, 13]. Затем установите candidate в 1. Затем мы делаем следующее:

  • A[1] = 1, candidate = 1:
    • A[1] ≤ candidate, so set candidate = candidate + A[1] = 2
  • A[2] = 2, candidate = 2:
    • A[2] ≤ candidate, so set candidate = candidate + A[2] = 4
  • A[3] = 3, candidate = 4:
    • A[3] ≤ candidate, so set candidate = candidate + A[3] = 7
  • A[4] = 4, candidate = 7:
    • A[4] ≤ candidate, so set candidate = candidate + A[4] = 11
  • A[5] = 13, candidate = 11:
    • A[4] > candidate, so return candidate (11).

Итак, ответ - 11.

Время выполнения здесь - O (n + Sort), потому что вне сортировки время выполнения - O (n). Вы можете четко отсортировать по времени O (n log n) с помощью heapsort, и если вы знаете некоторую верхнюю границу чисел, вы можете отсортировать по времени O (n log U) (где U - максимально возможное число) с помощью сортировки по основанию. Если U - фиксированная константа (скажем, 10 9), то радиальная сортировка выполняется за время O (n), и весь этот алгоритм затем также выполняется за время O (n).

Надеюсь это поможет!

person templatetypedef    schedule 12.01.2014
comment
Он должен быть candidate = candidate + A[i] в else, без -1. Это точно такой же алгоритм, что и OP, но объяснение очень полезно. - person interjay; 12.01.2014
comment
это относится к решению nlogn. Я знал это, но не могли бы вы предоставить что-нибудь лучше, чем это? - person user3187810; 12.01.2014
comment
@ user3187810 - Это решение довольно быстрое - оно работает не хуже, чем за O (n log n), и, возможно, намного лучше, если вы можете отсортировать целые числа, используя что-то вроде сортировки по основанию. - person templatetypedef; 12.01.2014
comment
@interjay: Я обновил ответ. Я не понимал, когда писал это, что в итоге он был идентичен ответу OP. Теперь, когда я это понимаю, я думаю, что ответ по-прежнему полезен, поскольку он обеспечивает обоснование ответа, а также показывает, как его ускорить (а именно, улучшить этап сортировки). Однако, если вы считаете, что в этом нет необходимости, я могу удалить этот ответ. - person templatetypedef; 12.01.2014
comment
Я согласен, оставьте это здесь. Я поддержал это за объяснение. - person interjay; 12.01.2014
comment
@templatetypedef существует какое-то решение O (n), которое хотел знать интервьюер .. есть много решений nlog (n) для этого - person user3187810; 12.01.2014
comment
@ user3187810 - Если целые числа имеют фиксированную верхнюю границу (скажем, 10 ^ 9), вы можете отсортировать их за время O (n), используя сортировку с подсчетом или сортировку по основанию. Это снизит общее время выполнения до O (n). - person templatetypedef; 12.01.2014
comment
Если числа в массиве генерируются случайным образом, можно добиться статистически значимого улучшения, просто проверив, существует ли 1, перед выполнением остальной части алгоритма. - person neeKo; 13.01.2014
comment
Как мы узнаем, что кандидат A [k] + не может быть сделан с использованием от A [0] до A [k]? - person Ekalavya; 04.01.2019
comment
Мы не доказали, что кандидат A [0] + ... + A [k-1] ‹. Мы только знаем, что никакое подмножество A [0], ... A [k-1] не добавляет кандидата. - person Ekalavya; 05.01.2019
comment
Предположим, A [0] + ... + A [k-1] ›кандидат. В принципе, я мог бы отбросить несколько элементов этой суммы и добавить A [k] - это предположительно равный кандидат. Нет? Ваш аргумент применим, только если я отброшу один элемент и добавлю A [k]. - person Ekalavya; 05.01.2019
comment
@Ekalavya Подожди, не обращай внимания на мой предыдущий аргумент. Инвариант сильного цикла состоит в том, что (1) мы можем сделать любое число меньше candidate, используя A [0] - A [k-1], и (2) мы не можем сделать любое число равным candidate или выше, используя A [0] через A [k-1]. Итак, теперь предположим, что мы могли бы сделать candidate + A [k], используя A [0] - A [k]. Единственный способ сделать это - использовать A [k], поскольку мы не можем сделать ничего с candidate или выше, используя A [0] - A [k-1]. Итак, если мы затем удалим A [k] из этого суммирования, все, что останется, должно быть подмножеством от A [0] до A [k-1] в сумме до candidate, что невозможно. - person templatetypedef; 05.01.2019
comment
Теперь это правильно. Возможно, вам стоит изменить исходный ответ - если возможно. - person Ekalavya; 05.01.2019

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

Начните с пустого битового вектора b. Затем для каждого элемента k в вашем массиве сделайте следующее:

b = b | b << k | 2^(k-1)

Для ясности, i-й элемент установлен в 1, чтобы представить число i, а | k устанавливает k-й элемент в 1.

После того, как вы закончите обработку массива, вашим ответом будет индекс первого нуля в b (считая справа, начиная с 1).

  1. b=0
  2. процесс 4: b = b | b ‹---------------- 4 | 1000 = 1000
  3. процесс 13: b = b | b ‹закрыть 13 | 1000000000000 = 10001000000001000
  4. процесс 2: b = b | b ‹---------------- 2 | 10 = 1010101000000101010
  5. процесс 3: b = b | b ‹---------------- 3 | 100 = 1011111101000101111110
  6. процесс 1: b = b | b ‹---------------- 1 | 1 = 11111111111001111111111

Первый ноль: позиция 11.

person Dave    schedule 12.01.2014
comment
Обратите внимание, что это линейное время, ЕСЛИ операции с битовым вектором являются постоянным временем, что может и не быть. - person Dave; 13.01.2014
comment
Насколько мне известно, не существует компьютеров, поддерживающих побитовые операции с числами произвольной ширины в постоянное время. Это определенно крутая идея, но я не думаю, что это действительно O (n). - person templatetypedef; 13.01.2014
comment
@templatetypedef: Справедливый вопрос. OP ответил в комментариях, что целые числа гарантированно находятся в диапазоне [1,10 ^ 9], поэтому достаточно большой битовый вектор, чтобы занять это все пространство, может быть зарезервирован в постоянное время в начале. Даже без этого разрешения удвоение зарезервированного размера каждый раз, когда было превышено выделенное пространство, должно ограничивать вас выделением O (lg n). - person Conspicuous Compiler; 15.01.2014
comment
@DaveGalvin Это >> смена? Потому что это сдвиг вправо, а не влево. Даже если это сдвиг влево, я, должно быть, чего-то не понимаю, потому что на вашем шаге 3: 1|8192|1 не равно 8209. - person Jonathan Mee; 09.01.2015
comment
@JonathanMee, как сказано в ответе, индекс 1 находится слева ... Если вы хотите преобразовать это с младшим битом справа, как с целочисленной арифметикой (и у вас есть большие целые числа), это что-то вроде кода Smalltalk: (# (4 13 2 3 1) ввести: 0 в: [: b: k | b bitOr: (b ‹закрыть k bitOr: (1 ‹( k-1)))]) bitInvert lowBit 11 - person aka.nice; 20.04.2015
comment
@JonathanMee Я написал версию алгоритма для зеркальной вселенной! Удивительно, что никто об этом не уловил и не упомянул. Теперь это правильно. Спасибо! - person Dave; 01.11.2015

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

Quicksort O(n*logn) сделает всю работу за вас:

def smallestPositiveInteger(self, array): 
    candidate = 1
    n = len(array)
    array = sorted(array)
    for i in range(0, n):
        if array[i] <= candidate:
            candidate += array[i]
        else:
            break
    return candidate
person LITDataScience    schedule 19.04.2021