Попытка оптимизировать мой код, чтобы удалить вложенный цикл или сделать его более эффективным - PullRequest
8 голосов
/ 05 мая 2019
  • Мой друг берет последовательность чисел от 1 до n (где n> 0)

  • В этой последовательности он выбирает два числа, aи b

  • Он говорит, что произведение a и b должно быть равно сумме всех чисел в последовательности, исключая a и b

  • Учитывая число n, не могли бы вы сказать мне числа, которые он исключил из последовательности?

Нашли решение для этого Ката из Code Wars, но оно истекло (через 12 секунд)в редакторе, когда я его запускаю;какие-нибудь идеи относительно того, как я должен далее оптимизировать вложенный цикл for или удалить его?

function removeNb(n) {
  var nArray = [];
  var sum = 0;
  var answersArray = [];
  for (let i = 1; i <= n; i++) {
    nArray.push(n - (n - i));
    sum += i;
  }
  var length = nArray.length;
  for (let i = Math.round(n / 2); i < length; i++) {
    for (let y = Math.round(n / 2); y < length; y++) {
      if (i != y) {
        if (i * y === sum - i - y) {
          answersArray.push([i, y]);
          break;
        }
      }
    }
  }
  return answersArray;
}

console.log(removeNb(102));
.as-console-wrapper { max-height: 100% !important; top: 0; }

Ответы [ 5 ]

9 голосов
/ 05 мая 2019

Я думаю, что нет смысла вычислять сумму после заполнения массива, вы можете сделать это во время заполнения.

function removeNb(n) {
    let nArray = [];
    let sum = 0;
    for(let i = 1; i <= n; i++) {
        nArray.push(i);
        sum += i;
    }
}

И поскольку в качестве входных данных для формулы a * b = sum - a - b могут быть только два числа a и b, для каждого из них может быть только одно возможное значение. Поэтому нет необходимости продолжать цикл, когда вы их найдете.

if(i*y === sum - i - y) {
    answersArray.push([i,y]);
    break;
}

Рекомендую посмотреть на проблему по-другому.

Вы пытаетесь найти два числа a и b, используя эту формулу a * b = sum - a - b.

Почему бы не уменьшить формулу следующим образом:

a * b + a = sum - b

a ( b + 1 ) = sum - b

a = (sum - b) / ( b + 1 )

Тогда вам нужен только один цикл for, который выдает значение b, проверьте, делится ли (сумма - b) на (b + 1), и если деление дает число, которое меньше n.

for(let i = 1; i <= n; i++) {
    let eq1 = sum - i;
    let eq2 = i + 1;
    if (eq1 % eq2 === 0) {
        let a = eq1 / eq2;
        if (a < n && a != i) {
            return [[a, b], [b, a]];
        }
    }
}
6 голосов
/ 05 мая 2019

Вы можете решить это за линейное время с помощью метода двух указателей (стр. 77 в книге).

Чтобы получить интуицию к решению, давайте начнем думать об этой части вашего кода:

