Динамическое программирование — это метод, используемый в информатике для эффективного решения сложных задач путем их разбиения на более мелкие подзадачи и сохранения решений в таблице или матрице. В этом руководстве мы подробно изучим динамическое программирование, обсудим его принципы, общие методы и приложения в JavaScript.

Принципы динамического программирования

Динамическое программирование основано на двух принципах: оптимальная подструктура и перекрывающиеся подзадачи.

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

Перекрывающиеся подзадачи — это тот факт, что одни и те же подзадачи встречаются в задаче неоднократно.

Динамическое программирование использует преимущества этих двух принципов для эффективного решения проблем, разбивая проблему на более мелкие подзадачи, решая их и сохраняя их решения в таблице или матрице. Затем решения объединяются для решения более крупной проблемы.

Методы динамического программирования

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

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

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

Распространенные проблемы динамического программирования

  1. Последовательность Фибоначчи

Последовательность Фибоначчи — это последовательность чисел, где каждое число является суммой двух предыдущих чисел. Последовательность начинается с 0 и 1, а каждое последующее число является суммой двух предыдущих чисел.

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

Вот пример вычисления n-го числа Фибоначчи с помощью восходящегодинамического программирования в JavaScript:

function fibonacci(n) {
  if (n < 2) {
    return n;
  }

  let table = [0, 1];

  for (let i = 2; i <= n; i++) {
    table[i] = table[i - 1] + table[i - 2];
  }

  return table[n];
}

В этом примере мы начинаем с проверки, меньше ли n 2. Если да, мы просто возвращаем n. Если нет, создаем таблицу с первыми двумя числами Фибоначчи (0 и 1). Затем мы используем цикл для вычисления следующих n — 1 чисел Фибоначчи и сохраняем их в таблице. Наконец, мы возвращаем n-е число Фибоначчи, которое является последним элементом в таблице.

2. Самая длинная возрастающая подпоследовательность

Задача о самой длинной возрастающей подпоследовательности заключается в поиске самой длинной подпоследовательности заданной последовательности, которая строго возрастает.

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

Вот пример вычисления самой длинной возрастающей подпоследовательности с помощью восходящегодинамического программирования в JavaScript:

function longestIncreasingSubsequence(nums) {
  const n = nums.length;
  const dp = new Array(n).fill(1);

  for (let i = 1; i < n; i++) {
    for (let j = 0; j < i; j++) {
      if (nums[j] < nums[i]) {
        dp[i] = Math.max(dp[i], dp[j] + 1);
      }
    }
  }

  return Math.max(...dp);
}

В этом примере мы начинаем с инициализации массива dp со всеми элементами, установленными в 1, поскольку каждый элемент в последовательности является подпоследовательностью длины 1. Затем мы используем вложенный цикл для сравнения каждой пары элементов в последовательности. Если элемент с индексом j меньше, чем элемент с индексом i, мы обновляем значение dp[i] так, чтобы оно было максимальным из его текущего значения и dp[j] + 1. Это означает, что мы рассматриваем возможность расширения подпоследовательности, которая заканчивается по индексу j с элементом по индексу i и взять максимальную длину всех таких расширений подпоследовательности. Наконец, мы возвращаем максимальное значение в массиве dp, которое представляет собой длину самой длинной возрастающей подпоследовательности.

Алгоритмы динамического программирования

  1. Запоминание

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

Вот пример вычисления n-го числа Фибоначчи с использованием мемоизации в JavaScript:

function fibonacci(n, memo = {}) {
  if (n in memo) {
    return memo[n];
  }

  if (n < 2) {
    return n;
  }

  memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo);
  return memo[n];
}

В этом примере мы начинаем с проверки наличия n в таблице мемоизации. Если это так, мы возвращаем его значение. Если нет, мы рекурсивно вычисляем n-е число Фибоначчи, рекурсивно вычисляя (n-1)-е и (n-2)-е числа Фибоначчи и сохраняя их сумму в таблице мемоизации. Это гарантирует, что мы вычисляем каждое число Фибоначчи только один раз, а последующие вызовы того же числа будут извлекаться из таблицы запоминания.

2. Табулирование

Табулирование — это восходящий метод динамического программирования, который включает в себя сначала решение подзадач и сохранение их решений в таблице или матрице. Затем решения более крупной задачи вычисляются с использованием решений подзадач.

Вот пример вычисления n-го числа Фибоначчи с использованием табуляции в JavaScript:

function fibonacci(n) {
  if (n < 2) {
    return n;
  }

  let table = [0, 1];

  for (let i = 2; i <= n; i++) {
    table[i] = table[i - 1] + table[i - 2];
  }

  return table[n];
}

В этом примере мы начинаем с проверки, меньше ли n 2. Если да, мы просто возвращаем n. Если нет, создаем таблицу с первыми двумя числами Фибоначчи (0 и 1). Затем мы используем цикл для вычисления следующих n — 1 чисел Фибоначчи и сохраняем их в таблице. Наконец, мы возвращаем n-е число Фибоначчи, которое является последним элементом в таблице.

Заключение

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