P18
projecteuler.net

Maximum Path Sum I

ℹ️Published on Friday, 31st May 2002, 06:00 pm; Solved by 157035;
Difficulty rating: 5%

By starting at the top of the triangle below and moving to adjacent numbers on the row below, the maximum total from top to bottom is $23$.

3
7 4
2 4 6
8 5 9 3

That is, $3 + 7 + 4 + 9 = 23$.

Find the maximum total from top to bottom of the triangle below:

75
95 64
17 47 82
18 35 87 10
20 04 82 47 65
19 01 23 75 03 34
88 02 77 73 07 63 67
99 65 04 28 06 16 70 92
41 41 26 56 83 40 80 70 33
41 48 72 33 47 32 37 16 94 29
53 71 44 65 25 43 91 52 97 51 14
70 11 33 28 77 73 17 78 39 68 17 57
91 71 52 38 17 14 91 43 58 50 27 29 48
63 66 04 68 89 53 67 30 73 16 69 87 40 31
04 62 98 27 23 09 70 98 73 93 38 53 60 04 23

NOTE: As there are only $16384$ routes, it is possible to solve this problem by trying every route. However, Problem 67, is the same challenge with a triangle containing one-hundred rows; it cannot be solved by brute force, and requires a clever method! ;o)



Soluzione

Impostiamo i numeri dentro alla matrice \(M\). Ci interessa trovare al somma massima ottenibile percorrendo \(M\) da cima a fondo e spostandoci con passi adiacenti. L'idea quindi è di partire dal numero in cima e scendere calcolando tutti i possibili valori che possiamo ottenere sommando quel numero ai numeri a lui adiacenti nella riga sotto, ovvero quello in basso a sinitra e quello in basso a destra (nella rappresentazione originale a triangolo). Di questi risultati, ci salviamo (in un'altra matrice \(M_\text{sum}\)) il massimo tra di loro, ovvero quando ad una cella possono contribuire diversi "percorsi" di somme selezioniamo quello che conduce alla somma più alta. Alla fine prendiamo il massimo valore che incontriamo sulla sua ultima riga: questo corrisponderà infatti al valore di somma massimo ottenibile.
L'idea funziona perché è come se filtrassimo di volta in volta l'informazione contenuta in M: se siamo alla riga 5 non ci interessano precisamente tutti i numeri nelle righe dalla 1 alla 4, ma piuttosto solo il loro effetto, cioè le somme più alte che loro sono riusciti a generare.

Ad esempio, nel caso che segue partiamo con la matrice \(M\) che nella prima riga contiene il numero 75. La seconda riga di \(M_\text{sum}\) conterrà quindi 75+95 e 75+64, senza troppi pensieri. La terza riga diventa ora interessante perché si aprono diversi percorsi. Ad esempio, nella prima cella di \(M_\text{sum}\) mettiamo il massimo tra 75+95+17 (il percorso sempre dritto verso il basso) e 75+64+17 (il percorso che al secondo step devia verso destra). Allo stesso modo si procede nelle altre celle.

M =[
75 00 00 00 00 00 00 00 00 00 00 00 00 00 00
95 64 00 00 00 00 00 00 00 00 00 00 00 00 00
17 47 82 00 00 00 00 00 00 00 00 00 00 00 00
18 35 87 10 00 00 00 00 00 00 00 00 00 00 00
20 04 82 47 65 00 00 00 00 00 00 00 00 00 00
19 01 23 75 03 34 00 00 00 00 00 00 00 00 00
88 02 77 73 07 63 67 00 00 00 00 00 00 00 00
99 65 04 28 06 16 70 92 00 00 00 00 00 00 00
41 41 26 56 83 40 80 70 33 00 00 00 00 00 00
41 48 72 33 47 32 37 16 94 29 00 00 00 00 00
53 71 44 65 25 43 91 52 97 51 14 00 00 00 00
70 11 33 28 77 73 17 78 39 68 17 57 00 00 00
91 71 52 38 17 14 91 43 58 50 27 29 48 00 00
63 66 04 68 89 53 67 30 73 16 69 87 40 31 00
04 62 98 27 23 09 70 98 73 93 38 53 60 04 23
]

R, C = size(M)
Msum = copy(M)

@time for i in 2:R
	for j in 1:C
		if j==1
			Msum[i,j] = Msum[i-1,j] + M[i,j]
		else
			Msum[i,j] = max(Msum[i-1,j] + M[i,j], Msum[i-1,j-1] + M[i,j])
		end
	end
end
  0.005866 seconds (1.02 k allocations: 29.891 KiB, 98.00% compilation time)
for i in 1:R
	for j in 1:C
		print(rpad(Msum[i,j],5, " "))
	end
	println()
