JOIN
Get Time
statistics_w  Match Editorial
SRM 150
Wednesday, June 11, 2003

Match summary

Divison-I had a tricky medium level problem which most competitors got stuck at; in the end, only 11 people solved two or more problems. Most likely more people would have solved the hard problem if they had skipped the medium one at an early stage. Winner was (dare I say as usual?) SnapDragon ahead of bstanescu and radeye, the only three to solve all three problems. With this SnapDragon passed the 3400 rating mark, 340 points ahead of the second highest ranked - incredible! In divison-II, rustyoldman won comfortable, more than 250 points ahead of the runner-up (and first timer) skye85.

The Problems

WidgetRepairs  discuss it
Used as: Division-II, Level 1:
Value250
Submission Rate188 / 204 (92.16%)
Success Rate150 / 188 (79.79%)
High Scorejimrandomh for 246.95 points

Reference implementation: rustyoldman

Implementation

We simulate each day by keeping track of the number of broken widgets the shop has each day. At the beginning of each day we check if any new broken widgets has arrived, and if so update the total count of broken widgets. If this count is greater than 0, we increase the count of how many days the repair shop has to work. We then decrease the number of broken widgets with how many the repair shop can fix in one day (just make sure the total amount of broken widgets don't go below 0, they don't create new widgets after all!).

One thing to watch out for is to know when we're done. We're done when we have no broken widgets left, and that no more broken widgets will arrive. The first condition is easy to check, and the second one is true after day 20 at the very least, since the number of elements in arrival is at most 20 (no more broken widgets arrive after day 20). The outer loop can then look like this:

while (brokenWidgets || day<=20) {
  ...
}

The reference solution uses a safe upper bound of 25,000 days. Check it out for a complete implementation of the problem.

InterestingDigits  discuss it
Used as: Division-II, Level 2:
Value500
Submission Rate95 / 204 (46.57%)
Success Rate76 / 95 (80.00%)
High ScoreSpiffage for 486.51 points
Used as: Division-I, Level 1:
Value250
Submission Rate119 / 123 (96.75% )
Success Rate115 / 119 (96.64%)
High ScoreSnapDragon for 248.83 points

Reference implementations: Yarin, SnapDragon

Implementation

We start with an outer loop from 2 to base-1, testing whether or not a digit is "interesting". If it is, we'll add it to the list of interesting values:

vector<int> digitList;
for(int i=2;i<base;i++) // Don't check 0 and 1
  if (interesting(i,base))
    digitList.push_back(i);
return digitList;

Now, there are two ways to check if a digit is interesting: brute force (according to the problem description) or using a discrete math property. Lets start with the brute force version.

According to the problem description, we should check all multiples of the digit, but only those having at most 3 digits. The largest number in base B with 3 digits is simply B3-1, so we have the following for-loop:

bool interesting(int d, int b) {
  // Loop i through all multiples of digit, that is:
  // 0, d, 2*d etc up to b^3
  for(int i=0;i<b*b*b;i+=d) {
    ...
  }
  return true;
}

Now we should check that the sum of the digits of i (in base b!) is also a multiple of d. To calculate the sum of the digits, remember how we convert a decimal number to a string: we take the number modulo 10 to get the last digit, then divide by 10, take modulo 10 again to get the second last digit etc. Now, we just replace the 10 with b to make the same thing in the base we're checking:

    int sum=0,n=i;
    while (n) {
      sum+=n%b;
      n/=b;
    }
    if (sum%d) return false; // The digit sum is not a multiple of d!

Now to the math(emagical) solution:

bool interesting(int d, int b) {
  return b%d==1;
}

Whoa! Wait a second, how can that work? Assume a digit D is interesting (that is, if A is a multiple of D, then so is the digit sum of A) in base B, and that B%D=1. Let Ai be digit i in A - that is, A = A0*B0 + A1*B1 + ... We have:

A0*B0 + A1*B1 + A2*B2 + ... ≡ 0 (mod D)
But since B ≡ 1 (mod D), we also have Bn ≡ 1 (mod D), so:
A0 + A1 + A2 + ... ≡ 0 (mod D)

And thus the digit sum of A is a multiple of D. It's slightly trickier to prove that if B%D is not 1, then D is not an interesting number, so I'll leave that as an exercise :)

BrickByBrick  discuss it
Used as: Division-II, Level 3:
Value1100
Submission Rate12 / 204 (5.88%)
Success Rate4 / 12 (33.33%)
High ScoreRustyoldman for 638.90 points

Reference implementation: rustyoldman

Implementation

This problem makes you think about Arkanoid (or Break Out or whatever), doesn't it?

