По условию задачи любое число этого ряда должно представлять собой произведение 3a · 5b · 7c.
Давайте посмотрим на список чисел, удовлетворяющих нашим требованиям:
Поскольку 3a-1 · 5b · 7c < 3a · 5b · 7c, то число 3a-1 · 5b · 7c должно попасть в список, как и все перечисленные далее числа:
- 3a-1 · 5 · 7c
- 3a · 5b-1 · 7c
- 3a · 5b · 7c-1
Другой способ — представить каждое число в следующем виде:
- 3 · (некоторое предыдущее число из числового ряда);
- 5 · (некоторое предыдущее число из числового ряда);
- 7 · (некоторое предыдущее число из числового ряда).
Мы знаем, что Ak можно записать как (3, 5 или 7) · (некоторое значение из { A1, …, Ak-1}). Мы также знаем, что Ak является следующим числом в данном ряду. Поэтому Ak должно быть наименьшим «новым» числом, которое может быть получено умножением каждого значения в списке на 3, 5 или 7.
Как найти Ak? Мы можем умножить каждое число в списке на 3, 5 или 7 и найти наименьший новый результат. Но такое решение потребует O(k2) времени. Неплохо, но можно сделать и лучше.
Вместо умножения всех элементов списка на 3, 5 или 7 можно рассматривать каждое предыдущее значение как основу для расчета трёх последующих значений. Таким образом, каждое число Ai может использоваться для формирования следующих форм:
- 3 · Ai
- 5 · Ai
- 7 · Ai
Эта идея поможет нам спланировать все заранее. Каждый раз, когда мы добавляем в список число Ai, мы держим значения 3Ai, 5Ai и 7Ai в «резервном» списке. Чтобы получить Ai+1, достаточно будет найти наименьшее значение во временном списке.
Наш код может быть таким:
public static int removeMin(Queue<integer> q) {
int min = q.peek();
for(Integer v : q) {
if (min >v){
min=v;
}
}
while (q.contains(min)) {
q.remove(min);
}
return min;
}
public static void addProducts(Queue<Integer> q, int v) {
q.add(v * 3);
q.add(v * 5);
q.add(v * 7);
}
public static int getKthMagicNumber(int k) {
if (k < 0) return 0;
int val = 1;
Queue<Integer> q = new LinkedList<Integer>();
addProducts(q, 1);
for(int i = 0; i < k; i++) {
val = removeMin(q);
addProducts(q, val);
}
return val;
}
Данный алгоритм гораздо лучше предыдущего, но все еще не идеален.
Для генерирования нового элемента Ai мы осуществляем поиск по связному списку, где каждый элемент имеет вид:
- 3 · предыдущий элемент;
- 5 · предыдущий элемент;
- 7 · предыдущий элемент.
Давайте уберем избыточные расчеты. Допустим, что наш список имеет вид:
q6 = {7A1, 5A2, 7A2, 7A3, 3A4, 5A4, 7A4, 5A5, 7A5}
Когда мы ищем минимальный элемент в списке, то проверяем сначала 7A1 < min, а затем 7A5 < min. Глупо, не правда ли? Поскольку мы знаем, что A1 < A5, то достаточно выполнить проверку 7A1 < min.
Если бы мы разделили список по постоянным множителям, то должны были бы проверить только первое из произведений на 3, 5 и 7. Все последующие элементы будут больше.
Таким образом, наш список принимает вид:
- Q36 = {3A4}
- Q56 = {5A2, 5A4, 5A5}
- Q76 = {7A1, 7A2, 7A3, 7A4, 7A5}
Чтобы найти минимум, достаточно проверить начало каждой очереди:
Y = min(Q3.head(), Q5.head(), Q7.head())
Как только мы вычислим y, нужно добавить 3y в список Q3, 5y в Q5 и 7y в Q7. Но мы хотим вставлять эти элементы, только если они отсутствуют в других списках.
Как, например, 3y может попасть в какой-нибудь другой список? Допустим, элемент y был получен из Q7, это значит, что y = 7x. Если 7x — наименьшее значение, значит, 3x уже было задействовано. А как мы действовали, когда увидели 3x? Правильно, мы вставили 7 · 3x в Q7. Обратите внимание, что 7 · 3x = 3 · 7x = 3y.
В общем виде: если мы берем элемент из Q7, он будет иметь вид 7 · suffix. Мы знаем, что 3 · suffix и 5 · suffix уже обработаны, и элемент 7 · 3 ·suffix добавлен в Q7. Единственное значение, которое мы еще не встречали — 7 · 7 · suffix, поэтому добавляем его в Q7.
Давайте рассмотрим пример, чтобы разобраться, как работает данный алгоритм. Инициализация:
Q3 = 3
Q5 = 5
Q7 = 7
Удаляем min = 3, вставляем 3*3 в Q3, 5*3 в Q5, 7*3 в Q7
Q3 = 3*3
Q5 = 5, 5*3
Q7 = 7, 7*3
Удаляем min = 5. 3*5 – дубль, значит, мы уже обработали 5*3.
Вставляем 5*5 в Q5, 7*5 в Q7
Q3 = 3*3
Q5 = 5*3, 5*5
Q7 = 7, 7*3, 7*5
Удаляем min = 7. 3*7 и 5*7 – дубли,
уже обработали 7*3 и 7*5.
Вставляем 7*7 в Q7
Q3 = 3*3
Q5 = 5*3, 5*5
Q7 = 7*3, 7*5, 7*7
Удаляем min = 3*3 = 9. Вставляем 3*3*3
в Q3, 3*3*5 в Q5, 3*3*7 в Q7.
Q3 = 3*3*3
Q5 = 5*3, 5*5, 5*3*3
Q7 = 7*3, 7*5, 7*7, 7*3*3
Удаляем min = 5*3 =15. 3*(5*3) –
дубль, так как уже обработали 5*(3*3).
Вставляем 5*5*3 в Q5, 7*5*3 в Q7
Q3 = 3*3*3
Q5 = 5*5, 5*3*3, 5*5*3
Q7 = 7*3, 7*5, 7*7, 7*3*3, 7*5*3
Удаляем min = 7*3 = 21. 3*(7*3) и 5*(7*3) – дубли,
уже обработали 7*(3*3) и 7*(5*3). Вставляем 7*7*3 в Q7
Q3 = 3*3*3
Q5 = 5*5, 5*3*3, 5*5*3
Q7 = 7*5, 7*7, 7*3*3, 7*5*3, 7*7*3
Структура нашего алгоритма будет иметь вид:
- Инициализируем array и очереди Q3, Q5 и Q7.
- Вставляем 1 в array.
- Вставляем 1·3, 1·5, 1·7 в Q3, Q5 и Q7 соответственно.
- Пусть x будет минимальным элементом в Q3, Q5 и Q7.Присоединим x к magic.
- Если x находится в:
- Q3 => присоединяем x·3, x·5 и x·7 к Q3, Q5 и Q7. Удаляем x из Q3.
- Q5 => присоединяем x·5 и x·7 к Q5 и Q7. Удаляем x из Q5.
- Q7 => присоединяем x·7 к Q7. Удаляем x из Q7.
- Повторяем шаги 4-6, пока k-й элемент не будет найден.
Следующий код реализует данный алгоритм:
public static int getKthMgicNumber(int k) {
if (k < 0) {
return 0;
}
int val = 0;
Queue<Integer> queue3 = new LinkedList<Integer>();
Queue<Integer> queue5 = new LinkedList<Integer>();
Queue<Integer> queue7 = new LinkedList<Integer>();
queue3.add(1);
/* Итерация от 0 до k */
for (int i = 0; i <= k; i++) {
int v3 = queue3.size() > 0 ? queue3.peek() :
Integer.MAX_VALUE;
int v5 = queue5.size() > 0 ? queue5.peek() :
Integer.MAX_VALUE;
int v7 = queue7.size() > 0 ? queue7.peek() :
Integer.MAX_VALUE;
val = Math.min(v3,Mathmin(v5, v7));
if(val == v3){ // ставим в очередь 3, 5 и 7
queue3.remove();
queue3.add(3 * val);
queue5.add(5 * val);
} else if (val == v5) {
// ставим в очередь 5 и 7
queue5.remove();
queue5.add(5 * val);
} else if (val == v7) {
// ставим в очередь Q7
queue7.remove();
}
queue7.add(7 * val);
// всегда добавляем в очередь Q7
}
return val;
}
Если вам досталась подобная задача, приложите все усилия, чтобы ее решить, потому что это действительно трудное задание. Вы можете начать с решения «в лоб» (спорно, зато не слишком сложно), а затем попытаться оптимизировать его. Или попытайтесь найти шаблон, спрятанный в числах.
Интервьюер поможет, если вы будете испытывать затруднения. Не сдавайтесь! Рассуждайте вслух, задавайте вопросы и объясняйте ход ваших мыслей. Интервьюер наверняка начнет помогать вам.
Помните, никто не ожидает, что вы найдете идеальное решение. Ваши результаты будут сравнивать с результатами других кандидатов. Все будут находиться в одинаковых условиях.
Разбор задачи по книге «Карьера программиста. Как устроиться на работу в Google, Microsoft или другую ведущую IT-компанию»