Sliding Window

12 min de leituraMédioPython

Janela deslizante é dois ponteiros com memória: em vez de olhar todos os subarrays possíveis, você mantém um pedaço contíguo, guarda o estado dele e vai ajustando as bordas. O que era O(n²) vira O(n), e a conta nunca é refeita do zero.

O problema que faz a técnica nascer

Comece pelo pedido mais simples possível: dado o array [2, 3, 4, 5, 6, 7], qual é a maior soma de 3 elementos seguidos?

A solução óbvia é testar todas as janelas de tamanho 3:

Python
def maior_soma_forca_bruta(nums, k):
    melhor = float("-inf")
    for i in range(len(nums) - k + 1):
        soma = 0
        for j in range(i, i + k):   # refaz a soma inteira, toda vez
            soma += nums[j]
        melhor = max(melhor, soma)
    return melhor

Funciona. E desperdiça um absurdo de trabalho. Olhe as duas primeiras janelas:

[2, 3, 4]  5, 6, 7      soma = 9
 2, [3, 4, 5]  6, 7     soma = 12

Elas compartilham o 3 e o 4. Você acabou de somar esses dois números e vai somá-los de novo. Com k = 3 são 2 somas jogadas fora por janela; com k = 100, são 99. O custo é (n - k + 1) * k, que para k proporcional a n vira O(n·k), ou seja, quadrático.

A pergunta que destrava a técnica é: se eu já sei a soma da janela anterior, quanto custa a próxima?

A intuição: o que entra e o que sai

Da primeira janela para a segunda, uma coisa saiu (o 2) e uma coisa entrou (o 5). Todo o resto ficou igual.

soma_nova = soma_velha - nums[esquerda] + nums[direita]
          =     9      -      2         +      5        = 12

Uma subtração e uma soma. Duas operações, não k. E o mais importante: esse custo não muda se k for 3, 100 ou 10.000.

Essa é a técnica inteira. O resto da página é aprender a reconhecer onde ela se aplica e como escrever o laço sem errar as bordas.

Rode os dois lado a lado no mesmo array. O contador da esquerda é a força bruta relendo a janela toda; o da direita é a janela mantendo a soma. Aumente o k para 6 e veja o que acontece com cada um.

Visualizador · força bruta contra janela, no mesmo array
passo 1 de 25
0
2
1
3
·
2
4
·
3
5
·
4
6
·
5
7
·
6
1
·
7
9
·
operações da força bruta1
operações da janela1
trabalho economizado0%
maior soma (as duas)0

Primeira janela: os dois somam nums[0] = 2. Até aqui empatados, ninguém tem estado guardado ainda.

comparacao.py · O(n·k) contra O(n)
1# força bruta: refaz a soma inteira de cada janela
2for i in range(n - k + 1):
3 soma = 0
4 for j in range(i, i + k):
5 soma += nums[j] # k leituras por janela
6
7# janela deslizante: mantém a soma
8soma = sum(nums[:k])
9for direita in range(k, n):
10 soma += nums[direita] # entra 1
11 soma -= nums[direita - k] # sai 1
Variáveis
soma (força bruta)2
soma (janela)2
janela[0..2]
k3

passo · espaço roda

Guarde a imagem: a janela não é recalculada, ela é mantida. Você não olha para dentro dela a cada passo, você só registra quem entrou e quem saiu.

Construindo a janela fixa

Quando o enunciado dá o tamanho (k = 3, "de 3 caracteres", "de tamanho k"), a janela tem tamanho travado: a cada passo entra um elemento pela direita e sai um pela esquerda.

O visualizador abaixo mostra exatamente isso, com o código acompanhando linha a linha. Rode uma vez inteiro e repare no painel de variáveis: a soma muda a cada passo, mas nenhum passo percorre a janela de novo. Depois edite o array e o k e veja que o número de operações não acompanha o k.

Visualizador · janela fixa, a maior soma de k elementos seguidos
passo 1 de 18
0
3
esq
1
6
·
2
2
dir
3
8
·
4
1
·
5
4
·
6
1
·
7
5
·

Monto a primeira janela somando do zero: 3 + 6 + 2 = 11. É a única vez que eu faço isso, e me custou 3 leituras.

solucao.py
1def melhor_soma(nums, k):
2 soma = sum(nums[:k])
3 melhor = soma
4 esquerda = 0
5 for direita in range(k, len(nums)):
6 soma += nums[direita]
7 soma -= nums[esquerda]
8 esquerda += 1
9 melhor = max(melhor, soma)
10 return melhor
Variáveis
esquerda0
direita2
soma11
melhor11
tamanho (n)8
leituras da janela3
leituras da força bruta18
memória extraO(1)

passo · espaço roda

O laço tem uma sutileza que pega todo mundo na primeira vez: você precisa montar a primeira janela antes de começar a deslizar.

