Topcoder SRM 519, D2, 600-point problem - Solving with enumeration

James S. Plank
CS302
September, 2016
Latest revision: February, 2022.


Problem description: http://community.topcoder.com/stat?c=problem_statement&pm=11554&rd=14544.

In case Topcoder's servers are down

The method signature for this problem is:

class ThreeTeleports {
  public:
    int shortestDistance(int xMe, int yMe,               // "Me" is (xMe,yMe)
                             int xHome, int yHome,       // "Home" is (xHome,yHome)
                             vector <string> teleports); // Each teleport is "x1 y1 x2 y2"
};


Examples

#  Me             Home        Teleports                                                                              Answer
- ----- --------------------- -------------------------------------------------------------------------------------- ------
0  3  3          4          5 {"1000 1001 1000 1002", "1000 1003 1000 1004", "1000 1005 1000 1006"}                       3
1  0  0         20         20 {"1 1 18 20", "1000 1003 1000 1004", "1000 1005 1000 1006"}                                14
2  0  0         20         20 {"1000 1003 1000 1004", "18 20 1 1", "1000 1005 1000 1006"}                                14
3 10 10      10000      20000 {"1000 1003 1000 1004", "3 3 10004 20002", "1000 1005 1000 1006"}                          30
4  3  7      10000      30000 {"3 10 5200 4900", "12212 8699 9999 30011", "12200 8701 5203 4845"}                       117
5  0  0 1000000000 1000000000 {"0 1 0 999999999", "1 1000000000 999999999 0", "1000000000 1 1000000000 999999999"}       36
Here's a picture of example 0. The shortest path goes directly from "Me" to "Home".

Here's a picture of example 1. The shortest path is shaded in yellow.


Solving this problem with enumeration

Our driver code is in Teleport-Main.cpp, which includes ThreeTeleports.cpp. That is where we will implement the class and method. (BTW, the topcoder writeup replaces the space-ship with a frog).

We're going to see this problem again later this semester. However, its constraints are so small that we can solve it with enumeration. There are only eight places in the frog's universe that we care about:

The frog's path from "Me" to "Home" can be defined by an ordering of these places. For example, 0-1-2-3-4-5-6-7 would be a path where the frog goes from: 0-2-1-3-4-5-6-7 would be the same path, but the frog would travel through the first teleport in reverse order.

We're going to solve this problem by enumerating the paths. There are a few details that we're going to have to take care of, though.


Part 1: Reading those teleport strings

Our first task is to read those teleport strings. What we're going to do is create two vectors, X and Y, where X[i] is the x position of location i, and Y[i] is the y position of location i. Our first program creates X and Y and prints them out. It is in Teleport-1.cpp:

#include <string>
#include <vector>
#include <algorithm>
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;

class ThreeTeleports {
  public:
    int shortestDistance(int xMe, int yMe, int xHome, int yHome, vector <string> teleports);
    vector <int> Order;
    void Permute(int index);
};

int ThreeTeleports::shortestDistance(int xMe, int yMe, int xHome, int yHome, 
                                     vector <string> teleports)
{
  vector <long long> X, Y;
  int i, x1, x2, y1, y2;

  /* Create the vectors X and Y, which hold the x and y coordinates of the eight
     locations that we care about. */

  X.push_back(xMe);
  Y.push_back(yMe);

  for (i = 0; i < teleports.size(); i++) {
    sscanf(teleports[i].c_str(), "%d %d %d %d", &x1, &y1, &x2, &y2);
    X.push_back(x1);
    Y.push_back(y1);
    X.push_back(x2);
    Y.push_back(y2);
  }

  X.push_back(xHome);
  Y.push_back(yHome);
  
  /* We use %lld to print out long long's using printf(). */

  for (i = 0; i < X.size(); i++) printf("[%lld,%lld]\n", X[i], Y[i]);

  return 0;
}

I'm storing X and Y as long long vectors. The reason is that the constraints are pretty big: 1,000,000,000. While that number fits easily into an integer, three times that number does not, and since our paths may go from small x/y values to high ones and back, we may have paths that are over 3,000,000,000. So it's smart to use long long's.

We test it on examples 0 and 1, and everything looks good:

UNIX> cp Teleport-1.cpp ThreeTeleports.cpp
UNIX> g++ Teleport-Main.cpp
UNIX> a.out 0
[3,3]
[1000,1001]
[1000,1002]
[1000,1003]
[1000,1004]
[1000,1005]
[1000,1006]
[4,5]
0
UNIX> a.out 1
[0,0]
[1,1]
[18,20]
[1000,1003]
[1000,1004]
[1000,1005]
[1000,1006]
[20,20]
0
UNIX>

