Segmento de soma máxima

Considere o seguinte problema do segmento de soma máxima: dado um vetor de A[1 .. n] de números inteiros encontrar um segmento do vetor que tenha soma máxima.

Exemplo A:  Vou ao cassino diariamente. Todo dia ganho uma certa quantia. Infelizmente a quantia é muitas vezes negativa. Meus amigos querem saber qual foi a sequência de dias, na minha história de idas ao cassino, em que meu ganho acumulado foi máximo. A tabela abaixo dá um exemplo para um período de 8 dias. Nesse exemplo, o ganho acumulado foi máximo ($35) no período que vai do dia 3 ao dia 6.
20 −30 15 −10 30 −20 −30 30

Embora simples, o problema do segmento de soma máxima é um bom laboratório de algoritmos. Esta página estuda quatro algoritmos para o problema. Cada algoritmo usa uma estratégia diferente; cada algoritmo é mais eficiente que o anterior.

A página contém lições de caráter geral sobre (1) a estimativa do desempenho de algoritmos, (2) o método da divisão e conquista e (3) o método de programação dinâmica.

Esta página foi baseada num capítulo do excelente livro de Bentley.

O problema

Convém introduzir uma terminologia auxiliar para tratar do problema. Diremos que um segmento de um vetor A[1 .. n] é qualquer subvetor da forma A[i .. k], com 1 ≤ ikn. A condição i ≤ k garante que segmentos não são vazios. (Teria sido mais natural aceitar segmentos vazios, mas isso poderia tornar a discussão excessivamente sutil em algumas situações.)

soma de um segmento A[i .. k] é o número A[i] + A[i+1] + ⋅⋅⋅ + A[k]. A altura de um vetor A[1 .. n] é a soma de um segmento de soma máxima. Em outras palavras, a altura de A[1 .. n] é o maior número do tipo A[i] + A[i+1] + ⋅⋅⋅ + A[k] com 1 ≤ ikn. (Por exemplo, a altura do vetor no exemplo A é 15 − 10 + 30 = 35.)

Problema do Segmento de Soma Máxima:  Calcular a altura de um vetor A[1 .. n] de números inteiros.

Em virtude da maneira como o conceito de segmento foi definido, a única instância do problema que não tem solução é aquela em que o vetor A[1 .. n] é vazio. Por isso, vamos supor, implicitamente, que n ≥ 1 no enunciado do problema. O problema tem outras sutilezas, que os exercícios abaixo ajudam a entender.

Exemplo B:  O vetor da figura tem altura 35. O subvetor A[3 .. 5] é um segmento de soma máxima.
1 2 3 4 5 6 7 8
 20   −30   15   −10   30   −20   −30   30 

Exercícios 1

  1. Que acontece se todos os elementos do vetor são positivos? Que acontece se todos os elementos do vetor são negativos? Que acontece se os elementos do vetor são, alternadamente, +1 e −1?
  2. Que aconteceria se permitíssemos segmentos vazios?
  3. ★ Seja x a altura de A[1 .. n]. É verdade que x ≥ A[i] para todo i entre 1 e n?  É verdade que x ≥ 0?
  4. Digamos que A[i .. k] é um segmento de soma máxima de um vetor A[1 .. n]. É verdade que A[i] ≥ 0? É verdade que A[k] ≥ 0?
  5. Digamos que um segmento A[i .. k] de A[1 .. n] tem soma maximal se A[i] + ⋅⋅⋅ + A[k] ≥ A[i'] + ⋅⋅⋅ + A[k'] sempre que 1 ≤ i' ≤ i ≤ k ≤ k' ≤ n. Mostre como encontrar um segmento de soma maximal.

Primeiro algoritmo

O algoritmo óbvio para o problema do segmento de soma máxima examina, sistematicamente, todos os segmentos de A[1 .. n] e escolhe o que tiver maior soma.

Altura-Zero (A, n)   ▷ supõe n ≥ 1
1 . x := A[1]
2 . para i := 1 até n
3 .ooo para k := i até n
4 .oooooo s := 0
5 .oooooo para j := i até k
6 .ooooooooo s := s + A[j]
7 .oooooo x := max (x, s)
8 . devolva x