end
75   0    0    0    0    0    0    0    0    0    0    0    0    0    0    
170  139  0    0    0    0    0    0    0    0    0    0    0    0    0    
187  217  221  0    0    0    0    0    0    0    0    0    0    0    0    
205  252  308  231  0    0    0    0    0    0    0    0    0    0    0    
225  256  390  355  296  0    0    0    0    0    0    0    0    0    0    
244  257  413  465  358  330  0    0    0    0    0    0    0    0    0    
332  259  490  538  472  421  397  0    0    0    0    0    0    0    0    
431  397  494  566  544  488  491  489  0    0    0    0    0    0    0    
472  472  520  622  649  584  571  561  522  0    0    0    0    0    0    
513  520  592  655  696  681  621  587  655  551  0    0    0    0    0    
566  591  636  720  721  739  772  673  752  706  565  0    0    0    0    
636  602  669  748  798  812  789  850  791  820  723  622  0    0    0    
727  707  721  786  815  826  903  893  908  870  847  752  670  0    0    
790  793  725  854  904  879  970  933  981  924  939  934  792  701  0    
794  855  891  881  927  913  1040 1068 1054 1074 977  992  994  796  724  
@show maximum(Msum[end,:])
maximum(Msum[end, :]) = 1074

Possiamo anche provare a ricostruire il percorso della soluzione scorrendo \(M_\text{sum}\) dal basso verso l'alto e cofrontandola con \(M\).

cfr = maximum(Msum[end,:])
# se siamo in fondo il target è il massimo valore, la somma più alta
j_focus = argmax(Msum[end,:])
for i in R:-1:1
	if i<R
		global cfr = cfr - M[i+1,j_focus]
		# altrimenti il target diventa la somma parziale meno il numero che c'era in M
		global j_focus = findfirst(x -> x==cfr, Msum[i,:])
	end
	for j in 1:C
		if j==j_focus
			print(rpad("[$(M[i,j])]",7, " "))
		else
			print(rpad(M[i,j],7, " "))
		end
	end
	println()
end
4      62     98     27     23     9      70     98     73     [93]   38     53     60     4      23     
63     66     4      68     89     53     67     30     [73]   16     69     87     40     31     0      
91     71     52     38     17     14     91     43     [58]   50     27     29     48     0      0      
70     11     33     28     77     73     17     [78]   39     68     17     57     0      0      0      
53     71     44     65     25     43     [91]   52     97     51     14     0      0      0      0      
41     48     72     33     47     [32]   37     16     94     29     0      0      0      0      0      
41     41     26     56     [83]   40     80     70     33     0      0      0      0      0      0      
99     65     4      [28]   6      16     70     92     0      0      0      0      0      0      0      
88     2      77     [73]   7      63     67     0      0      0      0      0      0      0      0      
19     1      23     [75]   3      34     0      0      0      0      0      0      0      0      0      
20     4      [82]   47     65     0      0      0      0      0      0      0      0      0      0      
18     35     [87]   10     0      0      0      0      0      0      0      0      0      0      0      
17     47     [82]   0      0      0      0      0      0      0      0      0      0      0      0      
95     [64]   0      0      0      0      0      0      0      0      0      0      0      0      0      
[75]   0      0      0      0      0      0      0      0      0      0      0      0      0      0      

Dicevo "provare" perché questo metodo a volte può sbagliare, dato che opera su confronti aritmetici, che possono risultare ambigui in caso ci siano più numeri che soddisfano la condizione. Perciò occorre invece aggiornare la funzione sopra che assembla la soluzione per tenere anche traccia dei predecessori usati nel percorso ottimale.

parent = zeros(Int,R, C)
for i in 2:R
    for j in 1:i
        if j == 1
            Msum[i,j] = Msum[i-1,j] + M[i,j]
            parent[i,j] = j
        else
			# sciogliamo il max tra i due casi per aggiornare correttamente parent
            if Msum[i-1,j] > Msum[i-1,j-1]
                Msum[i,j] = Msum[i-1,j] + M[i,j]
                parent[i,j] = j
            else
                Msum[i,j] = Msum[i-1,j-1] + M[i,j]
                parent[i,j] = j-1
            end
        end
    end
end
j_focus = argmax(Msum[end, :])
path = Int[]
path_indices = zeros(Int,R)
for i in R:-1:1
	push!(path, M[i,j_focus])
    global path_indices[i] = j_focus
    global j_focus = parent[i,j_focus]
end
@show path sum(path)
path = [93, 73, 58, 78, 91, 32, 83, 28, 73, 75, 82, 87, 82, 64, 75]
sum(path) = 1074
for i in 1:R
    for j in 1:i
        if j == path_indices[i]
            print(rpad("[$(M[i,j])]",5," "))
        else
            print(rpad(M[i,j],5," "))
        end
    end
    println()
end
[75] 
95   [64] 
17   47   [82] 
18   35   [87] 10   
20   4    [82] 47   65   
19   1    23   [75] 3    34   
88   2    77   [73] 7    63   67   
99   65   4    [28] 6    16   70   92   
41   41   26   56   [83] 40   80   70   33   
41   48   72   33   47   [32] 37   16   94   29   
53   71   44   65   25   43   [91] 52   97   51   14   
70   11   33   28   77   73   17   [78] 39   68   17   57   
91   71   52   38   17   14   91   43   [58] 50   27   29   48   
63   66   4    68   89   53   67   30   [73] 16   69   87   40   31   
4    62   98   27   23   9    70   98   73   [93] 38   53   60   4    23   

Last modified: September 20, 2025. Website built with Franklin.jl and the lovely Julia programming language.