Part 2: Creating an adjacency matrix from X and Y

Second, when we enumerate paths, we want to calculate the frog's distance along the paths. To help us with this, we're going to create a data structure called an adjacency matrix. This is going to be a vector of vectors of long longs. Let's call it AM. Then AM[i][j] will hold the distance that the frog has to jump to go from place i to place j. The code to create this is in Teleport-2.cpp. Here's the variable declaration and the new code. For the moment, I'm ignoring the teleports and just calculating the frog's hopping distance between places:

  ...
  vector < vector <long long> >AM;
  ...
  /* Create the adjacency matrix. */

  AM.resize(X.size());
  for (i = 0; i < X.size(); i++) {
    for (j = 0; j < X.size(); j++) {
      xd = X[j]-X[i];
      if (xd < 0) xd = -xd;
      yd = Y[j]-Y[i];
      if (yd < 0) yd = -yd;
      AM[i].push_back(xd+yd);
    }
  }

  /* Print the adjacency matrix. */

  for (i = 0; i < AM.size(); i++) {
    for (j = 0; j < AM[i].size(); j++) {
      printf("%6lld", AM[i][j]);
    }
    printf("\n");
  }

  return 0;
}

We run this on examples 0 and 1, verifying that it is correct:

UNIX> cp Teleport-2.cpp ThreeTeleports.cpp
UNIX> g++ Teleport-Main.cpp
UNIX> a.out 0
     0  1995  1996  1997  1998  1999  2000     3
  1995     0     1     2     3     4     5  1992
  1996     1     0     1     2     3     4  1993
  1997     2     1     0     1     2     3  1994
  1998     3     2     1     0     1     2  1995
  1999     4     3     2     1     0     1  1996
  2000     5     4     3     2     1     0  1997
     3  1992  1993  1994  1995  1996  1997     0
0
UNIX> a.out 1
     0     2    38  2003  2004  2005  2006    40
     2     0    36  2001  2002  2003  2004    38
    38    36     0  1965  1966  1967  1968     2
  2003  2001  1965     0     1     2     3  1963
  2004  2002  1966     1     0     1     2  1964
  2005  2003  1967     2     1     0     1  1965
  2006  2004  1968     3     2     1     0  1966
    40    38     2  1963  1964  1965  1966     0
0
UNIX> 

Part 3: Adding the teleports to the adjacency matrix

Next, we need to add the teleports to the adjacency matrix -- these are between 1/2, 3/4, 5/6, and are equal to 10 jumps. This is in Teleport-3.cpp. Here's the new code, which is added right before we print the adjacency matrix:

  ...
  /* Add the teleports to the adjacency matrix. */

  AM[1][2] = 10;
  AM[2][1] = 10;
  AM[3][4] = 10;
  AM[4][3] = 10;
  AM[5][6] = 10;
  AM[6][5] = 10;
  ...

I've highlighed the teleports in the output below:

UNIX> cp Teleport-3.cpp ThreeTeleports.cpp 
UNIX> g++ Teleport-Main.cpp
UNIX> a.out 1
     0     2    38  2003  2004  2005  2006    40
     2     0    10  2001  2002  2003  2004    38
    38    10     0  1965  1966  1967  1968     2
  2003  2001  1965     0    10     2     3  1963
  2004  2002  1966    10     0     1     2  1964
  2005  2003  1967     2     1     0    10  1965
  2006  2004  1968     3     2    10     0  1966
    40    38     2  1963  1964  1965  1966     0
0
UNIX> 

Part 4: Generating all possible paths

Now we need to generate all possible paths. We're going to be a little lazy doing this, because we're in a "Topcoder" mindset: Let's solve the problem so that it's fast enough for Topcoder, but let's not spend the extra work making it optimally fast.

Our first piece of laziness is the following: We're going to generate more paths than we need, by generating all permutations of the numbers from 1 to 7. We'll put zero in the front the permutation, and then treat that as a path. For example, the permutation 1,2,3,4,5,6,7 is the path 0-1-2-3-4-5-6-7, that I explained above.

This approach generates more paths than we need for two reasons:

We're going to ignore this wastefulness, though, because the number of permutations, (7!), is 5040, which is well fast enough for Topcoder. We couldn't use this solution if there were more teleports.

Our second piece of laziness is using next_permutation() from C++'s algorithm library. This generates the "next" lexicographic permutation of a vector, and we don't need to do recursion.