O operação max devolve o valor do maior de seus argumentos. Portanto, x := max (x, s) equivale a se s > x então x := s.

O algoritmo está obviamente correto pois implementa literalmente a definição de altura. (A incialização de x na linha 1 está correta pois a altura do vetor é pelo menos A[1].) No início de cada execução da linha 7, s é a soma do segmento A[i .. k]. Como i varia de 1 até n e k varia de i até n, o valor de x na linha 8 é a altura do vetor A[1 .. n].

Desempenho.  Vamos supor que cada execução de qualquer das linhas do pseudocódigo consome uma unidade de tempo. Assim, o número de execuções de cada linha é proporcional ao consumo de tempo da linha. O consumo total de tempo do algoritmo é proporcional à soma dos números de execuções das linhas.

Comecemos a análise pela linha 6, pois ela é a mais executada. Para cada valor fixo de i e de k, a linha 6 é executada k − i + 1 vezes. Para cada i fixo, o número de execuções da linha é

i ≤ k ≤ n (k − i + 1) = 1 + 2 + 3 + ⋅⋅⋅ + (n −i + 1)
  = ½ (n − i + 1)(n − i + 2) .

Como i varia de 1 a n, o número total de execuções da linha é a metade de

 1 ≤ i ≤ n (n − i + 1)(n − i + 2) =  1≤ m ≤ n m(m+1)
  =  1≤ m ≤ n m²  +  ∑ 1≤ m ≤ n m
  = n(n+1)(2n+1)/6 + n(n+1)/2
  = an³ + bn² + cn + d ,

sendo a, b, c e d constantes e a > 0.  Esse é o número de execuções da linha 6. O número total de execuções de cada uma das demais linhas não passa de n². Por exemplo, o número total de execuções da linha 7 é  1≤ i ≤ n (n−i+1) = ½ n(n+1).

Concluímos assim que n³ é o termo dominante no consumo de tempo total do algoritmo. Portanto, para valores grandes de n, o consumo de tempo é praticamente proporcional a n³. Assim, o desempenho do algoritmo está em

Θ(n³)

tanto no pior caso quanto no melhor. O algoritmo é, portanto, cúbico.

Exercícios 2

  1. Onde o algoritmo Altura-Zero usa a hipótese n ≥ 1?
  2. ★ Na linha 1 do algoritmo Altura-Zero, que acontece se trocarmos x := A[1] por x := 0 ou por x := A[2] ou por x := A[n]?

Segundo algoritmo

O algoritmo Altura-Zero é muito ineficiente pois a soma de cada segmento é recalculada muitas vezes. Por exemplo, a soma de A[100 .. 200] é refeita durante o cálculo da soma de todos os segmentos A[i .. k] que têm i ≤ 100 e k ≥ 200. Nosso segundo algoritmo procura não recalcular a soma de alguns dos segmentos:

Altura-Um (A, n)   n ≥ 1
1 . x := A[1]
2 . para q := 2 até n
3 .ooo s := 0
4 .ooo para j := q decrescendo até 1
5 .oooooo s := s + A[j]
6 .oooooo x := max (x, s)
7 . devolva x

O algoritmo está correto.  A cada passagem pela linha 2, imediatamente antes da comparação de q com n, vale temos a seguinte propriedade invariante:

x é a altura do vetor A[1 .. q−1].

Essa propriedade vale quando q = 2 pois, por definição, segmentos não são vazios. Suponha agora que a propriedade vale no início de uma iteração qualquer. Observe que o bloco de linhas 3-6 examina todos os segmentos que terminam no índice q e atualiza o valor de x de acordo. Portanto, a propriedade continua valendo no início da iteração seguinte.

A propriedade invariante mostra que o algoritmo está correto, pois na última passagem pela linha 2 teremos q = n + 1 e portanto x é a altura do A[1 .. n].

