Надрукуйте наступне найменше з 2 ^ i * 5 ^ j, де i, j> = 0


10

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

Створити {2^i * 5^j | i,j >= 0}відсортовану колекцію. Постійно друкуйте наступне найменше значення.

Приклад: { 1, 2, 4, 5, 8, 10...}

"Наступний найменший" змушує мене думати, що йде міні-купа, але я насправді не знав, куди піти звідти, і інтерв'юер не надав допомоги.

Хтось має поради, як вирішити таку проблему?


Я думаю, що інтерв'ю хочу попросити вас зробити це в постійній пам'яті. Використання O (n) пам'яті робить це досить тривіально. Або, принаймні, використовуючи O (logn) пам'ять, оскільки розмір кодування для введення n буде logn. O (n) для рішення пам’яті - це експоненціальне рішення пам’яті.
Поінформовано

Відповіді:


14

Давайте переформулюємо проблему: виведіть кожне число від 1 до нескінченності таким чином, що число не має факторів, крім 2 і 5.

Нижче простий фрагмент C #:

for (int i = 1;;++i)
{
    int num = i;
    while(num%2 == 0) num/=2;
    while(num%5 == 0) num/=5;
    if(num == 1) Console.WriteLine(i);
}

Підхід Кіліана / QuestionC набагато ефективніший. C # фрагмент при такому підході:

var itms = new SortedSet<int>();
itms.Add(1);
while(true)
{
    int cur = itms.Min;
    itms.Remove(itms.Min);
    itms.Add(cur*2);
    itms.Add(cur*5);
    Console.WriteLine(cur);
}

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), і O(n log n)вчасно знайде n чисел . Перший фрагмент знаходить числа в експоненціальному часі.


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

1
@Snowbody: Питання ОП - це питання алгоритмів, тож це дещо не має значення. Ігноруючи це, вашим першим питанням має бути справу з дуже великими цілими числами, оскільки це стає проблемою набагато швидше, ніж накладні збирачі сміття.
Брайан

8

Це досить поширене питання інтерв'ю, на яке корисно знати відповідь. Ось відповідний запис у моєму особистому ліжечку:

  • щоб сформувати числа форми 3 a 5 b 7 c для того , щоб почати з 1, ввести всі три можливі наступники (3, 5, 7) в допоміжну структуру, а потім додати до свого списку найменше число.

Іншими словами, вам потрібно двоступеневий підхід з додатковим відсортованим буфером для ефективного вирішення цього питання. (Хороший довший опис - у розгортанні інтерв'ю кодування Гейл Макдауелл.


3

Ось відповідь, яка працює з постійною пам'яттю за рахунок процесора. Це не є гарною відповіддю в контексті оригінального питання (тобто відповіді під час інтерв'ю). Але якщо співбесіда триває 24 години, то це не так вже й погано. ;)

Ідея полягає в тому, що якщо я маю n, що є правильною відповіддю, то наступний у послідовності буде n разів меншим за потужність у два, поділене на деяку потужність 5. Або ж n разів більше потужність 5, поділене на a потужність двох. За умови, що вона ділиться рівномірно. (... або дільник може бути 1;); у цьому випадку ви просто множите на 2 або 5)

Наприклад, перейти від 625 до 640, помножити на 5 ** 4/2 ** 7. Або, загалом, помножити на деяке значення 2 ** m * 5 ** nна деякий m, n, де одне додатне, а одне - негативне або нульове, і множник ділить число рівномірно.

Тепер, хитра частина - знайти множник. Але ми знаємо: а) дільник повинен ділити число рівномірно; б) множник повинен бути більшим одиниці (числа постійно збільшуються), і в) якщо ми виберемо найменший множник, більший за 1 (тобто 1 <f <всі інші f ), то гарантовано це буде наш наступний крок. Крок після цього буде найнижчим кроком.

Неприємна частина - знаходження значення m, n. Є лише можливості журналу (n), тому що відмовитись лише стільки 2-х або 5-х, але мені довелося додати коефіцієнт -1 до +1, як неохайний спосіб вирішити питання про округлення. Отже, нам залишається лише повторити через O (log (n)) кожен крок. Таким чином, це O (n log (n)) загалом.

Хороша новина полягає в тому, що вона приймає значення і знаходить наступне значення, ви можете почати будь-де в послідовності. Отже, якщо ви хочете, щоб наступний був після 1 мільярда, ви можете просто знайти його, повторивши через 2/5 або 5/2 і вибравши найменший множник, більший за 1.

(пітон)

MAX = 30
F = - math.log(2) / math.log(5)

def val(i, j):
    return 2 ** i * 5 ** j