The code in Teleport-4.cpp does this, and prints out the permutations. What I do here is put all of the numbers from 0 through 7 into a vector path, and keep calling next_permutation() until path[0] changes from 0 to 1. Here's the new code:

  ...
  /* Generate and print the permutations of 1-7. 
     We do that with a vector containing the numbers 0 through 7,
     and keep calling next_permutation() until path[0] changes from 0 to 1. */
   
  for (i = 0; i < 8; i++) path.push_back(i);
  while (path[0] == 0) {
    for (i = 0; i < path.size(); i++) printf(" %d", path[i]);
    printf("\n");
    next_permutation(path.begin(), path.end());
  }
  
  return 0;
}

UNIX> cp Teleport-4.cpp ThreeTeleports.cpp
UNIX> g++ Teleport-Main.cpp
UNIX> a.out 0 | head -n 10
 0 1 2 3 4 5 6 7
 0 1 2 3 4 5 7 6
 0 1 2 3 4 6 5 7
 0 1 2 3 4 6 7 5
 0 1 2 3 4 7 5 6
 0 1 2 3 4 7 6 5
 0 1 2 3 5 4 6 7
 0 1 2 3 5 4 7 6
 0 1 2 3 5 6 4 7
 0 1 2 3 5 6 7 4
UNIX> a.out 0 | tail -n 10
 0 7 6 5 3 2 4 1
 0 7 6 5 3 4 1 2
 0 7 6 5 3 4 2 1
 0 7 6 5 4 1 2 3
 0 7 6 5 4 1 3 2
 0 7 6 5 4 2 1 3
 0 7 6 5 4 2 3 1
 0 7 6 5 4 3 1 2
 0 7 6 5 4 3 2 1
0
UNIX> a.out 0 | wc
    5041   40321   85682
UNIX> 
There are 5041 lines, because the last line prints the return value of the procedure.

Part 5: Calculating the path lengths and returning the minimum.

The final piece of code takes each permutation and calculates a path length from it, keeping track of the minimum. I start by setting my minimum to a number larger than it can possibly be -- in this case, I choose a gigantic long long -- this is a one followed by 36 zeros, or 236. I specify the number in hexadecimal (remember, each hex digit is 4 bits, so I have 9 zeros to make 236.). I have to put "LL" at the end of the to tell the compiler that it's a long long.

Here's the new code (in Teleport-5.cpp):

  ...
  minimum = 0x1000000000LL;   /* This number is 2^36. */

  for (i = 0; i < 8; i++) path.push_back(i);
  while (path[0] == 0) {
 
    /* Here is where we calculate the path length. */     
    d = 0;                    
    for (i = 0; path[i] != 7; i++) {
      d += AM[path[i]][path[i+1]];
    }
    if (d < minimum) minimum = d;
    next_permutation(path.begin(), path.end());
  }
  
  return minimum;
}

This solves the problem fast enough for topcoder, so if all we cared about was solving the topcoder problem, we'd be done.

But.....


Doing the proper enumeration

As a mention in part 4 above, the enumeration is wasteful, because it generates too many paths that are either redundant, or are paths that the frog would never take. I'm going to do this correctly here, just because I wouldn't be a good teacher if I didn't.

What I'm going to do is enumerate teleport orderings. I'm going to label them by their lowest indices in path: 1, 3 and 5, and I need to enumerate all possible orderings of zero through three teleports. Let's start with that code first. I'm going to write a recursive proecedure enumerate_teleports, which will enumerate all strings that contain at most one "1", one "3" and one "5". This is pretty much identical to the recursive permutation enumeration from the Enumeration lecture notes, except at the beginning of the recursive procedure, we store the first index characters of the working string into the vector all_orders. At the end, all_orders will contain the strings we want.

The code is in Teleport-6.cpp. Here's the recursive procedure:

/* Enumerate all orderings of teleports.  We'll do this by having "work" start as "135",
   and then we recursively enumerate permutations, storing each prefix when we enter the
   recursive procedure. */

void enumerate_teleports(int index, string &work, vector <string> &all_orders)
{
  char tmp;
  size_t i;

  all_orders.push_back(work.substr(0, index));  // This stores the prefix - first index characters
  for (i = index; i < work.size(); i++) {
    tmp = work[i];
    work[i] = work[index];
    work[index] = tmp;
    enumerate_teleports(index+1, work, all_orders);
    tmp = work[i];
    work[i] = work[index];
    work[index] = tmp;
  }
}

