Loading [MathJax]/jax/output/HTML-CSS/jax.js

Técnicas de Programação - Soma Máxima em Intervalo

Aula por Arthur Lobo

Nessa aula, vamos falar sobre técnicas usadas para encontrar a maior soma de um intervalo contínuo em um vetor e suas aplicações.

Motivação inicial

Vamos imaginar o seguinte problema: Dado um vetor inicial de tamanho N, A1,A2...AN, com números inteiros entre 100 e 100, responda qual é a maior soma de um intervalo contínuo. Formalmente, diga qual o maior valor de Ai+Ai+1+...+Aj, com 1ijN.

Se quiser ler o enunciado inteiro e com mais detalhes, clique aqui.

Uma primeira ideia que podemos ter para resolver o problema é testar todos os intervalos (i,j) (começando em i e terminando em j) e ver qual deles tem a maior soma. Para isso podemos criar uma função soma(i,j), que retorna a soma de i até j fazendo um for no intervalo inteiro e somando os valores correspondentes Essa solução tem a complexidade O(N3), pois testamos O(N2) pares e a função soma(i,j) funciona em O(N).

Ainda podemos melhorar essa solução usando soma de prefixo: primeiro calculamos o vetor de somas de prefixo S, em que Si guardará a soma A1+A2+...+Ai, e então podemos fazer com que a função soma(i,j) seja respondida em O(1), já que podemos apenas retornar SjSi1. Dessa maneira, podemos fazer essa ideia rodar em O(N2), mas ainda podemos fazer melhor!

Algoritmo de Kadane

A ideia do algoritmo de Kadane é realizarmos uma solução gulosa. Vamos iterar do 1 até o N, quando estivermos em i, sempre manteremos 2 valores: a resposta máxima até aqui e o valor do intervalo de soma máximo terminando em i, guardaremos eles em resp e soma, respectivamente.

O algoritmo é o seguinte, digamos que acabamos de calcular a iteração de i1, na iteração de i vamos seguir os seguintes passos:

  • Primeiro, verificamos se soma é menor que 0, caso seja, fazemos com que ela retorne a ser 0. A ideia é que sempre que o intervalo ficar negativo, vamos descartar ele inteiro.
  • Depois, adicionamos Ai em soma, fazendo soma=soma+Ai. Nesse momento, é garantido que soma guarda o valor do intervalo de soma máxima que termina em i (vamos provar isso logo em seguida).
  • Por último, iremos atualizar resp caso soma seja maior que resp fazendo resp=max(resp,soma)

A intuição por trás do algoritmo de Kadane é que sempre que a soma do intervalo fica menor que 0, nós resetamos ela, fazendo com que volte a ser 0. Isso faz com que, sempre que estamos em i, soma guardá o valor do intervalo de soma máxima que termina em i, portanto, pegamos o intervalo de soma máxima do vetor inteiro. A prova do porque podemos resetar (fazer o início e o final do intervalo ser i) fica como um exercício para o leitor, mas a ideia é imaginar o que aconteceria se fosse melhor mudar o início do intervalo para uma posição diferente de i.

// Curso NOIC de Informática - Técnicas de Programação - Soma máxima em intervalo
// Algoritmo de Kadane
// Escrito por Arthur Lobo
#include<bits/stdc++.h>
using namespace std;
const int infinito = 1000000000;
const int maxn = 5e5+10;
int n, a[maxn];
int main() {
cin >> n;
for(int i = 1; i <= n; i++) {
cin >> a[i];
}
int soma = 0; // comecamos a soma como 0
int resp = -infinito;
for(int i = 1; i <= n; i++) {
// sempre que o intervalo terminando em i-1
// for menor que 0, resetamos ele para 0
if(soma < 0) soma = 0;
soma+= a[i];
resp = max(resp,soma);
}
cout << resp << endl;
}

Soma de Prefixo

Além da solução com o algoritmo de Kadane, esse problema também pode ser resolvido utilizando somas de prefixo. Além de ser mais intuitiva, a solução com soma de prefixo pode ser usada em diversas variações do problema. Agora vamos olhar ela mais de perto:

No início da aula, falamos sobre fazer um vetor de somas de prefixos S, em que Si guarda A1+A2+...+Ai (em particular, S0=0), e usar isso para descobrir a soma entre quaisquer de i até j (ij) fazendo a conta SjSi1. Portanto, queremos encontrar o valor máximo de SjSi1.

Assim como no algoritmo de Kadane, nosso objetivo vai ser descobrir qual a maior soma de intervalo terminando em cada j. Para isso, vamos fixar o j na nossa conta e olhar apenas para o i, Fazemos isso para todo j de 1 até N.

Queremos encontrar o maior valor de SjSi1, a partir do momento que fixamos um j, o valor do primeiro termo (Sj) é fixado também, então nosso objetivo virou apenas maximizar o segundo termo, ou seja, Si1, e maximizar isso é o mesmo que minimizar Si1, já que está ele subtraindo. Como temos que 1ij0i1j1, então queremos que Si1 seja o menor valor entre S0,S1,...,Sj1.

Para computarmos isso em cada j de 1 até N, podemos iterar nessa ordem, guardando sempre o valor do menor prefixo até j1 na variável menor, ao mesmo tempo que vamos atualizando a resposta em cada iteração, guardando ela em resp:

Começamos com menor=0 e resp=infinito (com infinito sendo um número muito grande), e para cada j de 1 até N nosso algoritmo realizará os seguintes passos:

  • Primeiro, vemos se Sj1<menor, e caso for, o atualizamos fazendo menor=min(menor,Sj1).
  • Agora sabemos que o valor do intervalo de soma máxima que termina em j é Sjmenor, então comparamos a resposta atual com essa possível resposta fazendo resp=max(resp,Sjmenor).

A intuição por trás do algoritmo é que sempre queremos subtrair o menor valor possível de Sj, então a melhor opção é pegar o prefixo com a menor soma até j1. Vale ressaltar que o problema inicial diz que temos que pegar pelo menos um valor do vetor, então se todos os elementos em A fossem negativos, nossa resposta seria negativa. Mas caso um problema fale que podemos escolher não pegar nenhum valor do vetor, então caso todos os elementos em A sejam negativos, a resposta será 0.

// Curso NOIC de Informática - Técnicas de Programação - Soma máxima em intervalo
// Soma de Prefixo
// Escrito por Arthur Lobo
#include<bits/stdc++.h>
using namespace std;
const int infinito = 1000000000;
const int maxn = 5e5+10;
int n, a[maxn], s[maxn];
int main() {
cin >> n;
for(int i = 1; i <= n; i++) {
cin >> a[i];
s[i] = s[i-1]+a[i];
}
int menor = 0; // comecamos menor como S[0], ou seja, 0
int resp = -infinito;
for(int i = 1; i <= n; i++) {
// vemos se s[i-1] é < que menor,
// se for, o substituimos
menor = min(menor,s[i-1]);
resp = max(resp,s[i]-menor);
}
cout << resp << endl;
}

Problemas para praticar

Agora é a sua vez! Tente fazer esses problemas que usam os algoritmos ensinados na aula de hoje ou ideias semelhantes: