Мне недавно задали этот вопрос во время технической проверки телефона, и я не справился. Вопрос включен дословно ниже.
Создать
{2^i * 5^j | i,j >= 0}
отсортированную коллекцию. Непрерывно печатайте следующее наименьшее значение.Пример:
{ 1, 2, 4, 5, 8, 10...}
«Следующее наименьшее» заставляет меня думать, что речь идет о минимальной куче, но я действительно не знал, куда идти, и интервьюер не оказал никакой помощи.
У кого-нибудь есть советы, как решить такую проблему?
algorithms
Джастин Скилс
источник
источник
Ответы:
Давайте перефразируем проблему: выведите каждое число от 1 до бесконечности так, чтобы число не имело факторов, кроме 2 и 5.
Ниже приведен простой фрагмент кода C #:
Подход Килиана / QuestionC гораздо более производительный. Фрагмент C # с этим подходом:
SortedSet
предотвращает повторные вставки.В основном это работает, гарантируя, что следующий номер в последовательности находится в
itms
.Доказательство того, что этот подход верен:
описанный алгоритм гарантирует, что после любого числа, выводимого в форму
2^i*5^j
, набор теперь содержит2^(i+1)*5^j
и2^i*5^(j+1)
. Предположим, что следующий номер в последовательности2^p*5^q
. Должно существовать ранее выведенное число вида2^(p-1)*5^(q)
или2^p*5^(q-1)
(или обоих, если ни p, ни q не равны 0). Если нет, то2^p*5^q
это не следующий номер, так как2^(p-1)*5^(q)
и2^p*5^(q-1)
оба меньше.Второй фрагмент использует
O(n)
память (где n - это число чисел, которые были выведены), посколькуO(i+j) = O(n)
(поскольку i и j оба меньше, чем n), и найдет n чисел воO(n log n)
времени. Первый фрагмент находит числа в экспоненциальном времени.источник
1 = 2^0*5^0, 2 = 2^1*5^0, 4 = 2^2*5^0, 5 = 2^0*5^1, 8 = 2^3*5^0, 10 = 2^1*5^1
,.Remove()
и.Add()
будут вызывать плохое поведение сборщика мусора, или это выяснит?Это достаточно распространенный вопрос интервью, поэтому полезно знать ответ. Вот соответствующая запись в моей личной кроватке:
Другими словами, вам нужен двухэтапный подход с дополнительным отсортированным буфером для эффективного решения этой проблемы. (Хорошее длинное описание в « Взломе кодирования » Гейл Макдауэлл.
источник
Вот ответ, который работает с постоянной памятью, за счет процессора. Это не очень хороший ответ в контексте исходного вопроса (т.е. ответ во время интервью). Но если интервью длится 24 часа, то это не так уж плохо. ;)
Идея состоит в том, что если у меня есть n, который является действительным ответом, то следующий в последовательности будет n умножить на некоторую степень двух, деленную на некоторую степень 5. Или иначе n раз степень 5, деленную на сила двух. При условии, что он делится равномерно. (... или делитель может быть 1;) в этом случае вы просто умножаете на 2 или 5)
Например, чтобы перейти от 625 к 640, умножьте на 5 ** 4/2 ** 7. Или, в более общем случае, умножьте на некоторое значение
2 ** m * 5 ** n
для некоторого m, n, где один положительный, а другой отрицательный или ноль, и множитель делит число равномерно.Теперь самое сложное - найти множитель. Но мы знаем, что а) делитель должен делить число равномерно, б) множитель должен быть больше единицы (числа продолжают расти), и в) если мы выберем наименьший множитель больше 1 (то есть 1 <f <все остальные f ), то это будет нашим следующим шагом. Шаг после этого будет самым низким шагом.
Неприятная часть находит значение m, n. Есть только log (n) возможности, потому что есть только 2 или 5, которые нужно сдать, но мне пришлось добавить коэффициент от -1 до +1, как неаккуратный способ борьбы с округлением. Таким образом, нам нужно только пройти через O (log (n)) каждый шаг. Так что это O (n log (n)) в целом.
Хорошей новостью является то, что, поскольку оно принимает значение и находит следующее значение, вы можете начать в любом месте последовательности. Поэтому, если вам нужен следующий после 1 миллиарда, он может просто найти его, перебирая 2/5 или 5/2 и выбирая наименьший множитель больше 1.
(Python)
Я проверил первые 10000 чисел, которые это генерирует, против первых 10000, сгенерированных решением с отсортированным списком, и это работает, по крайней мере, так далеко.
Кстати, следующий после триллиона, кажется, 1 024 000 000 000.
...
Гектометр Я могу получить производительность O (n) - O (1) на значение (!) - и использование памяти O (log n), рассматривая
best()
как справочную таблицу, которую я постепенно расширяю. Прямо сейчас он экономит память путем итерации каждый раз, но выполняет много избыточных вычислений. Держа эти промежуточные значения - и список минимальных значений - я могу избежать дублирования работы и значительно ускорить ее. Тем не менее, список промежуточных значений будет расти с ростом n, следовательно, O (log n) памяти.источник
n
иm
которые были использованы путем из чисел в последовательности до сих пор. На каждой итерации,n
илиm
может или не может идти вверх. Мы создаем новый номер, а2^(max_n+1)*5^(max_m+1)
затем уменьшаем этот номер исчерпывающим рекурсивным образом при каждом вызове, уменьшая показатель степени на 1, пока не получим минимум, который больше текущего номера. Мы уточняемmax_n
, поmax_m
мере необходимости. Это постоянный мем. МожноO(log^2(n))
запомнить, если в вызове сокращения используется кэш DPБрайан был абсолютно прав - мой другой ответ был слишком сложным. Вот более простой и быстрый способ сделать это.
Представьте себе Квадрант I евклидовой плоскости, ограниченный целыми числами. Назовите одну ось по оси i, а другую ось по оси j.
Очевидно, что точки рядом с началом координат будут выбраны раньше точек, удаленных от начала координат. Также обратите внимание, что активная область отойдет от оси i, прежде чем она отойдет от оси j.
Как только точка была использована, она больше никогда не будет использоваться. Точка может быть использована только в том случае, если точка, расположенная непосредственно под ней или слева от нее, уже была использована.
Собрав их вместе, вы можете представить себе «границу» или «передний край», который начинается вокруг начала координат, и они распространяются вверх и вправо, распространяясь вдоль оси i дальше, чем на оси j.
На самом деле, мы можем придумать что-то большее: для любой заданной i-величины будет самое большее одна точка на границе / крае. (Вы должны увеличивать i более чем в 2 раза, чтобы равняться приращению j.) Таким образом, мы можем представить границу в виде списка, содержащего один элемент для каждой координаты i, который изменяется только в зависимости от координаты j и значения функции.
Каждый проход мы выбираем минимальный элемент на переднем крае, а затем перемещаем его в направлении j один раз. Если нам случается поднять последний элемент, мы добавляем новый последний дополнительный элемент с увеличенным значением i и значением j, равным 0.
Пробел: O (n) в количестве напечатанных элементов.
Скорость: O (1) вставки, но это не каждый раз. (Иногда дольше, когда
List<>
должен расти, но все еще O (1) амортизируется). Большое сокращение времени - это поиск минимума O (n) в количестве напечатанных элементов.источник
Does anyone have advice on how to solve such a problem?
попытка понять основную проблему. Дамп кода плохо отвечает на этот вопрос.Решение, основанное на множестве, вероятно, было тем, что искал ваш интервьюер, однако, к сожалению, оно имеет следствие наличия
O(n)
памяти иO(n lg n)
общего времени дляn
элементов последовательности .Немного математики помогает нам найти решение в
O(1)
пространстве иO(n sqrt(n))
времени. Обратите внимание на это2^i * 5^j = 2^(i + j lg 5)
. Нахождение первыхn
элементов{i,j > 0 | 2^(i + j lg 5)}
сводится к нахождению первыхn
элементов,{i,j > 0 | i + j lg 5}
потому что функция(x -> 2^x)
строго монотонно возрастает, поэтому единственный путь для некоторыхa,b
это2^a < 2^b
еслиa < b
.Теперь нам просто нужен алгоритм, чтобы найти последовательность
i + j lg 5
, гдеi,j
натуральные числа. Другими словами, учитывая наше текущее значение тогоi, j
, что минимизирует следующий ход (т. Е. Дает нам следующее число в последовательности), это некоторое увеличение одного из значений (скажемj += 1
) наряду с уменьшением другого (i -= 2
). Единственное, что нас ограничивает, так этоi,j > 0
.Есть только два случая, чтобы рассмотреть -
i
увеличивается илиj
увеличивается. Один из них должен увеличиваться, так как наша последовательность увеличивается, и оба не увеличиваются, потому что в противном случае мы пропускаем термин, в котором у нас есть только один терминi,j
увеличение. Таким образом, один увеличивается, а другой остается неизменным или уменьшается. Выраженный в C ++ 11, весь алгоритм и его сравнение с заданным решением доступны здесь .Это обеспечивает постоянную память, поскольку в методе выделяется только постоянное количество объектов, кроме выходного массива (см. Ссылку). Метод достигает логарифмического времени на каждой итерации, поскольку для любой заданной точки
(i,j)
он пересекает лучшую пару(a, b)
, что(i + a, j + b)
является наименьшим увеличением значенияi + j lg 5
. Это обходO(i + j)
:Каждая итерация пытается обновить
i
, а затемj
идет с меньшим обновлением из двух.Так как
i
иj
самое большееO(sqrt(n))
, у нас есть общееO(n sqrt(n))
время.i
иj
расти со скоростью квадрата с техn
пор для любых максимальных значений,imax
иjmax
существуютO(i j)
уникальные пары, из которых можно составить нашу последовательность, если наша последовательность являетсяn
членами,i
иj
расти в некотором постоянном множителе друг друга (поскольку показатель степени состоит из линейного сочетание для двух), мы знаем, чтоi
иj
естьO(sqrt(n))
.В отношении ошибки с плавающей запятой беспокоиться не стоит - поскольку условия растут экспоненциально, нам придется иметь дело с переполнением, прежде чем ошибка флопа догонит нас на несколько величин. Я добавлю больше обсуждений к этому, если у меня будет время.
источник