for(let i = Math.round(n/2); i < length; i++) {
    for(let y = Math.round(n/2); y < length; y++) {
        ...

Вы уже поняли, что это та часть вашего кода, которая работает медленно. Вы пробуете каждую комбинацию i и y, но что, если вам не нужно было пробовать каждую комбинацию?

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

Предположим, n == 10, поэтому мы имеем 1 2 3 4 5 6 7 8 9 10, где sum = 55.

Предположим, что первая комбинация, которую мы попробовали, была 1*10.

Имеет ли смысл попробовать 1*9 дальше? Конечно, нет, так как мы знаем, что 1*10 < 55-10-1 мы знаем, что мы должны увеличить наш продукт, а не уменьшить его.

Итак, давайте попробуем 2*10. Ну, 20 < 55-10-2, поэтому нам еще нужно увеличить.

3*10==30 < 55-3-10==42

4*10==40 < 55-4-10==41

Но тогда 5*10==50 > 55-5-10==40. Теперь мы знаем, что мы должны уменьшить наш продукт. Мы могли бы либо уменьшить 5, либо уменьшить 10, но мы уже знаем, что не будет никакого решения, если мы уменьшим 5 (так как мы попробовали это на предыдущем шаге). Таким образом, единственный выбор - уменьшить 10.

5*9==45 > 55-5-9==41. Снова то же самое: мы должны уменьшить 9.

5*8==40 < 55-5-8==42. И теперь мы должны снова увеличить ...


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

  • переместить левый указатель вправо
  • или переместить указатель вправо влево

В начале разница между указателями составляет n-1. На каждом шаге разница между указателями уменьшается на единицу. Мы можем остановиться, когда указатели пересекаются друг с другом (и сказать, что решение не может быть найдено, если оно не было найдено до сих пор). Очевидно, что мы не можем сделать больше, чем n вычислений, прежде чем прийти к решению. Это то, что значит сказать, что решение линейное относительно n; независимо от размера n, мы никогда не делаем больше, чем n вычислений. Сравните это с вашим исходным решением, в котором мы фактически выполняем вычисления n^2 по мере роста n.

0 голосов
/ 05 мая 2019

Если это интересно, CY Aries указал на это :

ab + a + b = n(n + 1)/2

добавьте 1 в обе стороны

ab + a + b + 1 = (n^2 + n + 2) / 2
(a + 1)(b + 1) = (n^2 + n + 2) / 2

, поэтому мы ищемфакторы (n^2 + n + 2) / 2 и имеют некоторые признаки относительно наименьшего размера фактора.Это не обязательно означает значительное улучшение сложности для реального поиска, но все равно это круто.

0 голосов
/ 05 мая 2019

Это частичный комментарий, частичный ответ.

В инженерном плане опубликованная исходная функция использует "грубую силу" для решения проблемы, повторяя каждую (или более чем необходимую) возможную комбинацию. Число итераций n велико - если бы вы сделали все возможное, это было бы

n * (n-1) = bazillio n

Меньше - больше

Итак, давайте рассмотрим вещи, которые можно оптимизировать, сначала некоторые мелочи, я немного запутался в первом цикле for и nArray:

// OP's code
 for(let i = 1; i <= n; i++) {
    nArray.push(n - (n - i));
    sum += i;
  }

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

sum = ( n + 1 ) * n * 0.5 ;

ПЕТЛИ

   // OP's loops, not optimized
for(let i = Math.round(n/2); i < length; i++) {
    for(let y = Math.round(n/2); y < length; y++) {
      if(i != y) {
        if(i*y === sum - i - y) {

Особенности оптимизации:

Я вижу, что вы на правильном пути в некотором смысле , сокращая начальные значения i, y пополам с учетом факторов. Но вы перебираете их обоих в одном направлении: UP. Кроме того, нижние числа выглядят так, как будто они могут доходить чуть ниже половины от n (возможно, не потому, что последовательность начинается с 1, я не подтвердил это, но, похоже, это так).

Кроме того, мы хотим избегать деления каждый раз, когда начинаем создание экземпляра цикла (т.е. устанавливаем переменную один раз, а также собираемся ее изменить). И, наконец, с помощью операторов IF, i и y никогда не будут равны друг другу, как мы собираемся создавать циклы, так что это условие может исчезнуть.

Но более важным является направление прохождения петель . Меньший коэффициент low, вероятно, будет близок к самому низкому значению цикла (около половины от n), а больший коэффициент hi равен , вероятно , будет около значения n. Если у нас есть какая-то основательная математическая теория, которая говорит что-то вроде «привет, никогда не будет меньше 0,75n», то мы могли бы сделать пару модов, чтобы воспользоваться этими знаниями.

То, как показаны циклы ниже, они ломаются и повторяются до того, как встречаются циклы hi и low.

Более того, не имеет значения, какой цикл выбирает меньшее или большее число, поэтому мы можем использовать это для сокращения внутреннего цикла при проверке пар чисел, делая цикл каждый раз меньше. Мы не хотим тратить время на проверку одной и той же пары номеров более одного раза! Цикл с более низким коэффициентом начнется чуть ниже половины n и увеличится, а цикл с более высоким коэффициентом начнется с n и опустится.

// Code Fragment, more optimized:

let nHi = n;
let low = Math.trunc( n * 0.49 );
let sum = ( n + 1 ) * n * 0.5 ;

                    // While Loop for the outside (incrementing) loop
while( low < nHi ) {
                                       // FOR loop for the inside decrementing loop
    for(let hi = nHi; hi > low; hi--) {
                                        // If we're higher than the sum, we exit, decrement.
        if( hi * low + hi + low > sum ) { 
            continue; 
        }
                                               // If we're equal, then we're DONE and we write to array.
        else if( hi * low + hi + low === sum) {
            answersArray.push([hi, low]);
            low = nHi; // Note this is if we want to end once finding one pair
            break; // If you want to find ALL pairs for large numbers then replace these low = nHi; with low++;
        } 
               // And if not, we increment the low counter and restart the hi loop from the top.
        else {
            low++;
            break;
        }
    } // close for
} // close while

Учебник

Итак, мы установили несколько переменных. Обратите внимание, что low установлено чуть меньше половины n, так как большие числа выглядят так, как будто они могут быть на несколько пунктов меньше. Кроме того, мы не округляем, мы усекаем, что, по сути, «всегда округляет вниз» и немного лучше для производительности (хотя в данном случае это имеет значение только с одним назначением).

Цикл while начинается с самого низкого значения и увеличивается, возможно, вплоть до n-1. Цикл hi FOR начинается в n (копируется в nHi), а затем уменьшается до тех пор, пока коэффициент не будет найден, ИЛИ не перехватывает при низком + 1.

Условия: Первый IF: Если мы выше суммы, мы выходим, уменьшаем и продолжаем с более низким значением для фактора hi. ИЛИ ЕСЛИ: Если мы РАВНЫ, то мы закончили и перерыв на обед. Мы устанавливаем low = nHi, чтобы при выходе из цикла FOR мы также выходили из цикла WHILE. ELSE: Если мы попадаем сюда, это потому, что мы меньше суммы, поэтому нам нужно увеличить цикл while и сбросить цикл hi FOR, чтобы снова начать с n (nHi).

0 голосов
/ 05 мая 2019

Хасан прав, вот полное решение:

function removeNb (n) {
  var a = 1;
  var d = 1;

  // Calculate the sum of the numbers 1-n without anything removed
  var S = 0.5 * n * (2*a + (d *(n-1)));

  // For each possible value of b, calculate a if it exists.
  var results =  [];
  for (let numB = a; numB <= n; numB++) {
      let eq1 = S - numB;
      let eq2 = numB + 1;
      if (eq1 % eq2 === 0) {
          let numA = eq1 / eq2;
          if (numA < n && numA != numB) {
              results.push([numA, numB]);
              results.push([numB, numA]);
          }
      }
  }

  return results;
}
...