Python
def maior_soma(nums, k):
    soma = sum(nums[:k])        # monta a primeira janela: k operações, uma vez só
    melhor = soma
    for direita in range(k, len(nums)):
        soma += nums[direita]           # entra o novo
        soma -= nums[direita - k]       # sai o que ficou para trás
        melhor = max(melhor, soma)
    return melhor

O índice direita - k é o que sai. Se direita está em 3 e k é 3, sai o índice 0. Erre isso por um e você tem uma janela de tamanho k + 1 sem perceber, com um resultado que parece quase certo.

Construindo a janela variável

Agora o caso mais comum em entrevista: o enunciado não dá o tamanho, ele dá uma condição, e pergunta qual é o maior (ou o menor) pedaço que a respeita.

"Qual o maior subarray com soma menor ou igual a 15?" Aqui o tamanho é a resposta, não a entrada.

A mecânica muda em um ponto só: a borda esquerda não anda junto com a direita. Ela só anda quando precisa.

  • Cresça pela direita, sempre, incluindo nums[direita] no estado.
  • Encolha pela esquerda, enquanto a janela estiver inválida, removendo nums[esquerda].
  • Atualize a resposta quando a janela voltar a ser válida.

No visualizador abaixo a condição é soma <= k. Repare no comportamento que diferencia a janela variável: a esquerda fica parada por vários passos e depois anda várias casas de uma vez, quando a janela estoura.

Visualizador · janela variável, o maior subarray com soma ≤ k
passo 1 de 28
0
2
·
1
3
·
2
4
·
3
5
·
4
6
·
5
7
·
6
9
·

Janela vazia: esquerda e direita em 0, soma 0. Vou crescer pela direita enquanto a soma couber em 15.

solucao.py
1def maior_subarray(nums, k):
2 esquerda = 0
3 soma = 0
4 melhor = 0
5 for direita in range(len(nums)):
6 soma += nums[direita]
7 while soma > k:
8 soma -= nums[esquerda]
9 esquerda += 1
10 melhor = max(melhor, direita - esquerda + 1)
11 return melhor
Variáveis
esquerda0
direita-
soma0
melhor (tam.)0
tamanho (n)7
leituras da janela0
força bruta (pior caso)28
memória extraO(1)

passo · espaço roda

Acompanhando à mão em [2, 3, 4, 5, 6, 7] com limite 15:

PassoJanelaSomaO que acontece
1[2]2válida, melhor = 1
2[2, 3]5válida, melhor = 2
3[2, 3, 4]9válida, melhor = 3
4[2, 3, 4, 5]14válida, melhor = 4
5[2, 3, 4, 5, 6]20inválida, encolhe
6[3, 4, 5, 6]18ainda inválida, encolhe
7[4, 5, 6]15válida de novo, tamanho 3

A resposta é 4, do passo 4. Note que depois do encolhimento a janela nunca mais alcançou esse tamanho, e é por isso que a resposta é atualizada a cada janela válida, não só no fim.

O padrão generalizado

Os dois casos são o mesmo template. Vale a pena decorar este esqueleto, porque ele resolve a maioria dos problemas de janela variável trocando só três pedaços:

Python
def janela(nums):
    esquerda = 0
    estado = 0                       # 1. o que a janela guarda
    melhor = 0
    for direita in range(len(nums)):
        estado += nums[direita]      # 2. entra o elemento da direita
        while invalida(estado):      # 3. a condição do enunciado
            estado -= nums[esquerda] #    sai o elemento da esquerda
            esquerda += 1
        melhor = max(melhor, direita - esquerda + 1)   # 4. janela válida, atualiza
    return melhor

Os quatro encaixes:

  1. O estado. Uma soma, uma contagem, um dicionário de frequências. É o que precisa ser atualizado em O(1) na entrada e na saída.
  2. A entrada. Sempre acontece, sempre na direita.
  3. A condição. É a tradução literal do enunciado. estado > k, len(vistos) > 2, zeros > 1.
  4. A resposta. Para "a maior", atualize depois do while. Para "a menor", atualize dentro do while, porque é lá que a janela está apertada no limite.

Inicialize a resposta com cuidado. Se você procura um máximo, comece com float("-inf") ou 0 conforme o problema aceite ou não janela vazia. Se procura um mínimo, comece com float("inf") e lembre de tratar o caso "nenhuma janela válida existe", que costuma pedir retorno 0.

Como reconhecer que é janela deslizante

Esta é a parte que mais economiza tempo numa prova. Existem pistas quase infalíveis no enunciado, e elas vêm em conjunto:

É janela quando

O enunciado fala de subarray ou substring (pedaço contíguo), existe uma condição que torna o pedaço válido ou inválido, e ele pede um extremo ou uma contagem: o maior, o menor, quantos existem.

Não é janela quando

O enunciado fala de subsequence, subset ou permutação. Esses não exigem adjacência, então não existe "janela" para deslizar. O caminho costuma ser backtracking ou programação dinâmica.

