Где применяется
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 и увеличивай счётчик. Пока в окне слишком много разных значений, убирай элемент слева и уменьшай его счётчик, удаляя ключ на нуле. Когда правило снова выполняется, записывай длину окна.
Проследим «самый длинный отрезок, где не больше 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 именно так.
Как узнать
Эти слова в условии значат этот паттерн
- самый длинный/короткий непрерывный подмассив или подстрока
- удовлетворяющий условию (сумма <= 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 счётчик каждого значения.
Держи последние k чисел в множестве и спрашивай, есть ли в нём новое число.
Задача урока с k = 2, только вместо букв типы фруктов.
Сужай слева, пока у только что вошедшей буквы счётчик снова не станет 1.
Окно подходит, пока его длина минус число самой частой буквы не больше k.
Расширяй, пока не покроешь все нужные буквы, потом сужай слева до самого короткого окна, которое их ещё покрывает.
Твоя задача (Tier 1)
Напиши функцию longestAtMostKDistinct(s, K).
Найди самый длинный отрезок букв подряд, в котором не больше K разных букв. Веди счёт каждой буквы в окне и сужай окно слева, когда разных букв становится слишком много.
- Вход:
s, строка, иK, сколько разных букв допустимо одновременно. Пример:"eceba",2. - Возвращает: длину самого длинного отрезка, в котором не больше
Kразных букв.
Шаги:
- Заведи пустую карту
count(буква -> сколько раз она встречается в окне), маркерleft = 0и лучшую длинуbest = 0. - Двигай
rightпо строке. Прибавляй 1 к счёту буквыs[right]. - Пока в карте больше
Kразных букв, отнимай 1 от счётаs[left]. Если этот счёт дошёл до0, удали букву из карты. Сдвиньleftна шаг вправо. - Если длина окна
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.
}
Ты здесь как гость
Тесты запускаются прямо здесь, без аккаунта. Код сохраняется только в этом браузере. Бесплатный аккаунт сохранит его на любом устройстве, отметит шаг пройденным и покажет, где ты в курсе.
Лестница подсказок
Застрял? Открывай по одной, но сначала попробуй сам.