We should try to simulate the balls movement in the grid until all bricks are gone, or return -1 if this never happens. How can we know if this will never happen? If the ball ever appears on the same position and same direction as it has before and no brick was destroyed between these two occasions, we're stuck in an infinite loop and can safely return -1. We don't actually have to store all previous ball locations and directions, because this condition will happen if we haven't destroyed a brick in a sufficient long time. If there are only, say, 256 possible locations and 4 possible directions for the ball, if after 1025 time steps no brick has been destroyed, we must (by the pigeon hole principle) be at a location and direction we have been at before.

Now to the simulation. If we look at the diagram in the problem description, we see that the relevant position of the balls is at the grid lines, not the actual squares within the grid (even though that is where the bricks are). Thus we set out coordinate system so that x=0 means the leftmost vertical grid line, x=2 the one after that etc while the odd x coordinates indicates that the ball is a on horizontal grid line, see figure.

The ball starts at position x=1,y=0 and has direction dx=1,dy=1. During the simulation, we update the ball location (x+=dx, y+=dy) and then figure out which grid square it's adjacent too. This is slightly tricky, because you have to split into several cases: If x is even, the desired grid square is (x/2-1,y/2) if dx<0 or (x/2,y/2) if dx>0. If x is odd, then y is even (one of the coordinates is always even) and we calculate the grid square in a similar way. Once we know the grid square, we check if contains a brick or a wall (we may start the problem by adding a wall surrounding the whole grid so we don't have to check if the ball gets out of bounds). If it's a wall or a brick, the ball changes direction. If we hit a brick to the left or right (which is the case if x is even), then we change direction horizontally (dx=-dx), otherwise we change direction vertically (dy=-dy). If the square was a brick, remove it, and don't forget to reset the counter when we last hit a brick!

And that's basically it. See the reference solution for the details of how to implement all this.

StripePainter  discuss it
Used as: Division-I, Level 2:
Value500
Submission Rate23 / 123 (18.70%)
Success Rate10 / 23 (43.48%)
High Scorebstanescu for 390.72 points

Reference implementations: DjinnKahn, ValD

Implementation

This was an unusually hard 500p, requiring a non-straightforward dynamic programming solution (or memoization). As almost always is the case with this type of problems, the solution is easy once you see it. However, in an ordinary SRM, this would be rated as a hard problem. Why it was not a 600 pointer, I don't understand.

Lets try the standard induction approach: given a continuous part of the original strip and the current color this strip has (initially the whole strip has an undefined color), we want to break this down into smaller instances of the same problem. Define this problem like this:

min[left,size,color] = the minimum number of brushes required to paint position
                       left, left+1, ... , left+size-1 of the original strip,
                       assuming these position currently has color color.

The desired return value is then simply min[0,stripes.length(),'?'].

It requires one insight to solve this min-problem: the next stroke may always start at the leftmost position unless this position is already in the correct color. If the leftmost position is in the wrong color, we must paint it sooner or later anyway, so why not now? This reasoning only make sense for the leftmost (and rightmost) position, because there is no gain to first paint this position with another color, which may be true of interior positions.

So we first check if the leftmost position is in the wrong color. If it already is in the desired color, we simply return min[left+1,size-1,color] since we then have one less position to worry about. Otherwise we loop over all possible stroke lengths, from 1 to size:

loop i from 1 to size
   sum = 1 + min[left+1,i-1,stripe[left]] + min[left+i,size-i,color];
   update best solution found so far if necessary
end loop

The logic is as follows: we make a stroke of color stripe[left] (the color of the leftmost position) from left to left+i-1, inclusive - this accounts for the 1. We then get two subproblems, one part of the strip is in color stripe[left], the other part is in the original color. We solve these subproblems by a recursive call. On the part of the strip which we changed color, we know that the leftmost position is in the correct color, so we can skip ahead one position there (thus explaining the left+1,i-1).

What remains is a terminating case, so we don't recurse forever. That one is simple: if the length of the stripe is zero, it requires no strokes at all.

When implementing this, it's essential that we use memoization, otherwise our solution will time out pretty fast. Memoization should always be considered in a recursive function which doesn't have any side effects, such as changing global variables. A function is a pure mathematical function if it always returns the same value given the same input parameters. Such functions are the only ones memoization can be applied on: if we ever call the function with the same parameters as we've done before, we just look up what answer we got last time instead of evaluating it again. It can be implemented like this:

int memo[50][51][128]; // Initialize to -1 to mark not evaluated

int min(int left, int size, char col)
{
  if (memo[left][size][col]>=0) return memo[left][size][col];
  
  // Evaluate function and store result in best
  
  memo[left][size][col]=best;
  return best;
}

One can also use dynamic programming to solve the problem. This is basically the same thing, except that instead of the recursive calls, we evaluate the memo table in such an order that the results of recursive calls already has been calculated, like this:

for(int size=0;size<=stripes.length();size++)
  for(left=0;left<stripes.length()-size;left++)
    for(col='@';col<='Z';col++) {
      // Evaluate min[left][size][col] here
    }