And here's the code that prints out the orderings:

  /* Do the enumeration of teleport orderings and print it out. */

  work = "135";
  enumerate_teleports(0, work, all_orders);
  for (i = 0; i < all_orders.size(); i++) cout << all_orders[i] << endl;
  exit(0);

We run it, and you'll see it prints all of the teleport orderings (including the empty string):

UNIX> cp Teleport-6.cpp ThreeTeleports.cpp 
UNIX> g++ Teleport-Main.cpp 
UNIX> ./a.out 0

1
13
135
15
153
3
31
315
35
351
5
53
531
51
513
UNIX> 

Doing a power-set enumeration of how we travel through the teleports

Suppose we have an ordering of teleports, like "13". We need to now enumerate all of the ways that we can travel through each teleport. With "13", there will be four such ways -- I'll show them as paths:
1:     0 -> 1 -> 2 -> 3 -> 4 -> 7
2:     0 -> 2 -> 1 -> 3 -> 4 -> 7
3:     0 -> 1 -> 2 -> 4 -> 3 -> 7
4:     0 -> 2 -> 1 -> 4 -> 3 -> 7
We can do this enumeration with a power set enumeration -- if there are n teleports, we enumerate all possible strings of length n composed of the characters '<' and '>'. Each teleport corresponds to a character in the string. If the character is '<', then we travel the teleport in one direction, and if it is '>', then we travel it in the other direction. The code to generate these strings is in Teleport-7.cpp:

  /* For each ordering of teleports (t), do a power set enumeration to determine
     the order through the teleports (o).  Print them out. */

  for (i = 0; i < all_orders.size(); i++) {
    t = all_orders[i];
    for (j = 0; j < (1 << t.size()); j++) {
      o = "";
      for (k = 0; k < t.size(); k++) {
        if (j & (1 << k)) o.push_back('<'); else o.push_back('>');
      }
      printf("%3s %3s\n", t.c_str(), o.c_str());
    }
  }
}

Since there are a lot of strings to print (79), I just show the first 15, but you can see how the power set enumeration works:

UNIX> cp Teleport-7.cpp ThreeTeleports.cpp 
UNIX> g++ Teleport-Main.cpp 
UNIX> a.out 0 | head -n 15
       
  1   >
  1   <
 13  >>
 13  <>
 13  ><
 13  <<
135 >>>
135 <>>
135 ><>
135 <<>
135 >><
135 <><
135 ><<
135 <<<
UNIX>

Finishing up -- calculating the path lengths

Finally, I write a procedure to calculate the path length from the two strings and the adjacency matrix, and then calculate the minimum length. Here's the procedure (in Teleport-8.cpp).

/* Calculate the path lengths from the two strings: 

     t is the ordering of the teleports, composed of the characters '1', '3' and '5'
     o is the ordering through the teleports, composed of the characters '<' and '>'
     AM is the adjacency matrix
 */

long long path_length(const string &t, 
                      const string &o, 
                      const vector < vector <long long > > &AM)
{
  vector <int> path;
  int i;
  long long length;

  /* Create the path vector */

  path.push_back(0);
  for (i = 0; i < t.size(); i++) {
    if (o[i] == '<') {
      path.push_back(t[i]-'0');
      path.push_back(t[i]-'0'+1);
    } else {
      path.push_back(t[i]-'0'+1);
      path.push_back(t[i]-'0');
    }
  }
  path.push_back(7);

  /* Use that to calculate the path length */

  length = 0;
  for (i = 1; i < path.size(); i++) {
    length += AM[path[i-1]][path[i]];
  }
  return length;
}

And here's the code where we do the enumeration and calculate the minimum:

  /* For each ordering of teleports (t), do a power set enumeration to determine
     the order through the teleports (o).  Use path_lengh() to calculate the path
     lengths and store the minimum. */

  minimum = path_length("", "", AM);   /* Handle this case, because it isn't handled in
                                          the enumeration below. */

  for (i = 0; i < all_orders.size(); i++) {
    t = all_orders[i];
    for (j = 0; j < (1 << t.size()); j++) {
      o = "";
      for (k = 0; k < t.size(); k++) {
        if (j & (1 << k)) o.push_back('<'); else o.push_back('>');
      }
      d = path_length(t, o, AM);
      if (d < minimum) minimum = d;
    }
  }
  return minimum;
}

We're done! And instead of enumerating 5040 times, we're only calling path_length() 79 times!

UNIX> cp Teleport-8.cpp ThreeTeleports.cpp ; g++ Teleport-Main.cpp 
UNIX> for i in 0 1 2 3 4 5 ; do ./a.out $i ; done
3
14
14
30
117
36
UNIX>