Lattice Paths Part 1 (Brute Force)

Starting in the top left corner of a 2 x 2 grid, and only being able to move to the right and down, there are exactly 6 routes to the bottom right corner.

How many such routes are there through a 20 x 20 grid, or any N x N grid?

--

This is the first of a three-part series article about explaining how you could have arrived at the solution yourself in solving the Lattice Paths Problem. If you haven't tried it yet, do it first. I do not intend to deny you of your epiphany moment!

Note: All implementations are written in C/C++.

--

Brute Force

The most intuitive way to solve this problem is to think of a grid with coordinates (0, 0) at the top-left corner and (N, N) at the bottom-right corner. Then an arrowhead will traverse the grid from (0, 0) using all the combinations of Down and Right until it reaches (N, N). The number of combinations is incremented every time the arrowhead reaches (N, N). In other words, we will take a look at each possible combination of traversal.

First, we need to define a data structure where we can monitor the location of our arrowhead as it traverses the square grid. We will use this to check if the arrowhead is already on the boundary. We also need a string to monitor what has been traversed so far for each arrowhead. The string is only for debugging purposes and can be removed after we have finished the implementation of the program.

struct Coord
{
    long long x, y;          // X, Y

    std::string combo = "";  // D-R combinations 
};

Next, let's write a function and call it countRouteBruteForce() to traverse the grid with all the combinations of Down and Right from (0, 0) to (N, N). We can easily generate combinations using stacks. A step-by-step visualization for novice developers is shown below to generate the combinations of a 2 x 2 grid.

Action Last Pop Stack Count
push(s)   <Blank>  
last = pop() <Blank> <Empty>  
push(last + "r")   r  
push(last + "d")   r, d  
last = pop() d r  
push(last + "r")   r, dr  
push(last + "d")   r, dr, dd  
last = pop() dd r, dr  
push(last + "r")   r, dr, ddr  
last = pop() ddr r, dr  
push(last + "r")   r, dr, ddrr  
last = pop() ddrr r, dr +1
last = pop() dr r  
push(last + "r")   r, drr  
push(last + "d")   r, drr, drd  
last = pop() drd r, drr  
push(last + "r")   r, drr, drdr  
last = pop() drdr r, drr +1
last = pop() drr r  
push(last + "d")   r, drrd  
last = pop() drrd r +1
last = pop() r <Empty>  
push(last + "r")   rr  
push(last + "d")   rr, rd  
last = pop() rd rr  
push(last + "r")   rr, rdr  
push(last + "d")   rr, rdr, rdd  
last = pop() rdd rr, rdr  
push(last + "r")   rr, rdr, rddr  
last = pop() rddr rr, rdr +1
last = pop() rdr rr  
push(last + "d")   rr, rdrd  
last = pop() rdrd rr +1
last = pop() rr <Empty>  
push(last + "d")   rrd  
last = pop() rrd <Empty>  
push(last + "d")   rrdd  
last = pop() rrdd   +1
      +6

The implementation of a stack-based determination of countRouteBruteForce function is shown below.

long long countRouteBruteForce(void)
{
    std::stack <Coord>s;
    Coord c;
    long long count = 0;

    s.push({ 0, 0, "" });

    do
    {
        c = s.top(); s.pop();

        if (c.x == MAX_SIDES && c.y == MAX_SIDES)
        {
            count++;
            std::cout << c.combo << " (" << count << ")" << "\n";
        }
        else
        {
            // Right
            if (c.x < MAX_SIDES) 
                s.push({ c.x + 1, c.y, c.combo + "r" });
            
            // Down 
            if (c.y < MAX_SIDES) 
                s.push({ c.x, c.y + 1, c.combo + "d" });
        }
    } while (!s.empty());

    return count;
}

 

To optimize the code, we can reduce the number of operations, remove the string, and represent the stack as an array. This will give you the following code, let's call it countRouteBruteForceArray():

long long countRouteBruteForceArray(void)
{
    Coord2 s[21], c;
    long long stackIdx = 0;
    long long count = 0;

    s[stackIdx++] = { 0, 0 };

    do
    {
        c = s[--stackIdx];

        if (c.x == MAX_SIDES && c.y == MAX_SIDES)
            count++;

        else
        {
            // Right
            if (c.x < MAX_SIDES) 
                s[stackIdx++] = { c.x + 1, c.y };

            // Down 
            if (c.y < MAX_SIDES) 
                s[stackIdx++] = { c.x, c.y + 1 };    
        }
    } while (stackIdx > 0);

    return count;
}

A recursive approach can also be written but will not be discussed here. I'll leave it up to the reader to write a recursive implementation of this function as an exercise.

The problem with a stack-based (or recursive) approach is that the generation takes some time, especially for large values of N. The table below shows how the number of combinations grow with respect to (N, N).

No (N, N) N2  Routes  No. Loops
1 1, 1 1                                   2                                  5
2 2, 2 4                                   6                               19
3 3, 3 9                                 20                               69
4 4, 4 16                                 70                             251
5 5, 5 25                              252                             923
6 6, 6 36                              924                         3,431
7 7, 7 49                           3,432                       12,869
8 8, 8 64                        12,870                       48,619
9 9, 9 81                        48,620                     184,755
10 10, 10 100                      184,756                     705,431
11 11, 11 121                      705,432                 2,704,155
12 12, 12 144                  2,704,156               10,400,599
13 13, 13 169                10,400,600               40,116,599
14 14, 14 196                40,116,600            155,117,519
15 15, 15 225              155,117,520            601,080,389
16 16, 16 256              601,080,390         2,333,606,219
17 17, 17 289          2,333,606,220         9,075,135,299
18 18, 18 324          9,075,135,300      35,345,263,799
19 19, 19 361        35,345,263,800    137,846,528,819
20 20, 20 400     137,846,528,820    538,257,874,439

The optimized code outperforms the original function. However, it will still take even your powerful computer several hours to calculate the number of routes in a 20 x 20 grid, as the complexity in time (based on loops) is O( Routes(N+1) ) for any N x N grid (we'll uncover the nature of Routes(N) in the last part of the series). Thus, the need for another algorithm to solve this problem is necessary.

In the next aritcle (click here), you'll discover how we can reduce the time complexity of the solution to O(N2) with space complexity of O(N2).

If you're looking for the source codes, you may find it in the last part of the article (click here).