Top-down dynamic programming
Top-down dynamic programming is probably the most common approach, since (at least looks like) is the easiest to implement. The whole point is avoiding the unnecessary computations that we have already done.
In our case, we can use our naïve solution and put a cache on top of it that will make sure, we don't do unnecessary calculations.
// This “structure” is required, since I have decided to use ‹TreeMap› which
// requires the ordering on the keys. It represents one position in the pyramid.
record Position(int row, int col) implements Comparable<Position> {
public int compareTo(Position r) {
if (row != r.row) {
return Integer.valueOf(row).compareTo(r.row);
}
if (col != r.col) {
return Integer.valueOf(col).compareTo(r.col);
}
return 0;
}
}
public static int longestSlideDown(
int[][] pyramid,
TreeMap<Position, Integer> cache,
Position position) {
int row = position.row;
int col = position.col;
if (row >= pyramid.length || col < 0 || col >= pyramid[row].length) {
// BASE: out of bounds
return Integer.MIN_VALUE;
}
if (row == pyramid.length - 1) {
// BASE: bottom of the pyramid
return pyramid[position.row][position.col];
}
if (!cache.containsKey(position)) {
// We haven't computed the position yet, so we run the same “formula” as
// in the naïve version »and« we put calculated slide into the cache.
// Next time we want the slide down from given position, it will be just
// retrieved from the cache.
int slideDown = Math.max(
longestSlideDown(pyramid, cache, new Position(row + 1, col)),
longestSlideDown(pyramid, cache, new Position(row + 1, col + 1)));
cache.put(position, pyramid[row][col] + slideDown);
}
return cache.get(position);
}
public static int longestSlideDown(int[][] pyramid) {
// At the beginning we need to create a cache and share it across the calls.
TreeMap<Position, Integer> cache = new TreeMap<>();
return longestSlideDown(pyramid, cache, new Position(0, 0));
}
You have probably noticed that record Position
have appeared. Since we are
caching the already computed values, we need a “reasonable” key. In this case we
share the cache only for one run (i.e. pyramid) of the longestSlideDown
, so
we can cache just with the indices within the pyramid, i.e. the Position
.
Record is relatively new addition to the Java language. It is basically an
immutable structure with implicitly defined .equals()
, .hashCode()
,
.toString()
and getters for the attributes.
Because of the choice of TreeMap
, we had to additionally define the ordering
on it.
In the longestSlideDown
you can notice that the computation which used to be
at the end of the naïve version above, is now wrapped in an if
statement that
checks for the presence of the position in the cache and computes the slide down
just when it's needed.
Time complexity
If you think that evaluating time complexity for this approach is a bit more tricky, you are right. Keeping the cache in mind, it is not the easiest thing to do. However there are some observations that might help us figure this out:
- Slide down from each position is calculated only once.
- Once calculated, we use the result from the cache.
Knowing this, we still cannot, at least easily, describe the time complexity of finding the best slide down from a specific position, but we can bound it from above for the whole run from the top. Now the question is how we can do that!
Overall we are doing the same things for almost1 all of the positions within the pyramid:
-
We calculate and store it (using the partial results stored in cache). This is done only once.
For each calculation we take 2 values from the cache and insert one value. Because we have chosen
TreeMap
, these 3 operations have logarithmic time complexity and therefore this step is equivalent to