Editorial for IOI '94 P1 - The Triangle
Submitting an official solution before solving the problem yourself is a bannable offence.
Let us introduce some notation to formalize the problem. The triangle consists of rows with
. We number the rows of the triangle from top to bottom starting at
, and the positions in each row from left to right also starting at
. The number appearing in row
at position
is denoted by
where
and
. There are
numbers in the triangle. This amounts to
numbers in the largest possible triangle (
). The numbers are in the range from
to
.
A route starts with . For
, a route may step from
to either
or
. It ends in some
with
. Thus, a route visits
numbers in
steps. At each step in a route, there are two possible ways to go. Hence, there are
routes in the triangle. The largest possible triangle (
) has
routes. This is a huge number close to
. The smallest value for a route sum is zero, and the largest value is
, which is less than
.
Our first design decision concerns the input data. Two approaches are possible:
- Read all input data at the beginning of the program and store it internally for later use.
- Process the input data while reading it, without ever storing all of it together.
Because the amount of input data is not so large (at most small numbers), it is easy to store all of it in program variables. Our first three programs start by reading all input data. In the fourth program, we illustrate the other approach.
For , the problem is easy because there is only one route. In that case, the maximum route sum equals
. For
, there are two types of routes: half of the routes turn left on the first step from the top, and the other half goes to the right. Each route sum equals
plus the sum over the route continuing from
when turning left, or from
when turning right. Therefore, the maximum route sum equals
plus the maximum of the sums over routes starting either in
or in
. Determining the maximum sum for routes starting in
is the same problem for a smaller triangle.
Program 1
It is now easy to write a recursive function, say MaxRS
, that computes the maximum route sum from an arbitrary position in the triangle:
function MaxRS(r, p: index): integer ;
{ return maximum sum over all down routes starting in T[r, p] }
begin
if r = N then MaxRS := T[r, p]
else MaxRS := T[r, p] + Max(MaxRS(r+1, p), MaxRS(r+1, p+1))
end { MaxRS } ;
The invocation MaxRS(1, 1)
then solves the problem. However, this program passes the first three or four tests only. For a triangle with rows, the number of routes is simply too large (more than
) to be investigated in 30 seconds. You can eliminate the separate function
Max
(for computing the maximum of two numbers) by writing it out into the body of function MaxRS
, but that will not help (enough).
This problem was included precisely because it is tempting to use recursion. The 'plain' recursive solution was intended to fail (on some of the tests).
Program 2
There is a standard technique to speed up such a recursive program, known by names such as dynamic programming, tabulation, memoization, and memoing. Whenever the function MaxRS
is called with actual parameters and
for the first time, the result is computed and memoized in a table, say as
. For every subsequent invocation of
MaxRS
with these same parameters and
, the result is not recomputed, but simply retrieved from the table as
.
We introduce a table MRS
that is initialized at to indicate that the results are not yet known (note that all route sums are at least
). The function
MaxRS
can be modified as follows:
function MaxRS(r, p: index): integer ;
{ return maximum sum over all down routes starting in T[r, p] }
begin
if MRS[r, p] = -1 then
if r = N then MRS[r, p] := T[r, p]
else MRS[r, p] := T[r, p] + Max(MaxRS(r+1, p), MaxRS(r+1, p+1)) ;
MaxRS := MRS[r, p]
end { MaxRS } ;
It is still a simple program and now it is also fast enough, because not all routes are followed completely. A disadvantage of this solution is that it requires additional storage for the table. In fact, Program 2 uses at least twice the amount of data memory compared to Program 1 (such a trade-off between speed and memory usage is a common dilemma). You can squeeze the table into the upper-right triangle of the square array, but that would complicate the program needlessly.
Program 3
When program 2 is analyzed a bit further, it becomes clear that table MRS
is filled in a particular order. Assuming that in the call Max(E, F)
, expression is, for instance, always evaluated before
, the recursion pattern turns out to be very regular. Filling starts at the lower-left corner and continues from the bottom in diagonals slanting upward to the left. For five rows, the order is:
15
10 14
6 9 13
3 5 8 12
1 2 4 7 11
It is now a small step to come up with a program that fills table MRS
in a non-recursive way. Furthermore, this can be done in a more convenient order, say row by row from the bottom. That is, for five rows in the order:
15
13 14
10 11 12
6 7 8 9
1 2 3 4 5
The following procedure computes MRS
non-recursively (also called iteratively):
procedure ComputeAnswer ;
{ m := the maximum route sum }
var r, p: index ;
begin
for r := N downto 1 do
for p := 1 to r do
if r = N then MRS[r, p] := T[r, p]
else MRS[r, p] := T[r, p] + Max(MRS[r+1, p], MRS[r+1, p+1]) ;
m := MRS[1, 1]
end { ComputeAnswer } ;
(Of course, you can eliminate the if-statement by introducing a separate -loop for
, but the program is fast enough as it is and better to understand this way.) Observe that each number
is now inspected exactly once. Therefore, we do not need a separate table for storing
MRS
if we put at
. The type of
T
must be adjusted because the input T
-values are numbers in the range , but
MRS
-values possibly not.
Program 4
In Program 3, all input data must still be read and stored before the actual computation starts, because MRS
is computed from bottom to top. Routes in the triangle can, however, also be considered from bottom to top by flipping the direction. If we take that point of view, then we can say that:
- a route starts somewhere on the base at
for
;
- it steps upward either left or right (on the boundary there is no choice), that is, from
, with
and
, it may step to
if
, and to
if
;
- it always terminates at
.
We now redefine as the maximum sum over all up-routes starting in
. The final answer is then obtained as the maximum value among
with
. The following recurrent relationships hold for
MRS
:
if
if
and
if
The case distinction can be avoided by defining for
or
or
. We then simply have:
MRS
can be computed iteratively from top to bottom. Therefore it is not even necessary to store all input values. Only one row of MRS
needs to be stored. From this row and an input row, the next row of MRS
is computed (this computation is a little tricky). The final answer is the maximum of the last row computed.
Comments