February 4, 2019

Leetcode.com, задача №300 (Medium). "Наибольшее возрастающее подмножество". Часть 1.


!!НАШ БЛОГ ПЕРЕЕХАЛ!!

Мы создали свой сайт! Все материалы, опубликованные в этом блоге, переехали туда.

Наш новый сайт maddevelop.ru

А данную статью вы можете найти по ссылке


Условие

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

Пример

Входной массив: [10, 9, 2, 5, 3, 7, 101, 18]

Ответ: 4

Объяснение: длиннейшие возрастающие подмножества - [2, 3, 7, 101], [2, 5, 7, 18] и [2, 3, 7, 18]; их длины равны 4.

Решение №1. Прямой перебор с рекурсией.

Для использования данного метода нам понадобится перегрузка функции: int LengthofLIS(int[] nums, int prev, int curpos), где nums - входной массив, prev - предыдущее значение элемента массива, curpos - индекс текущего рассматриваемого значения.

public static int LengthofLIS(int[] nums, int prev, int curpos)
{
    if (curpos == nums.Length)
        return 0;
    int taken = 0;
    if (nums[curpos] > prev)
        taken = 1 + LengthofLIS(nums, nums[curpos], curpos + 1);
    int nottaken = LengthofLIS(nums, prev, curpos + 1);
    return Math.Max(taken, nottaken);
}

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

После этого сравниваем значение элемента массива в текущей позиции (nums[curpos]) со значением предыдущего элемента (prev). Если первое больше второго, то увеличиваем taken на 1 и результат вызова функции LengthofLIS, где предыдущим значением становится nums[curpos], индекс текущего рассматриваемого значения сдвигаем на единицу вперёд. Если же неравенство не выполняется, то переменной nottaken присваиваем результат выполнения функции LengthofLIS, где предыдущее значение оставляется равным prev, а индекс текущего рассматриваемого значения также сдвигается на единицу вперёд.

Таким образом, функция LengthofLIS() возвращает наибольшее значение среди двух чисел: taken и nottaken.

Функция, возвращающая ответ задачи, выглядит следующим образом:

public static int LengthofLIS(int[] nums)
{
    return LengthofLIS(int[] nums, int.MinValue, 0);    
}

Временная сложность: 2 в степени n, именно такой размер у дерева рекурсии.

Пространственная сложность: n * n, т.к. каждый элемент массива перебирается n раз.

Решение №2. Рекурсия с мемоизацией.

Для значительного ускорения времени выполнения запишем результат выполнения каждого рекурсивного вызова в массив. А перед выполнением очередной рекурсии проверим, осуществлялся ли вызов ранее. Если да, то возьмём результат функции из массива.

public static int MemoLengthOfLIS(int[] nums)
{
    int[,] memo = new int[nums.Length, nums.Length];    
    return MemoLengthofLIS(nums, -1, 0, memo);
}

public static int MemoLengthofLIS(int[] nums, int prevIndex, int curpos, int[,] memo)
{
    if (curpos == nums.Length)
        return 0;
    if (memo[prevIndex + 1, curpos] > 0)
        return memo[prevIndex + 1, curpos];
    int taken = 0;
    if (prevIndex < 0 || nums[curpos] > nums[prevIndex])
        taken = 1 + MemoLengthofLIS(nums, curpos, curpos + 1, memo);
    int nottaken = MemoLengthofLIS(nums, prevIndex, curpos + 1, memo);
    memo[prevIndex + 1, curpos] = Math.Max(taken, nottaken);
    return memo[prevIndex + 1, curpos];
}

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

Во второй версии изменилось назначение второго параметра: теперь вместо значения предыдущего элемента храним его индекс. Это сделано для быстрого перемещения по массиву memo. В теле самой функции появилась проверка на наличие результата выполнения рекурсивной функции:

if (memo[prevIndex + 1, curpos] > 0)
    return memo[prevIndex + 1, curpos];

Если он ненулевой, то из массива сразу выбирается значение длины подмножества, повторных вызовов не происходит.

После нахождения значений переменных taken и nottaken их максимум записывается в memo, и только после этого происходит возврат результата выполнения функции.

Временная сложность: n * n, для каждого элемента происходит перебор всего массива.

Пространственная сложность: n * n, требуется дополнительный массив такого размера.



Ещё больше интересной информации на нашем Telegram канале.