February 18, 2019

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


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

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

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

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


Условие

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

Пример

Входной массив: { 4, 6, 7, 7 }

Выходные подмножества: { 4, 6 }, { 4, 7 }, { 4, 6, 7 }, {4, 6, 7, 7}, { 6, 7 }, { 6, 7, 7}, {7, 7}, { 4, 7, 7}

Уточнения:

  • Длина массива не превышает 15 элементов.
  • Значения чисел массива лежат в интервале (-100, 100).
  • В массиве могут быть одинаковые числа, два одинаковых числа составляют подмножество.

Мой вариант решения

Для начала подумаем, о чём говорят уточнения к задаче.

Из первого следует, что прямой перебор будет крайне ресурсоёмким, ведь число возможных вариантов равняется (n! + 1), где n - длина входного массива. 15! = 1 307 674 368 000 - гигантское число, поэтому прямой перебор следует отложить.

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

Третье условие сообщает нам о том, что подмножества на самом деле должны быть неубывающими. Именно оно принесло много хлопот во время решения.

Приведём сразу код решения, а потом пошагово его разберём:

public static IList<IList<int>> FindSubsequences(int[] nums)
{
    IList<IList<int>> answear = new List<IList<int>>();
    if (nums.Length < 2)
        return answear;
    Dictionary<int, int> dict = new Dictionary<int, int>();
    bool povtor;
    int count = 0;

    for (int i = 0; i < nums.Length; i++)
    {
        count = answear.Count;
        povtor = dict.ContainsKey(nums[i]);                

        if (!povtor)
        {
            answear.Add(new int[] { nums[i] }.ToList());
            dict.Add(nums[i], count);
        }
                
        for (int j = 0; j < count; j++)
        {

            if (povtor)
            {
                if ((j >= dict[nums[i]]) && (answear[j].Last() <= nums[i]))
                {                    
                    List<int> helped = answear[j].Select(y => y).ToList();
                    helped.Add(nums[i]);
                    answear.Add(helped);                    
                }
            }
            else
            {
                if (answear[j].Last() <= nums[i])
                {
                    List<int> helped = answear[j].Select(y => y).ToList();
                    helped.Add(nums[i]);
                    answear.Add(helped);
                }
            }   
        }

        if (povtor)
            dict[nums[i]] = count;
    }

    for (int i = 0; i < answear.Count;)
    {
        if (answear[i].Count == 1)
            answear.RemoveAt(i);
        else
            i++;
    }

    return answear;
}

В начале инициализируем список списков answear, который и будет содержать выходные подмножества. Словарь dict предназначен для хранения элементов входного массива и индекс их первого появления в answear. Флаг povtor указывает, был ли ранее исследован элемент из входного массива nums. Переменная count показывает текущую длину списка answear.

IList<IList<int>> answear = new List<IList<int>>();
if (nums.Length < 2)
    return answear;
Dictionary<int, int> dict = new Dictionary<int, int>();
bool povtor;
int count = 0;

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

count = answear.Count;
povtor = dict.ContainsKey(nums[i]); 

Если элемент ранее не исследовался, то мы помещаем его в список и добавляем в конец answear. Также заносим nums[i] в словарь. Сам элемент является ключом, а значением - его индекс в answear, равный count.

if (!povtor)
{
    answear.Add(new int[] { nums[i] }.ToList());
    dict.Add(nums[i], count);
}

После этого начинаем перебирать все списки в answear.

for (int j = 0; j < count; j++)
{
   .....

Затем обработка списков разделяется на две ветви в зависимости от значения переменной povtor.

Если значение i-го элемента входного массива обрабатывалось, то проверяем, индекс текущего списка больше ли последнего индекса текущего рассматриваемого элемента массива (j >= dict[nums[i]]), а также превышает (или равняется)элемент массива последнее число рассматриваемого списка (answear[j].Last() <= nums[i]).

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

if (povtor)
{
    if ((j >= dict[nums[i]]) && (answear[j].Last() <= nums[i]))
    {                    
        List<int> helped = answear[j].Select(y => y).ToList();
        helped.Add(nums[i]);
        answear.Add(helped);                    
    }
} 

Если второе условие второго оператора if требуется для создания неубывающего подмножества, то для чего же первое условие?

Рассмотрим простой массив {1, 2, 1, 1}.

Первая итерация
    answear = { 1 };
Вторая итерация
    answear = { 1 }, { 2 }, {1, 2}
Третья итерация
    answear = { 1 }, { 2 }, { 1, 2 }, { 1 }, { 1, 1 }
Четвёртая итерация
    answear = { 1 }, { 2 }, { 1, 2 }, { 1 }, { 1, 1 }, { 1 }, { 1, 1 }, { 1, 1, 1 }

Видно, что после четвёртой итерации появился повтор списка { 1, 1 }. Повторы в дальнейшем очень накладно удалять из answear. Поэтому используется dict - словарь с индексами последних появлений элементов во входном массиве.


Если же элемент ранее не появлялся в массиве, то мы сравниваем его с последним числом в предыдущих списках и при верном равенстве добавляем элемент в конец списков. Так как код повторяется в обоих условиях оператора if, то можно создать метод типа void.

if (povtor)
{
    .....                
}
else
{
    if (answear[j].Last() <= nums[i])
    {
    List<int> helped = answear[j].Select(y => y).ToList();
    helped.Add(nums[i]);
    answear.Add(helped);
    }
}   

После прохода по всем предыдущим спискам, если элемент повторный, обновляем индекс в словаре dict.

if (povtor)
    dict[nums[i]] = count;

В самом конце удаляем списки, состоящие из одного элемента, и выводим answear в качестве ответа.

for (int i = 0; i < answear.Count;)
{
    if (answear[i].Count == 1)
       answear.RemoveAt(i);
    else
        i++;
} 

Оценим сложности выполнения алгоритма.

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

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



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