Desempenho.  Suponhamos que cada execução de qualquer das linhas do pseudocódigo consome uma unidade de tempo. Então o consumo total de tempo é proporcional à soma dos números de execuções de todas as linhas.

Para cada valor fixo de q, o bloco de linhas 5-6 é executado q vezes. Como q cresce de 2 até n, o número total de execuções do bloco de linhas 5-6 é

 2 ≤ q ≤ n q = 2 + 3 + 4 + ⋅⋅⋅ + n
  = ½ (n² + n − 2) .

O número total de execuções de cada uma das demais linhas é essencialmente igual a n. (Por exemplo, o número total de execuções da linha 3 é n − 1 e o número total de execuções da linha 2 é n.) Portanto, o termo dominante no consumo total de tempo do algoritmo é n² (tanto no pior quanto no melhor caso). Logo, o consumo de tempo está em

Θ(n²)

e portanto o algoritmo é quadrático.

Exercícios 3

  1. ★ Qual a diferença entre provar que um algoritmo está correto e descrever os passos do algoritmo em português (o algoritmo faz isso, depois faz aquilo …, etc.)?
  2. Que acontece se a linha 4 do algoritmo Altura-Um for trocada por para j := 1 até q? A indentação da linha 6 está correta?
  3. Modifique o algoritmo Altura-Um de modo que ele devolva um par (i, k) de índices tal que a soma A[i] + ⋅⋅⋅ + A[k] é a altura de A[1 .. n].
  4. Escreva um algoritmo análogo a Altura-Um para tratar da variante do problema que admite segmentos vazios. Nessa variante, um segmento A[i .. k] é vazio se i = k + 1 e a soma de um segmento vazio é 0.

Terceiro algoritmo: divisão e conquista

O algoritmo Altura-Um é ineficiente porque a soma de alguns segmentos é calculada mais de uma vez. Por exemplo, a soma de A[1 .. 100] é refeita durante o cálculo das somas de A[1 .. 101], A[1 .. 102], etc. Nosso terceiro algoritmo procura remediar essa ineficiência.

O algoritmo usa o método da divisão e conquista: divide a instância dada ao meio, resolve cada uma das metades e finalmente combina as duas soluções.

Para descrever o algoritmo, é preciso que o índice do primeiro elemento do vetor A seja variável. Portanto, o enunciado do problema precisa ser generalizado como segue: calcular a altura de um vetor A[p .. r] de números inteiros. (Assim, o problema original é uma coleção de instâncias do problema generalizado.) Como já fizemos acima, vamos supor que o vetor não é vazio, ou seja, que p ≤ r.

Altura-DC (A, p, r)   p ≤ r
01 . se p = r
02 .ooo devolva A[p] e pare
03 . q := ⌊(p + r)/2⌋
04 . x′ := Altura-DC (A, p, q)
05 . x″ := Altura-DC (A, q+1, r)
06 . y′ := s := A[q]
07 . para i := q − 1 decrescendo até p
08 .ooo s := A[i] + s
09 .ooo y′ := max (y′, s)
10 . y″ := s := A[q+1]
11 . para j := q + 2 até r
12 .ooo s := s + A[ j]
13 .ooo y″ := max (y″, s)
14 . x := max (x′, y′ + y″, x″)
15 . devolva x

O algoritmo está correto.  Se p = r, é claro que o algoritmo dá a resposta correta. Suponha agora que p < r. Depois da linha 4, por hipótese de indução, x′ é a altura de A[p .. q]. Depois da linha 5, x″ é a altura de A[q+1 .. r]. Depois do bloco de linhas 6-13, y′ + y″ é a maior soma da forma A[i] + ⋅⋅⋅ + A[ j] com iq < j6. (Veja exercício abaixo.)

Resumindo, y′ + y″ cuida dos segmentos que contêm A[q] e A[q + 1], enquanto x′ e x″ cuidam de todos os demais segmentos. Concluímos assim que o número x calculado na linha 14 é a altura correta de A[p .. r].

Exemplo C:  Início da linha 14 do algoritmo Altura-DC. Nesse ponto, x′ = 20, x″ = 30, y′ = 5y″ = 30.
p q q+1 r
 20   −30   15   −10   30   −20   −30   30 

