Thursday, 28 August 2008

Project Euler Problems 18 and 67: Finding the Maximal Route through a Triangle

If this doesn't pique the interest of my fellow developers then I don't know what will: a problem that will take twenty billion years to solve (at an extremely optimistic estimate), unless we find an efficient algorithm. The problem in question is Project Euler Problem 67. We're given a triangle made up of one hundred rows of numbers, and asked to consider all routes from the top to the bottom, made by moving from one row to the next via adjacent cells. Over all such routes, we have to find the maximum sum. Can't picture it? Let me help:

TriangleRoute

For the route that I've drawn, the sum is 5 + 15 + 29 + 45 = [train your brain and work it out yourself!].

If we only wanted to tackle the baby brother of this problem (Problem 18 - the same problem for a triangle with just 15 rows), we could get away with checking each of the routes. But it's not feasible to do that here because there are 299 possibilities: hence the need for an efficient algorithm. Any ideas?

You could take a bath and wait for the Eureka moment. Or if you want to avoid the ensuing streaking, read on.

An algorithm

Suppose that for each cell in a row we know the maximum sum over all routes ending at that cell. From that it's easy to work out the same for each of the cells in the next row. Here's how you do it. If you've got a cell with value c, somewhere in the middle of the row, then you can only reach it from either of the two cells adjacent to it in the row above. 

CalculatingCellMaxima

We're assuming that we already know the maximum sum for routes ending at these cells: suppose it is a and b respectively. So the biggest possible sum we can get for routes ending at c is Max(c + a, c + b). For the two cells at either end of the row the calculation is even simpler: they can only be reached from the cell at the appropriate end of the row above them, so we just sum the cell value with the known maximum sum to the cell above. In this way, we can crunch through the rows, 'flattening' the triangle until we reach the last row, at which point we will know the maximum possible sum over all routes ending at each of the cells in that row.

That's the middle of an algorithm. To solve the overall problem we just need to add a beginning and an end. The middle part starts by assuming that we already know the maximum sum for all routes ending at a particular row. So to get the algorithm rolling we need a row for which we do have that information, and the first row of the triangle is the obvious candidate: all routes start there, so the maximum up to that point is just the value of the cell in that row. How about the end of the algorithm? To solve the problem, we need to know the maximum sum from top to bottom through the triangle; we've calculated the maximum sum ending at each of the cells in the last row, so we just need to take the maximum over all these cells.

Some code

To code this, we'll reach once again into our Functional Programming toolbox. I think the Aggregate method is just the hammer to crack this particular nut. Do you see how?

Ignore the input part for now, and assume we've got a sequence of List<int>, one List for each row of the triangle. Like this:

{75}
{95, 64}
{17, 47, 82}
{18, 35, 87, 10}
...
In this layout, it's a little tricky to visualise which cells are adjacent to which: you need to look at the cell directly above, and the one above but to the left. For example, the 35 in row four is adjacent to 47 and 17 in the row above.

We'll use Aggregate to 'flatten' the triangle represented by this sequence of Lists using the algorithm I described above: the aggregate that we'll be calculating is a List containing the maximum sum over all routes to each cell up to the last row we've processed. Remember that Aggregate needs a function that combines the aggregate so far with the next item in the series. We'll supply a function that takes the maximums so far and combines them with the cell values for the next row. The code looks like this:

return rows.Aggregate(
    new List<int> {0},
    (currentMaxima, nextRow) =>
        {
            var rowLength = nextRow.Count();
            return nextRow
                .Select((cell, index) =>
                        index == 0 ? cell + currentMaxima[index]
                        : index == rowLength - 1 ? cell + currentMaxima[index - 1]
                        : Math.Max(cell + currentMaxima[index - 1], cell + currentMaxima[index]))
                .ToList();
        }
    )
    .Max();

Notice that at line 7 we're processing the sequence nextRow (containing the cell values) using an overload of the Select method that gives us the index of each cell value in the sequence; we use this index to find the appropriate entries in the list currentMaxima containing the maximum sums to the cells in the row above.

And with that, I believe we've solved the problem. It takes 2 milliseconds to solve the 100-row triangle problem on my computer - a satisfying improvement over 20 billion years.

Here's the complete code. Note that in the source code project I have included two files containing the data for these problems.

namespace ProjectEuler
{
    [EulerProblem(18, Title = "Find the maximum sum travelling from the top of the triangle to the base.")]
    public class Problem18
    {
        public long Solve()
        {
            var dataPath = @"ProblemData\Problem18Data.txt";

            return MaximumSumThroughTriangleSolver.SolveFromFile(dataPath);
        }
    }

    [EulerProblem(67, Title = "Using an efficient algorithm find the maximal sum in the triangle?")]
    public class Problem67
    {
        public long Solve()
        {
            var dataPath = @"ProblemData\Problem67Data.txt";

            return MaximumSumThroughTriangleSolver.SolveFromFile(dataPath);
        }
    }

