Euler 18 - Maximum Path Sum

João Pedro Neto

Seja o seguinte problema do projeto Euler.

Dado um triângulo de números calcular a soma máxima entre os caminhos possíveis desde o vértice até a um valor na base do triângulo.

No exemplo seguinte, a soma máxima é 23 resultante da soma dos elementos no caminho a vermelho.

3
7 4
2 4 6
8 5 9 3

Considerem que leem o input para uma matriz Java triangle[][] ficando os valores neste formato:

3 0 0 0
7 4 0 0
2 4 6 0
8 5 9 3

Uma abordagem Greedy que parece funcionar: começando por cima, escolher o elemento maior entre os dois possíveis da linha abaixo:

3
7 4
2 4 6
8 5 9 3


int solveGreed(int[][] triangle) {
    int sum=triangle[0][0];
    
    for(int i=1, currentCol=0; i<SIZE; i++) {   // for each row
        if (triangle[i][currentCol] < triangle[i][currentCol+1])
            currentCol++;
        sum += triangle[i][currentCol];
    }
    return sum;
}

mas não é difícil encontrar um contra-exemplo:

3
7 4
2 4 6
8 5 9 30

Uma outra abordagem é a força bruta, ou seja, testar todas as hipóteses. Para um triângulo de altura N existem 2^(N-1) caminhos possíveis. Podemos usar bitmasks: cada caminho é representado por N-1 bits de um número inteiro (eg, bit 0 desce pela esquerda, 1 pela direita). Por exemplo, o número 6 cuja representação binária é 110 representa o caminho máximo descrito acima. Já o número 3 é 011 representa o caminho

3
7 4
2 4 6
8 5 9 3

Assim podemos implementar este algoritmo:


int solveBruteForce(int[][] triangle) {
    int maxSum =0;
    int nPaths = 2 << (SIZE-1);
    
    for(int i=0; i<nPaths; i++) {
        int sum = triangle[0][0];
        for (int j=0, index=0; j<SIZE-1; j++) {
            index += i >> j & 1;
            sum   += triangle[j+1][index];
        }
        if (sum>maxSum)
            maxSum = sum;
    }
    return maxSum;
}

Mas a força bruta tem complexidade exponencial na altura N do triângulo, O(2^N)...

Podemos usar a programação dinâmica para calcular a resposta em O(N^2).

Uma abordagem passa por guardar em cada linha (a contar de cima), a soma máxima atual conseguida pelos caminhos parciais que começam no vértice. No exemplo acima temos

3 3 3 3
7 4 10 7 10 7 10 7
2 4 6 => 2 4 6 => 12 14 13 => 12 14 13
8 5 9 3 8 5 9 3 8 5 9 3 20 19 23 16

vamos descendo linha a linha, fazendo as somas de acordo com os máximos parciais de cima. Quando terminamos este processo, o resultado que queremos será o maior valor da linha da base.

O código:


int solveDP(int[][] triangle) {
    
    for(int i=1; i<SIZE; i++) {   // for each row
        // treat extremities (just one possible sum)
        triangle[i][0] += triangle[i-1][0];
        triangle[i][i] += triangle[i-1][i-1];
        
        for(int j=1; j<i; j++)
            triangle[i][j] += Math.max(triangle[i-1][j-1], triangle[i-1][j]);
    }

    int maxSum=0;
    for(int i=0; i<SIZE; i++)
        if (triangle[SIZE-1][i] > maxSum)
            maxSum = triangle[SIZE-1][i];
    return maxSum;
}

Podem também pensar como se poderia resolver com programação dinâmica começando da base para o vértice.

Para um triângulo com 25 linhas, estes são os resultados e os tempos obtidos:

Brute Force: 1867, time: 1046 ms
Greed: 1546, time: 0 ms
DP: 1867, time: 0 ms

Como seria de esperar, a abordagem gananciosa obteve uma solução sub-óptima. E observamos a diferença de desempenho da força bruta que demorou mais de um segundo enquanto a solução de programação dinâmica demorou menos de 1 ms.


Um obrigado ao Diogo Sousa por me enviar este problema.

Last modified: Tuesday, 7 April 2020, 10:09 AM