Desempenho.  Seja T(n) o consumo de tempo do algoritmo quando aplicado a um vetor de tamanho n = rp+1. O bloco de linhas 6-13 examina cada elemento de A[p .. r] uma só vez e portanto consome tempo proporcional a n. Temos então

  T(n) = T(⌈n/2⌉) + T(⌊n/2⌋) + n . (A)

A parcela T(⌈n/2⌉) descreve o consumo da linha 4, a parcela T(⌊n/2⌋) descreve o consumo da linha 5, e a parcela n resume o consumo das demais linhas. (Veja um dos exercícios abaixo.) Se adotarmos o valor inicial T(1) = 1, como é razoável, teremos a seguinte tabela para valores pequenos de n:

n) 1 2 3 4 5
T(n) 1 4 8 12 17

Precisamos de uma fórmula fechada para T. Poderíamos recorrer ao Teorema Mestre para mostrar que T(n) = Θ(n lg n), mas é mais instrutivo verificar isso diretamente. Começamos por mostrar que, para todo número natural n maior que 1,

T(n)  ≤  2 n lg n.

Eis a prova:  Quando n = 2, a desigualdade vale porque T(n) = T(2) = 4 ≤ 2⋅2⋅lg 2 = 2 n lg n. Quando n = 3, a desigualdade vale pois T(n) = T(3) = 8 < 9.5 < 2⋅3 < 2⋅3⋅lg 3 = 2 n lg n. Suponha agora que n ≥ 4 e que a desigualdade vale para argumentos de T menores que n. Então

T(n) = T(⌈n/2⌉) + T(⌊n/2⌋) + n
  2 ⌈n/2⌉ lg ⌈n/2⌉ + 2 ⌊n/2⌋ lg ⌊n/2⌋ + n
  2 (n/2⌉ + ⌊n/2⌋) lg ⌈n/2⌉ + n
  2 n lg (2n/3) + n
  = 2 n(lg n + lg (2/3)) + n
  < 2 n(lg n − 0.5) + n
  = 2 n lg nn + n
  = 2 n lg n ,  CQD.

Um cálculo semelhante prova que T(n) ≥ ½ n lg n para todo n natural positivo. Portanto,

T(n) = Θ(n lg n) .

Concluímos assim que o algoritmo Altura-DC é linearítmico.

Exercícios 4

  1. ★ Qual a diferença entre provar que um algoritmo está correto e descrever os passos do algoritmo em português (o algoritmo faz isso, depois faz aquilo …, etc.)?
  2. É verdade que ⌊(p + r)/2⌋ = p + ⌊(r − p)/2⌋ ?
  3. É verdade que p ≤ q na linha 4?  É verdade que q+1 ≤ r na linha 5?  (Por que isso é relevante?)
  4. ★ Considere a seguinte versão alternativa do algoritmo Altura-DC: apague as linhas 6 a 13 e troque a linha 14 por x := max (x′, x″). Essa versão alternativa está correta?
  5. Prove que depois do bloco de linhas 6-13 do algoritmo Altura-DC, y′+y″ é a maior soma dentre todas as que têm a forma A[i] + ⋅⋅⋅ + A[ j] com iq < j.
  6. Por que não trocar q + 1 por q nas linhas 5 e 10 do algoritmo Altura-DC?
  7. ★ Se cada execução de cada linha de Altura-DC consumir 1 unidade de tempo, as linhas 1-3 e 6-15 do algoritmo consumirão 5n + 1 unidades de tempo. Reescreva a recorrência (A) sob essa hipótese. Mostre que a solução da nova recorrência está em Θ(n lg n) .

Quarto algoritmo: programação dinâmica

Embora mais eficiente que Altura-Um, o algoritmo Altura-DC tem suas próprias ineficiências, pois refaz alguns cálculos várias vezes (por exemplo, a soma de A[i .. q] nas linhas 6-9 já foi calculada durante a execução da linha 4). O próximo algoritmo procura eliminar essa ineficiência.