    public static class MaximumSumThroughTriangleSolver
    {
        public static int SolveFromFile(string dataFilePath)
        {
            string problemData = File.ReadAllText(dataFilePath);

            return Solve(problemData);
        }

        public static int Solve(string problemData)
        {
            var rows = problemData
                .Split(new[] {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries)
                .Select(line =>
                        line.Split(' ')
                            .Select(token => int.Parse(token))
                            .ToList()
                )
                .ToList();

            return rows.Aggregate(
                new List<int> {0},
                (currentMaxima, nextRow) =>
                    {
                        var rowLength = nextRow.Count();
                        return nextRow
                            .Select((cell, index) =>
                                    index == 0 ? cell + currentMaxima[index]
                                    : index == rowLength - 1 ? cell + currentMaxima[index - 1]
                                    : Math.Max(cell + currentMaxima[index - 1], cell + currentMaxima[index]))
                            .ToList();
                    }
                )
                .Max();
        }
    }
}

Monday, 25 August 2008

Happy Birthday to Me

My colleague Roman had his birthday a couple of days ago; today I have caught up with him in age. His mathematician's heart was saddened by the thought that this year is the last when his age can be represented in the form xx - unless he discovers the secrets of the Philosopher's stone.

So now you know exactly how old I am.

Friday, 22 August 2008

Anyone for a Slice of LINQ?

Over on his blog, Eric White has a post about how to chunk a collection into groups of three using standard LINQ operators. As it happens, I've been thinking about the same issue as part of a solution to an upcoming Project Euler problem. Rather than use standard LINQ operators though, I've created a new method called Slice to do the job using a C# iterator.

Give Slice() a sequence, tell it how many items you want to be in each slice or chunk (pick your synonym - might depend on whether you prefer cake or chocolate!), and it will give you back a sequence consisting of slices of the original sequence (each slice is returned as a sequence - an IEnumerable). If the original sequence cannot be evenly divided into slices of exactly the size you require, then the last slice you get back will contain the leftover items (the crumbs, as I've called them in the code below - I clearly prefer cake!).

To adapt Eric's example to the gastronomic analogy that has emerged as I've been writing, consider this example:

public void SliceCakes()
{
    string[] cakeData =
        {
            "Mini Rolls Selection",
            "Cabury",
            "2.39",
            "6 Chocolate Flavour Cup Cakes",
            "Fabulous Bakin' Boys",
            "1.25",
            "Galaxy Cake Bars 5 Pack",
            "McVities",
            "1.00",
            "Apple Slices 6pk",
            "Mr Kipling",
            "1.39"
        };

    var cakes = cakeData
        .Slice(3)
        .Select(slice => new
                 {
                     Cake = slice.ElementAt(0),
                     Baker = slice.ElementAt(1),
                     Price = slice.ElementAt(2)
                 });

    foreach (var cake in cakes)
    {
        Console.WriteLine(cake);
    }
}

This produces the output:

{ Cake = Mini Rolls Selection, Baker = Cadbury, Price = 2.39 }
{ Cake = 6 Chocolate Flavour Cup Cakes, Baker = Fabulous Bakin' Boys, Price = 1.25 }
{ Cake = Galaxy Cake Bars 5 Pack, Baker = McVities, Price = 1.00 }
{ Cake = Apple Slices 6pk, Baker = Mr Kipling, Price = 1.39 }

Here's the code for Slice()

public static class Functional
{
    /// <summary>
    /// Slices a sequence into a sub-sequences each containing maxItemsPerSlice, except for the last
    /// which will contain any items left over
    /// </summary>
    public static IEnumerable<IEnumerable<T>> Slice<T>(this IEnumerable<T> sequence, int maxItemsPerSlice)
    {
        if (maxItemsPerSlice <= 0)
        {
            throw new ArgumentOutOfRangeException("maxItemsPerSlice", "maxItemsPerSlice must be greater than 0");
        }

        List<T> slice = new List<T>(maxItemsPerSlice);

        foreach (var item in sequence)
        {
            slice.Add(item);

            if (slice.Count == maxItemsPerSlice)
            {
                yield return slice.ToArray();
                slice.Clear();
            }
        }

        // return the "crumbs" that 
        // didn't make it into a full slice
        if (slice.Count > 0)
        {
            yield return slice.ToArray();
        }
    }
}

Thursday, 21 August 2008

Look at me. I'm on TV

We had a film crew from our regional BBC TV station come into the office on Tuesday. Their piece was aired at lunchtime today, and you can also see it on the BBC website . They focus on the need we have to recruit more people. We've already had one response from a lady who heard that we need people skilled in Mathematical Modelling. She rang to say that her daughter had a maths degree, and was really good with Lego, and did we have a job for her?

Look out for me. I'm the one you see peering over a partition whilst the fingers of my colleague Jason type gibberish into Visual Studio. I wasn't trying to get in shot. Honest!VideoScreenGrab