def best(i, j):
    f = 100
    m = 0
    n = 0
    max_i = (int)(math.log(val(i, j)) / math.log(2) + 1) if i + j else 1
    #print((val(i, j), max_i, x))
    for mm in range(-i, max_i + 1):
        for rr in {-1, 0, 1}:
            nn = (int)(mm * F + rr)
            if nn < -j: continue
            ff = val(mm, nn)
            #print('  ' + str((ff, mm, nn, rr)))
            if ff > 1 and ff < f:
                f = ff
                m = mm
                n = nn
    return m, n

def detSeq():

    i = 0
    j = 0
    got = [val(i, j)]

    while len(got) < MAX:
        m, n = best(i, j)

        i += m
        j += n
        got.append(val(i, j))

        #print('* ' + str((val(i, j), m, n)))
        #print('- ' + str((v, i, j)))

    return got

Я підтвердив перші 10 000 чисел, які це генерує, проти перших 10 000, створених рішенням відсортованого списку, і це працює принаймні так далеко.

До речі, наступний після трильйона здається 1024 000 000 000.

...

Гм. Я можу отримати продуктивність O (n) - O (1) на значення (!) - та O (log n) використання пам'яті, розглядаючи best()як таблицю пошуку, яку я збільшую поступово. Зараз він економить пам'ять, повторюючи кожен раз, але робить багато зайвих обчислень. Тримаючи ці проміжні значення - і список мінімальних значень - я можу уникнути дублюючої роботи та значно прискорити її. Однак список проміжних значень буде рости з n, отже, пам'яттю O (log n).


Чудова відповідь. У мене схожа думка, що я не кодував. В цій ідеї, я тримаю трекер для 2 і 5. Це буде відстежувати максимум nі mякі були використані шляхом з чисел в послідовності до сих пір. На кожній ітерації, nабо mможе або не може йти. Ми створюємо нове число, оскільки 2^(max_n+1)*5^(max_m+1)тоді зменшуємо це число вичерпно рекурсивно, кожен виклик зменшуючи показник на 1, поки ми не отримаємо мінімум, що перевищує поточне число. Ми уточнюємо max_n, по max_mмірі необхідності. Це постійна пам'ять. Можна O(log^2(n))запам'ятати, якщо кеш DP буде використаний у виклику скорочення
InformedA

Цікаво. Оптимізація тут полягає в тому, що не потрібно враховувати всі пари m & n, оскільки ми знаємо, що правильний m, n дасть найближчий множник до 1. Тому мені потрібно лише оцінити m = -i до max_i, і я можна просто порахувати n, кидаючи трохи сміття для округлення (я був неохайний і просто переглянув -1 до 1, але це більше думає;)).
Роб

Однак я так собі думаю, як ти ... послідовність буде детермінованою ... це насправді як великий трикутник Паскаля i + 1 в одному напрямку і j + 1 в іншому. Отже послідовність повинна бути математично детермінованою. Для будь-якого вузла в трикутнику завжди буде математично визначений наступний вузол.
Роб

1
Може бути формула наступного, нам може не знадобитися пошук. Я не знаю точно.
ПоінформованоA

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

2

Брайан був абсолютно правий - моя інша відповідь була занадто складною. Ось простіший і швидший спосіб зробити це.

Уявіть Квадрант I площини Евкліда, обмежений цілими числами. Назвіть одну вісь осі i, а другу - вісь j.

Очевидно, що точки, близькі до походження, вибиратимуться перед точками, далекими від початку. Також зауважте, що активна область відсунеться від осі i, перш ніж вона віддаляється від осі j.

Щойно точка буде використана, вона більше ніколи не буде використана. І точку можна використовувати лише в тому випадку, якщо точка безпосередньо під нею або зліва від неї вже використана.

Складаючи їх разом, ви можете уявити собі "кордон" або "передній край", який починається навколо початку, і вони поширюються вгору і вправо, поширюючись по осі i далі, ніж по осі j.

Насправді ми можемо розібратися у чомусь більшому: на кордоні / межі буде щонайменше одна точка для будь-якого заданого i-значення. (Ви повинні збільшити i більше 2 разів, щоб дорівнювати приріст j.) Отже, ми можемо представити кордон як список, що містить один елемент для кожної координати i, лише змінюючись залежно від координати j та значення функції.

Кожен прохід ми вибираємо мінімальний елемент на передньому краї, а потім переміщуємо його в j-напрямку один раз. Якщо нам трапляється підняття останнього елемента, ми додамо новий останній більше елемент із збільшенням i-значення та j-значенням 0.

using System;
using System.Collections.Generic;
using System.Text;

namespace TwosFives
{
    class LatticePoint : IComparable<LatticePoint>
    {
      public int i;
      public int j;
      public double value;
      public LatticePoint(int ii, int jj, double vvalue)
      {
          i = ii;
          j = jj;
          value = vvalue;
      }
      public int CompareTo(LatticePoint rhs)
      {
          return value.CompareTo(rhs.value);
      }
    }


