180 lines
7.0 KiB
Plaintext
180 lines
7.0 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "b0604090",
|
|
"metadata": {},
|
|
"source": [
|
|
"# [Maximum Path Sum I](https://projecteuler.net/problem=18)\n",
|
|
"\n",
|
|
"As the problem notes, we could brute force every single path through this triangle, since there aren't that many, relatively speaking. But we're going to get a tougher version in [problem 67](https://projecteuler.net/problem=67), so if we make the effort now, we can kill two birds with one stone.\n",
|
|
"\n",
|
|
"First, let's encode the triangle as a list of lists:"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 1,
|
|
"id": "c3db04f0",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
" triangle = [[75],\n",
|
|
" [95, 64],\n",
|
|
" [17, 47, 82],\n",
|
|
" [18, 35, 87, 10],\n",
|
|
" [20, 4, 82, 47, 65],\n",
|
|
" [19, 1, 23, 75, 3, 34],\n",
|
|
" [88, 2, 77, 73, 7, 63, 67],\n",
|
|
" [99, 65, 4, 28, 6, 16, 70, 92],\n",
|
|
" [41, 41, 26, 56, 83, 40, 80, 70, 33],\n",
|
|
" [41, 48, 72, 33, 47, 32, 37, 16, 94, 29],\n",
|
|
" [53, 71, 44, 65, 25, 43, 91, 52, 97, 51, 14],\n",
|
|
" [70, 11, 33, 28, 77, 73, 17, 78, 39, 68, 17, 57],\n",
|
|
" [91, 71, 52, 38, 17, 14, 91, 43, 58, 50, 27, 29, 48],\n",
|
|
" [63, 66, 4, 68, 89, 53, 67, 30, 73, 16, 69, 87, 40, 31],\n",
|
|
" [4, 62, 98, 27, 23, 9, 70, 98, 73, 93, 38, 53, 60, 4, 23]]"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "51f3aa5a",
|
|
"metadata": {},
|
|
"source": [
|
|
"There are a couple of important things to note about this problem. First, looking at the triangle, try to see how there are two \"sub-triangles\" below the top value, 75 (it's easier to see this on the original page rather than in the list-of-lists). One sub-triangle has a top value of 95, then the next row is 17 and 47, and the next row is 18, 35, and 87, and so on.\n",
|
|
"```\n",
|
|
" 95\n",
|
|
" 17 47\n",
|
|
" 18 35 87\n",
|
|
" ⋰ ⋮ ⋱\n",
|
|
"```\n",
|
|
"\n",
|
|
"The other sub-triangle has a top value of 64, and the next row is 47 and 82, and so on.\n",
|
|
"```\n",
|
|
" 64\n",
|
|
" 47 82\n",
|
|
" 35 87 10\n",
|
|
" ⋰ ⋮ ⋱\n",
|
|
"```\n",
|
|
"\n",
|
|
"If we think a little abstractly, the maximum path sum (MPS) through *the whole triangle* must equal the sum of the topmost value (75) with the larger of the MPSs through *each sub-triangle*. This fact holds for the lower layers as well: the MPS for the sub-triangle with top value 95 depends on the MPSs for the sub-triangles with top values 17 and 47, and the MPS for the sub-triangle with top value 64 depends on the MPSs for the sub-triangles with top values 47 and 82, and so on and so on.\n",
|
|
"\n",
|
|
"Because of this, the problem is said to have [optimal substructure](https://en.wikipedia.org/wiki/Optimal_substructure). In short, we find the MPS through the whole triangle by solving smaller and smaller subproblems, i.e. MPSs through sub-triangles. Eventually we will reach a base case: one of the values in the bottom-most layer of the triangle, where the maximum path sum of that \"sub-triangle\" is just the value itself.\n",
|
|
"\n",
|
|
"There's another important thing to consider: in trying to find the MPS through the 95-sub-triangle, we have to find the MPS through the 47-sub-triangle. Similarly, in trying to find the MPS through the 64-sub-triangle, we *also* have to find the MPS through the 47-sub-triangle. This reoccurrence of subproblems happens more and more frequently in the lower layers, as well. In other words, there are [overlapping subproblems](https://en.wikipedia.org/wiki/Overlapping_subproblems).\n",
|
|
"\n",
|
|
"When a problem has optimal substructure and overlapping subproblems, we can use [dynamic programming](https://en.wikipedia.org/wiki/Dynamic_programming). We can employ either a top-down or bottom-up approach.\n",
|
|
"\n",
|
|
"## Top-down Method\n",
|
|
"This approach recusively finds the maximum path sums of each sub-triangle starting from the topmost value, similarly to what's described above. The `cache` decorator is used to memoize and avoid recomputing MPSs for any overlapping sub-triangles. This is crucial to the speed of a top-down approach; without memoization, we will recompute the solutions to overlapping subproblems way too often."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 2,
|
|
"id": "778ba200",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from functools import cache\n",
|
|
"\n",
|
|
"@cache\n",
|
|
"def max_path_sum(tri):\n",
|
|
" if tri == tuple():\n",
|
|
" return 0\n",
|
|
"\n",
|
|
" left = tuple(row[:-1] for row in tri[1:])\n",
|
|
" right = tuple(row[1:] for row in tri[1:])\n",
|
|
" return tri[0][0] + max(max_path_sum(left), max_path_sum(right))"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "dec8855a",
|
|
"metadata": {},
|
|
"source": [
|
|
"One slight wrinkle to this approach is that to use the `cache` decorator, the inputs to our function need to be [hashable](https://docs.python.org/3/glossary.html#term-hashable). Lists are not hashable, but tuples are, so we can convert our triangle to a tuple of tuples."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 3,
|
|
"id": "789460a8",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"1074"
|
|
]
|
|
},
|
|
"execution_count": 3,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"max_path_sum(tuple(tuple(row) for row in triangle))"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "4e267302",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Bottom-up Method\n",
|
|
"Another approach is to instead find the MPSs of the very smallest sub-triangles first, then use those MPSs to find the MPSs in the next layer up. This doesn't require memoization, since by starting with the smallest subproblems, we ensure they are only computed once."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 4,
|
|
"id": "23352679",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"1074"
|
|
]
|
|
},
|
|
"execution_count": 4,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"def max_path_sum(tri):\n",
|
|
" for i in reversed(range(0, len(tri) - 1)):\n",
|
|
" for j in range(0, len(tri[i])):\n",
|
|
" tri[i][j] += max(tri[i+1][j], tri[i+1][j+1])\n",
|
|
" \n",
|
|
" return tri[0][0]\n",
|
|
"\n",
|
|
"max_path_sum(triangle)"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "SageMath 9.5",
|
|
"language": "sage",
|
|
"name": "sagemath"
|
|
},
|
|
"language_info": {
|
|
"codemirror_mode": {
|
|
"name": "ipython",
|
|
"version": 3
|
|
},
|
|
"file_extension": ".py",
|
|
"mimetype": "text/x-python",
|
|
"name": "python",
|
|
"nbconvert_exporter": "python",
|
|
"pygments_lexer": "ipython3",
|
|
"version": "3.11.2"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 5
|
|
}
|