SRM 340, D1, 500-point problem

I give this problem for you to do in lab. I don't really think that you'll finish it in lab, but I really want you to finish it. This type of problem is common, where you figure out how to reduce the problem to a graph problem where the graph is constructed on the fly. It is very good practice for you to complete this problem.

The problem description is here.. There is a driver in Cs-Main.cpp.

Like many problems in Topcoder, the key to this one is figuring out how to map it to a problem that you know how to solve already. In this case, it smells like a shortest path problem, where the classes will be the edges. The question is how to convert the problem to the proper graph? If you want to think about it on your own, go ahead, but don't write any code until you have figured out how to convert the problem to a graph with an unweighted shortest path solution.

It makes sense to create nodes that are indexed by [T,P], where T is the theoretical value that you have achieved, and P is the practical value. You will start at node [0,0], and you want to get to a node [T,P] where both T and P are ≥ skillBound.

Classes will compose the edges. Let's ignore when classes expire for now. There is an edge from node [T,P] to node [T',P'] as long as:

So, example 0 maps to the following graph (I'm labeling the edges with the class number, not a weight):

Because we are ignoring expiration, examples 1 and 2 map to:

After you create the graph, you want to find the shortest path from [0,0] to [T,P] where both T and P are ≥ skillBound. If there are multiple paths, as in example 1, you want the lexicographically smaller one, which means you perform the BFS in ascending order of edges. In Example 1, when you reach node [1,1], you process the edge to [1,2] first, because class 0 is smaller than class 1.

That's all well and good, but there is one subtlety -- when you reach a node, how do you know whether a class has expired? The answer is that the shortest path to that node is the quickest way that you can reach that node -- if the shortest path is s, then you can get to that node by month s. For example, in both examples 1 and 2, the shortest path to node [1,1] is 1, so you can get to that node by month 1. To leave the node, you have to take a class that expires in a month greater than one. In example 2, there are no such classes, so you cannot leave that node.


Organizing the solution

Much like the CarrotJumping problem, you are going to build your graph incrementally. Here's the strategy:

Writing the Solution

I used the following data structure for nodes:

class Node {
  public:
    int T;
    int P;
    int back_class;
    Node *back_node;
    int path_length;
};

Back_class is the number of the class that got me to this node. Back_node is a pointer to the previous node in the shortest path. path_length is the length of the shortest path to each node.

I created a doubly-indexed vector of pointers to nodes, and then resized it so that all values of T and P between 0 and 50 are defined. I created nodes for each of these, set their values of T and P, and sentinelized back_node to NULL and path_length to -1. You'll note that I end up creating more nodes than I really need. For example, if skillBound is 10, there will never be nodes where both T and P are greater than 10. However, I don't care -- it's easy to create the nodes in this way.

Then I wrote a BFS routine that first pushes node [0,0] on the queue and then processes the queue. For each node that I process, I traverse the classes and determine if there is an edge from that node to another. That's a little tricky. If there is an edge, then I check to make sure that the node is not already on the queue by testing to see if path_length is equal to -1. If it passes that test, I set path_length and push it on the queue.

If I get to an ending node, where T and P are big enough, I simply print the path length and exit. Note, I'm ignoring expiration here. I test this first program to make sure that it makes sense on the examples:

UNIX> a.out 0

On node [0,0]
Considering Class 0: [1,1]
Pushing [1,1] from [0,0] - path length 1

On node [1,1]
Path found: Length = 1
UNIX> a.out 1

On node [0,0]
Considering Class 0: [1,2]
Considering Class 1: [2,1]
Considering Class 2: [1,1]
Pushing [1,1] from [0,0] - path length 1

On node [1,1]
Considering Class 0: [1,2]
Pushing [1,2] from [1,1] - path length 2
Considering Class 1: [2,1]
Pushing [2,1] from [1,1] - path length 2
Considering Class 2: [1,1]

On node [1,2]
Considering Class 0: [1,2]
Considering Class 1: [2,1]
Pushing [2,2] from [1,2] - path length 3
Considering Class 2: [1,1]

On node [2,1]
Considering Class 0: [1,2]
Considering Class 1: [2,1]
Considering Class 2: [1,1]

On node [2,2]
Path found: Length = 3
UNIX> a.out 2 | tail -n 1
Path found: Length = 3        /* This is right because I'm ignoring expiration. */
UNIX> a.out 3 | tail -n 1
Path found: Length = 3
UNIX> a.out 4 | tail -n 1
Path found: Length = 7
UNIX> a.out 5 | tail -n 1
Path found: Length = 2
UNIX> 
Next, I add expiration. That kills example 2:
UNIX> a.out 1 | tail -n 1
Path found: Length = 3
UNIX> a.out 2 | tail -n 1
No path found
UNIX> 
Finally, I add back edges and the back classes, calculate and return the path:
UNIX> a.out 0
{0}
UNIX> a.out 1
{2 0 1}
UNIX> a.out 2
UNIX> a.out 3
{2 1 0}
UNIX> a.out 4
{0 1 2 3 4 5 6}
UNIX> a.out 5
{4 3}
UNIX> 

If you think about running time complexity, there are 2601 nodes, and each time you process a node, you run through up to 50 classes. 50 times 2601 equals 130,050, and you get roughly 2,000,000 operations with Topcoder -- it should easily run within the time bound.

My code is in CsCourses.cpp.