Все темы
23

Окно, которое помнит своё содержимое

Tier 1Бесплатно

Где применяется

1. Задача

Нужен самый длинный отрезок текста, в котором не больше 2 разных букв. Для "eceba" ответ "ece", длина 3.

В прошлом уроке окно хранило одно число, сумму. Одним числом на этот вопрос не ответить. Условие «не больше 2 разных букв» касается содержимого окна: какие буквы в нём есть и сколько каждой. Сумма тут ничего не скажет. Окно должно помнить, что в нём лежит.

Так же устроен ограничитель частоты (программа, которая не даёт клиенту слать запросы слишком часто). Он спрашивает: сколько запросов этот клиент сделал за последние 60 секунд? Это подсчёт того, что сейчас лежит в движущемся окне времени.

2. Простыми словами

Теперь правило касается того, что лежит внутри отрезка. Например: «не больше 2 разных букв одновременно». Одна сумма на это не ответит. Нужно знать, какие буквы есть в окне и сколько каждой.

Поэтому ты ведёшь счёт для каждой буквы. Возьми "eceba". Окно "ece": e две штуки, c одна, разных букв 2, всё в порядке. Добавляешь b, разных букв стало 3. Тогда убираешь буквы слева, пока одна из букв не исчезнет из окна совсем. Убрал первую e: e осталась одна, разных по-прежнему 3. Убрал c: её счёт стал 0, c ушла из окна. Разных снова 2. Число разных букв в счёте и есть то, что ты сверяешь с правилом, когда расширяешь и сужаешь окно.

3. Идея

Храни Map (таблицу «ключ → счётчик», из урока про Map) для элементов, которые сейчас в окне. Когда right добавляет элемент в окно, увеличь его счётчик. Когда left убирает элемент, уменьши счётчик. Если счётчик дошёл до нуля, удали ключ. Тогда map.size (число ключей в Map) равен числу разных значений в окне. С этим числом ты и сверяешь правило.

Удалять ключ на нуле обязательно. Допустим, ключ со счётчиком 0 остался в Map. Тогда map.size посчитает букву, которой в окне уже нет. Проверка на число разных букв ошибётся, и окно станет шире, чем можно.

Порядок такой. Сдвигай right и увеличивай счётчик. Пока в окне слишком много разных значений, убирай элемент слева и уменьшай его счётчик, удаляя ключ на нуле. Когда правило снова выполняется, записывай длину окна.

e c e b a e → 2 c → 1 size 2

Проследим «самый длинный отрезок, где не больше 2 разных букв» на "eceba".

  • Добавили e: {e:1}, длина 1.
  • Добавили c: {e:1, c:1}, длина 2.
  • Добавили e: {e:2, c:1}, длина 3 ("ece").
  • Добавили b: разных стало 3. Убрали слева e: {e:1, c:1, b:1}, разных всё ещё 3. Убрали c: {e:1, b:1}, разных 2. Окно "eb", длина 2.
  • Добавили a: разных снова 3. Убрали e: {b:1, a:1}. Окно "ba", длина 2.

Лучшая длина 3.

4. Во что это обходится

Время O(n). right добавляет каждый элемент один раз, left убирает каждый не больше одного раза. Каждая операция с Map занимает примерно один шаг. Память O(k): в Map лежит не больше ключей, чем разных значений может быть в окне. Это число ограничено K или размером алфавита (числом всех возможных букв).

6. Где это встречается

Ограничители частоты запросов к API и защита от злоупотреблений ведут счётчики в скользящем окне для каждого клиента или адреса. Мониторинг следит за правилом «не больше K разных пользователей или IP-адресов за окно». Поиск самой длинной подстроки без повторов использует то же окно с более строгим правилом. Если решение для движущегося окна зависит от того, какие элементы в нём лежат и сколько каждого, окно хранит Map именно так.

Как узнать

Скользящее окно (переменное) (Sliding window (variable))

Эти слова в условии значат этот паттерн

  • самый длинный/короткий непрерывный подмассив или подстрока
  • удовлетворяющий условию (сумма <= K, не более, без нарушения лимита)
  • не более K различных, без повторов, содержит все из X
  • непрерывный отрезок, не подпоследовательность

Похоже, но не то

  • Скользящее окно (фиксированный размер) (Sliding window (fixed size))

    Фиксированное окно шириной k от первого шага до последнего. Это окно выбирает ширину само.

  • Два указателя (с концов) (Two pointers (opposite ends))

    Те стартуют врозь на отсортированных данных и сходятся к паре. Здесь оба конца идут в одну сторону.

  • Префиксные суммы (Prefix sums)

    Префиксные суммы работают с отрицательными числами и любыми диапазонами. Сужение же считает, что удаление помогает.

  • Хеш-множество / словарь (Hash set / map)

    Словарь сам по себе описывает всю коллекцию. Окну нужны счётчики только для живого отрезка.

n до 1e6 и один лучший отрезок дают O(n). Каждый конец сдвигается не более n раз.

На LeetCode

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

Твоя задача (Tier 1)

Напиши функцию longestAtMostKDistinct(s, K).

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

  • Вход: s, строка, и K, сколько разных букв допустимо одновременно. Пример: "eceba", 2.
  • Возвращает: длину самого длинного отрезка, в котором не больше K разных букв.

Шаги:

  1. Заведи пустую карту count (буква -> сколько раз она встречается в окне), маркер left = 0 и лучшую длину best = 0.
  2. Двигай right по строке. Прибавляй 1 к счёту буквы s[right].
  3. Пока в карте больше K разных букв, отнимай 1 от счёта s[left]. Если этот счёт дошёл до 0, удали букву из карты. Сдвинь left на шаг вправо.
  4. Если длина окна right - left + 1 больше best, обнови best. В конце верни best.

Готово, когда проходят все тесты:

  • "eceba", 2 -> 3 ("ece")
  • "aa", 1 -> 2
  • "abcabc", 2 -> 2
  • "aabb", 5 -> 4 (K больше, чем разных букв в строке)
  • "aaabbb", 1 -> 3
  • "abc", 0 -> 0 (ни одной буквы брать нельзя)
  • "", 2 -> 0 (пустая строка)
  • "a", 1 -> 1

Практика

longestAtMostKDistinct

longestAtMostKDistinct(s, K) -> number (length of longest substring of s with at most K distinct characters)

export function longestAtMostKDistinct(s, K) {
  // Your code here. The task is described in the box above the editor.
}

Ты здесь как гость

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

Лестница подсказок

Застрял? Открывай по одной, но сначала попробуй сам.

    Мы используем необходимые куки для работы сайта (вход и язык). Если ты согласишься, мы также загрузим Google Analytics, чтобы видеть, какие страницы читают, и Google reCAPTCHA для защиты форм обратной связи и сообщений об ошибке от спама. Политика конфиденциальности