    class Program
    {
        static void Main(string[] args)
        {
            LatticePoint startPoint = new LatticePoint(0, 0, 1);

            var leadingEdge = new List<LatticePoint> { startPoint } ;

            while (true)
            {
                LatticePoint min = leadingEdge.Min();
                Console.WriteLine(min.value);
                if (min.j + 1 == leadingEdge.Count)
                {
                    leadingEdge.Add(new LatticePoint(0, min.j + 1, min.value * 2));
                }
                min.i++;
                min.value *= 5;
            }
        }
    }
}

Пробіл: O (n) у кількості елементів, надрукованих дотепер.

Швидкість: вставляє O (1), але це робиться не кожен раз. (Іноді довше, коли List<>має зростати, але все ще O (1) амортизується). Велика раковина часу - це пошук мінімального, O (n) кількості друкованих досі елементів.


1
Який алгоритм використовує це? Чому це працює? Ключова частина питання, що задається, полягає Does anyone have advice on how to solve such a problem?у спробі зрозуміти основну проблему. Дамп коду не відповідає на це питання добре.

Добрий момент, я пояснив своє мислення.
Snowbody

+1 Хоча це приблизно еквівалентно моєму другому фрагменту, використання ваших непорушних країв стає більш зрозумілим, як зростає кількість ребер.
Брайан

Це, безумовно, повільніше, ніж переглянений фрагмент Брайана, але його поведінка щодо використання пам'яті повинна бути набагато кращою, оскільки вона не постійно видаляє та додає елементи. (Якщо CLR або SortedSet <> не мають певного методу повторного використання елементів, про які я не знаю)
Snowbody

1

Налагоджене на основі рішення було, мабуть, те, що шукав ваш інтерв'юер, однак, це невдале наслідок наявності 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):

Attempt to increase i:
++i
current difference in value CD = 1
while (j > 0)
  --j
  mark difference in value for
     current (i,j) as CD -= lg 5
  while (CD < 0) // Have to increase the sequence
    ++i          // This while will end in three loops at most.
    CD += 1
find minimum among each marked difference ((i,j) -> CD)

Attempt to increase j:
++j
current difference in value CD = lg 5
while (j > 0)
  --i
  mark difference in value for
     current (i,j) as CD -= 1
  while (CD < 0) // have to increase the sequence
    ++j          // This while will end in one loop at most.
    CD += lg 5
find minimum among each marked difference ((i,j) -> CD)

Кожна ітерація намагається оновити i, потім j, і йде з меншим оновленням двох.

Оскільки iі jє щонайбільше O(sqrt(n)), ми маємо загальний O(n sqrt(n))час. iі jрости зі швидкістю квадрата , nтак як для будь-яких максимальних valiues imaxі jmaxіснують O(i j)унікальні пари , з яких можна зробити нашу послідовність , якщо наша послідовність nтермінів, а iй jзростати в межах деякого постійного множника один одного (тому що показник складається з лінійної поєднання двох), ми це знаємо iі jє O(sqrt(n)).

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


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

@randomA Дякую Після деякої подальшої думки я дійшов висновку, що в даний час мій алгоритм не такий швидкий, як я вважав, що це було. Якщо є більш швидкий спосіб оцінити "Спроба збільшити i / j", я думаю, що це ключ до отримання логарифмічного часу.
VF1

Я думав над цим: ми знаємо, що для збільшення кількості ми повинні збільшити кількість одного з простих. Наприклад, один із способів збільшення - це муль на 8 і ділення на 5. Отже, ми отримуємо безліч всіх способів збільшення та зменшення числа. Це буде містити лише основні способи, такі як mul 8 div 5, а не mul 16 div 5. Існує також інший набір основних способів зменшення. Сортуйте ці два набори за коефіцієнтом збільшення чи зменшення. Враховуючи число, наступний можна знайти, знайти відповідний спосіб збільшення з найменшим коефіцієнтом від набору збільшення ..
InformedA

.. застосовні засоби. Є достатня кількість праймів для виконання mul та div. Тоді ми знаходимо шлях зменшення до нового числа, тому починаємо з того, який зменшується найбільше. Продовжуйте використовувати нові способи зменшення, і ми зупиняємось, коли нове число менше початкового заданого числа. Оскільки набір простих чисел постійний, це означає постійний розмір для двох множин. Це також потребує трохи доказів, але мені це схоже на постійний час, постійну пам'ять на кожне число. Настільки постійна пам'ять і лінійний час для друку n чисел.
ПоінформованоA

@randomA звідки ти дійшов поділу? Ви не хочете зробити повну відповідь - я не зовсім розумію ваші коментарі.
VF1
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.