Notice that in the evaluation of min[left,size,col] the subproblems always had a smaller size than the original problem, so if we evaluate the subproblems starting from size 0 and then working up, these recursive calls can instead be looked up directly from the memo table as we know they have already been evaluated.

And that's it! The hardest part in a problem like this is figuring out the mathematical function and what parameters it should use. Once this is done, it's pretty straightforward. Just remember that the function must be a mathematical one (without side effects), otherwise we can't use memoization.

For an implementation of the recursive approach, see DjinnKahns solution and for a dynamic programming solution, see the solution by ValD.

RoboCourier  discuss it
Used as: Division-I, Level 3:
Value1000
Submission Rate7 / 123 (5.69%)
Success Rate4 / 7 (57.14%)
High Scoreradeye for 604.51 points

Reference implementation: Yarin

Implementation

It's apparent that this is a typical shortest-path problem, albeit a slightly trickier one due to the hexagonal coordinate system and different travelling speeds depending on if it's the first (or last) move in a direction or not.

The first thing to do is to get rid of the annoying hexagonal coordinate system. Or rather, how to translate the 6 hexagonal directions into equivalent directions in a grid world. The following picture shows how this can be done:

As can be seen, the 6 directions corresponds to movements in x & y coordinates according to the picture to the right. Or, if translated to code, the following constant arrays:

const int dx[6]={0,1,1,0,-1,-1};
const int dy[6]={1,1,0,-1,-1,0};

We can thus easily represent a position in the hexagonal grid using ordinary x,y coordinates.

The next step is to convert the input to an undirected graph. Each node in the graph should correspond to a hexagon that the scout robot has visited, and the edges in the graph should correspond to movements from one hexagon to another by the robot. One can note that each node can have at most 6 edges, one for each possible direction, and there can be at most 501 nodes (for instance, if the scout executes 500 F's). So to represent each node in the graph, we can use the following structure: (C++ code)

struct TNode
{
  int x,y; // x and y coordinates of the node
  int edge[6]; // Node number to reach if travelling in this direction. -1 if no edge.
};

TNode nodes[501];
int noNodes; // Number of nodes in graph

We create this graph by simulating the movements of the scout robot. Every time the scout moves ahead, we check which node it's at (if it's a new node, we add the information about this nodes coordinate in the structure above) and add the appropriate edge (in both directions!). It might look like this:

int x=0,y=0,dir=0; // Start square
for(int i=0;i<pathConcat.size();i++) {
  switch (pathConcat[i]) {
    case 'R' : dir=(dir+1)%6; break;
    case 'L' : dir=(dir+5)%6; break;
    case 'F' :        
      src=lookup(x,y);
      x+=dx[dir];
      y+=dy[dir];           
      dest=lookup(x,y);
      nodes[src].edge[dir]=dest;
      nodes[dest].edge[(dir+3)%6]=src;
      break;
  }
}
int start_node=lookup(0,0);
int dest_node=lookup(x,y);

The lookup function simply takes a pair of coordinates and returns which node in the graph has this coordinate pair. If no node has yet been assigned this coordinate pair, we create a new node in the graph:

int lookup(int x, int y) {
  for(int i=0;i<noNodes;i++)
    if (nodes[i].x==x && nodes[i].y==y) return i;         
  // Create a new node
  for(int i=0;i<6;i++)
    nodes[noNodes].edge[i]=-1; // No edges from this new node yet
  nodes[noNodes].x=x;
  nodes[noNodes].y=y;
  return noNodes++;
}

Now we have a nice undirected graph corresponding to legal movements in the graph for our courier robot. All that remains is finding the shortest path from start_node to dest_node. Since turning 60 degrees also takes time (3 seconds), the actual search graph must contain even more nodes (6 times as many), but this graph doesn't have to be built explicitly. Let a state be the complete knowledge of the robots whereabouts: the location and the direction. Imagine creating a new graph with one node for each state - it is in this graph we do the shortest path search. From each state we can either turn 60 degrees clockwise or counter clockwise, or we can move one or more steps in the current direction:

int curNode=state/6,curDir=state%6;

if (dist[curNode*6+(curDir+1)%6]>timeUsed+3)
  dist[curNode*6+(curDir+1)%6]=timeUsed+3;
if (dist[curNode*6+(curDir+5)%6]>timeUsed+3)
  dist[curNode*6+(curDir+5)%6]=timeUsed+3;
  
int steps=0;
// Try moving in the current direction until it's not possible
while (nodes[curNode].edge[curDir]>=0) {
  steps++;
  curNode=nodes[curNode].edge[curDir];
  int timeMove=4; 
  if (steps>1) timeMove=8+(steps-2)*2;
  if (dist[curNode*6+curDir]>timeUsed+timeMove)
    dist[curNode*6+curDir]=timeUsed+timeMove;
}

One can also build into the state whether or not the robot has started to move, and thus avoid the while-loop in the code above, see SnapDragons solution for an implementation of that version.

Author
By Yarin
TopCoder Member