Постановка задачи

Дана строка s, нужно найти длину самой длинной подстроки без повторяющихся символов.

Подход к решению

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

Алгоритм

  1. Инициализируем переменные: res для хранения максимальной длины подстроки, left для начала окна и seen для уникальных символов.

  2. Проходим по строке с помощью правого указателя:

    • Если символ под правым указателем уже в seen, сдвигаем левый указатель вправо, удаляя символы из seen, пока не уберем повторяющийся символ.

    • Добавляем текущий символ под правым указателем в seen.

    • Обновляем res, если текущая длина окна больше текущего максимума.

  3. Возвращаем результат.

Код решения

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        res = 0
        left = 0
        seen = set()

        for right in range(len(s)):
            right_char = s[right]

            while right_char in seen:
                left_char = s[left]
                seen.remove(left_char)
                left += 1
            
            seen.add(right_char)
            res = max(res, right - left + 1)

        return res

Объяснение кода

  1. Инициализация переменных:

    • res хранит максимальную длину подстроки без повторяющихся символов.

    • left указывает на начало текущего окна.

    • seen хранит уникальные символы текущего окна.

  2. Итерация по строке:

    • Цикл for итерирует по каждому символу строки, используя указатель right.

  3. Обнаружение повторяющихся символов:

    • Внутри цикла while проверяем, есть ли текущий символ в множестве seen. Если да, то удаляем символ под левым указателем из множества и сдвигаем левый указатель вправо.

  4. Добавление уникального символа:

    • После удаления всех повторяющихся символов из текущего окна, добавляем текущий символ в множество seen.

  5. Обновление результата:

    • Вычисляем длину текущего окна и обновляем res, если текущая длина больше.

  6. Возвращение результата:

    • По завершении цикла возвращаем максимальную длину подстроки без повторяющихся символов.

Визуализация решения

Рассмотрим строку s = "pwwkew" и визуализируем шаги решения задачи, показывая текущее состояние скользящего окна.

Шаги выполнения:

Итерация 0:

  • Текущий символ: p

  • Окно: [p]wwkew

  • seen: set()

  • Длина окна: 1

  • Окно после обновления результата: [p]wwkew

  • Текущий максимум: 1

Итерация 1:

  • Текущий символ: w

  • Окно: [pw]wkew

  • seen: {'p'}

  • Длина окна: 2

  • Окно после обновления результата: [pw]wkew

  • Текущий максимум: 2

Итерация 2:

  • Текущий символ: w

  • Окно: [pww]kew

  • seen: {'w', 'p'}

  • Длина окна: 3

  • Сокращение окна:

    • left: 1

    • Окно после сокращения: p[ww]kew

    • seen: {'w'}

    • Длина окна: 2

  • Сокращение окна:

    • left: 2

    • Окно после сокращения: pw[w]kew

    • seen: set()

    • Длина окна: 1

  • Окно после обновления результата: pw[w]kew

  • Текущий максимум: 2

Итерация 3:

  • Текущий символ: k

  • Окно: pw[wk]ew

  • seen: {'w'}

  • Длина окна: 2

  • Окно после обновления результата: pw[wk]ew

  • Текущий максимум: 2

Итерация 4:

  • Текущий символ: e

  • Окно: pw[wke]w

  • seen: {'w', 'k'}

  • Длина окна: 3

  • Окно после обновления результата: pw[wke]w

  • Текущий максимум: 3

Итерация 5:

  • Текущий символ: w

  • Окно: pw[wkew]

  • seen: {'w', 'k', 'e'}

  • Длина окна: 4

  • Сокращение окна:

    • left: 3

    • Окно после сокращения: pww[kew]

    • seen: {'k', 'e'}

    • Длина окна: 3

  • Окно после обновления результата: pww[kew]

  • Текущий максимум: 3

Асимптотическая сложность

  • Сложность по времени: O(n). Каждый символ строки добавляется и удаляется из множества не более одного раза.

  • Сложность по памяти: O(min(n, m)), где n - длина строки, m - размер алфавита.

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

Комментарии (5)


  1. idsulik Автор
    27.07.2024 11:15

    Если ставите минус, то большая просьба отписаться, чтобы я знал что не так и мог исправить)
    Я всегда рад аргументированной критике


  1. wataru
    27.07.2024 11:15

    Тут такая же критика, как и к другой статье: объяснения не должны повторять код. Не показаны идеи и суть решения.

    Например, раз уж вы начали про окно, можно объяснять так:

    Скользящее окно: мы будем поддерживать самый длинный отрезок, который хороший и заканчивается в текущей правой позиции. Будем двигать эту позицию и пересчитывать левую. Ясно*, что левая граница при таком подходе будет двигаться только вправо. Теперь нам осталось научится быстро сдвигать левую границу. Можно двигать ее по одному символу, пока окно не станет хорошим. Осталось быстро научиться понимать, хорошее ли у нас окно. Это можно сделать считая, сколько каждых букв у нас есть в окне. Вот таким вот счетчиком/хеш-таблицей. Суммарно левая граница сдвинется максимум n раз, поэтому все решение будет O(n).

    Прийти к этому можно вот так:

    Описали наивное решение, которое для каждого начала двигает конец, пока не встретит повтор. Улучшение: при сдвиге левой границы правая в конце будет не хуже. Т.е. можно начинать уже с того же самого места, но тут надо поддерживать счетчик между итерациями. Потом можно сказать, что неважно, какую из границ перебирать, а какую корректировать под первую. Можно перебирать правую границу, а сдвигать левую. Но так код легче - ибо надо будет сдвигать только пока окно плохое, а не надо будет смотреть наперед.


    1. idsulik Автор
      27.07.2024 11:15

      Спасибо за обратную связь!
      Дважды перечитал, но к сожалению такое объяснение даже мне было трудно понимать, хотя я знаю как этот механизм работает.
      Но в целом я понял, что нужно поменять подход для текстовых объяснений.
      Буду рад, если получу подобный фидбек для видео)


      1. wataru
        27.07.2024 11:15

        В видео тоже самое. Можно во время объяснения еще и рисовать эти окна. Вы там что-то рисуете, но идею решения, опять же, не объясняете.


        1. idsulik Автор
          27.07.2024 11:15

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