Nosso quarto algoritmo usa o método de programação dinâmica, que consiste em armazenar os resultados de cálculos intermediários numa tabela e assim evitar que eles sejam refeitos.

Para aplicar o método, será preciso introduzir o seguinte problema auxiliar, que restringe a atenção aos segmentos finais do vetor: dado um vetor A[1 .. q] com q ≥ 1, encontrar a maior soma da forma

A[i] + ⋅⋅⋅ + A[q] ,

com 1 ≤ iq. Para facilitar a discussão, diremos que a maior soma dessa forma é a semialtura do vetor A[1 .. q]. A altura do vetor A[1 .. n] é o máximo das semialturas de A[1 .. n], A[1 .. n−1], A[1 .. n−2], etc.

A semialtura de um vetor tem uma propriedade recursiva que a altura não tem: se A[i .. q] corresponde à semialtura de A[1 .. q] e i < q então A[i .. q−1] corresponde à semialtura de A[1 .. q−1]. (De fato, se assim não fosse, teríamos A[h] + ⋅⋅⋅ + A[q − 1] > A[i] + ⋅⋅⋅ + A[q − 1] para algum hq − 1 e portanto teríamos A[h] + ⋅⋅⋅ + A[q−1] + A[q] > A[i] + ⋅⋅⋅ + A[q−1] + A[q], o que é impossível.)

Se denotarmos a semialtura de A[1 .. q] por S[q], podemos resumir a propriedade recursiva por meio de uma recorrência: para qualquer vetor A[1 .. q],

S[q] = max (S[q−1] + A[q] , A[q]) .

Em outras palavras, S[q] = S[q−1] + A[q] a menos que S[q − 1] seja negativo, caso em que S[q] = A[q]. Essa recorrência serve de base para o seguinte algoritmo, que calcula a altura de A[1 .. n]:

Altura-PD (A, n)   n ≥ 1
1 . S[1] := A[1]
2 . para q := 2 até n
3 .ooo S[q] := A[q]
4 .ooo se S[q−1] ≥ 0
5 .oooooo S[q] := S[q] + S[q−1]
6 . x := S[1]
7 . para q := 2 até n
8 .ooo x := max (x, S[q])
9 . devolva x

(Compare esse código com o do algoritmo Altura-Um.)

O algoritmo está correto.  A cada passagem pela linha 2, imediatamente antes que q seja comparado com n, S[q−1] é a semialtura de A[1 .. q−1]. Mais que isso: para todo j de 1 a q − 1 ,

S[ j] é a semialtura de A[1 .. j] .

Em particular, no início da linha 6, esse fato vale para todo j de 1 a n. O bloco de linhas 6-8 escolhe a maior das semialturas e portanto, no fim do algoritmo, x é a altura correta de A[1 .. n].

Exemplo D:  Vetores A e S no fim da execução do algoritmo Altura-PD.
  p r
A  20   −30   15   −10   30   −20   −30   30 
                 
S  20   −10   15   5   35   15   −15   30 

Desempenho.  A estimativa do consumo de tempo do algoritmo é muito fácil. As linhas 1, 6 e 9 do algoritmo são executadas 1 vez cada. As linhas 2, 3, 4, 7 e 8 são executadas cerca de n vezes cada, e portanto o algoritmo consome tempo proporcional a n. Em outras palavras, o desempenho do algoritmo no pior caso está em

Θ(n)

e portanto o algoritmo é linear.

Exercícios 5

  1. Os dois processos iterativos de Altura-PD podem ser fundidos num só, evitando-se assim o uso do vetor S[1 .. n]. Uma variável s pode fazer o papel de um elemento genérico do vetor S. Implemente essa ideia.
  2. Suponha dada uma matriz quadrada A[1 .. n, 1 .. n] de números inteiros (nem todos positivos). Encontrar uma submatriz quadrada de A que tenha soma máxima.
  3. Na definição de semialtura, todos os segmentos A[i .. q] são não vazios. Onde essa observação é usado no algoritmo Altura-PD?