Se você ficou em dúvida entre os termos, vale a leitura de Subarray, Substring, Subsequence e Subset, que separa os quatro com exemplos.

Há também uma distinção fina que arruma a cabeça de vez:

Janela deslizante se importa com tudo que está dentro dela. Two Pointers só se importa com onde os ponteiros estão.

No Two Pointers clássico (palíndromo, Two Sum em array ordenado), a decisão sai da comparação entre os dois extremos; o miolo não entra na conta. Na janela deslizante, o que decide se ela é válida é o agregado de todos os elementos de dentro, e é por isso que sempre existe um estado sendo mantido.

Complexidade

O argumento que garante o O(n) é curto e vale entender, porque ele aparece de novo em outras técnicas.

Olhando o código da janela variável, dá a impressão de ser O(n²): tem um while dentro de um for. Não é. Cada ponteiro só anda para a frente, e nenhum dos dois passa de n. A direita anda n vezes no total, e a esquerda anda no máximo n vezes somando todas as iterações do while. São no máximo 2n movimentos, ou seja, O(n).

AbordagemTempoEspaço
Força bruta (todos os subarrays)O(n·k) ou O(n²)O(1)
Janela fixaO(n)O(1)
Janela variávelO(n)O(1)
Janela com dicionário de frequênciasO(n)O(k) ou O(1) se o alfabeto é fixo

Para a janela fixa, o número exato de janelas é n - k + 1, e o custo por janela é constante.

Esse raciocínio tem nome: análise amortizada. Um passo isolado pode custar caro (o while encolhendo cinco vezes seguidas), mas o custo total, dividido pelo número de passos, é constante. O mesmo argumento aparece no array dinâmico, em Arrays e Listas.

As armadilhas

Números negativos quebram a janela variável. Toda a lógica depende de "encolher pela esquerda só melhora". Com valores negativos, remover um elemento pode aumentar a soma, e uma janela maior pode ter soma menor que uma pequena. O while deixa de fazer sentido, porque encolher não garante voltar a ser válida. Nesse caso o caminho costuma ser Prefix Sum com hashmap.

Não ordene o array. É a armadilha mais cara desta técnica: ordenar destrói a adjacência, e subarray é definido por adjacência. Depois do sort, o pedaço que você achou não existe mais no array original. Ordenar é livre em problema de subset; é proibido em problema de subarray.

A resposta atualizada no lugar errado. Para "a maior janela válida", atualize depois do while. Para "a menor janela válida", atualize dentro. Trocar os dois dá um resultado plausível e errado, do tipo que passa nos exemplos do enunciado e falha no caso 47.

O estado que não é O(1) de manter. Se para saber se a janela é válida você precisa varrer o que está dentro dela, você não tem uma janela deslizante, tem uma força bruta com bordas móveis. O estado precisa ser atualizável só com quem entrou e quem saiu: soma, contagem, dicionário de frequências, tudo em O(1).

Esquecer o caso "nenhuma janela serve". Se nenhuma janela válida existe, o que se retorna? Zero, -1, string vazia? O enunciado quase sempre diz, e quase todo mundo pula essa linha.

Como praticar

Antes de ir para o LeetCode, use os visualizadores acima para prever e conferir. Faça assim: decida a resposta na sua cabeça primeiro, depois rode.

  • Na janela fixa, ponha k igual ao tamanho do array. Quantos passos o laço dá? (Resposta: um, a janela nunca desliza.)
  • Ainda na fixa, ponha k = 1. A janela vira o quê? (O próprio maior elemento.)
  • Na variável, edite o array para todos os valores serem maiores que o limite. Nenhuma janela é válida: o que o visualizador devolve?
  • Na variável, ponha um limite gigante. A esquerda chega a andar alguma vez?

Casos de borda que valem testar em qualquer solução sua:

CasoO que costuma quebrar
array vaziosum(nums[:k]) devolve 0 e o retorno mente
k > n na janela fixanão existe janela nenhuma, precisa de guarda
todos os elementos iguaisverifica se a resposta atualiza sempre, não só na mudança
um elemento sótesta as bordas do laço

Depois disso, resolva os problemas na ordem da lista abaixo. Os três primeiros são de janela fixa e servem para firmar as bordas; os seguintes são de janela variável, e o Minimum Window Substring é o clássico que junta janela com dicionário de frequências, o passo final da técnica.

Vídeo da aula

Direto do canal da comunidade Craft & Code Club · 2:08:22.

Problemas para praticar

Na ordem em que recomendamos resolver. Marque os que você já fez, fica salvo aqui.

Travou em algum passo? Traga sua questão para o Discord da comunidade ou para os encontros semanais.

Entrar
Concluiu este tópico?
Marque para acompanhar seu progresso.

Este tópico também tem página própria, fora deste roadmap: Sliding Window.