User:Jeff/Algorithms/Line Plotter

Back to ACM page

On a plotter, the mechanical arm that draws on paper with a pen has a motor that can move it back and forth across the paper sideways. The plotter also has a paper motor, which can move the paper orthogonally to the plotter's arm, which effectively is like moving the pen up and down the paper. The paper is square, and is oriented so its lower edge is parallel to the arm. This allows the plotter to move to any coordinates using the two motors. Both motors can be active simultaneously, and each motor can move the pen up to one pixel per millisecond in relation to the paper (it's possible for a motor to run slower).

Given the list of lines that the plotter must put on the paper, return the minimum time it takes to plot the given lines, in milliseconds. The plotter's pen starts at the coordinates (0,0), and after drawing all the lines, must return to (0,0). Note that the plotter cannot draw partial lines. It takes 0 time to lower the pen down on the paper, and to raise it off the paper.


 * There can be 0-length lines, which are drawn by moving to the designated location, lowering the pen and without moving, raising the pen.
 * lines will contain between 1 and 15 elements, inclusive.
 * Each element of lines will be of the form "x1 y1 x2 y2", where each value is an integer between 0 and 9999, inclusive.
 * No two lines can intersect at more than one point.

Examples
timeToPlot({"0 1 1 0"}) = 3 This is a line which draws a diagonal across the bottom corner of the paper. The line can be drawn by first running the arm motor to bring the pen over to (1,0) in 1 millisecond. Then the paper motor can be run simultaneously with the lateral motor to draw the line from (1,0) to (0,1) in one more millisecond. Finally, the paper motor brings the pen back to (0,0) in one more millisecond.

timeToPlot({"0 0 5 5", "5 5 0 5", "0 100 0 4"}) = 205 The best path first draws the line from (0,0) to (5,5). Since both motors can move at top speed, this line takes 5 milliseconds to draw. Without even picking up the pen, we can draw the line from (5,5) to (0,5) in 5 milliseconds. Finally, to draw the final line, we lift the pen, go all the way up to (0,100), and draw the line to (0,4). This adds 95 + 96 = 191 milliseconds. Finally, we lift the pen and move back to (0,0), adding an additional 4 milliseconds. The total time is 5 + 5 + 95 + 96 + 4 = 205 milliseconds.

Analysis
There can be up to 15 lines to draw, and each line can be drawn either forward or backward from how it's listed in the input. A brute force approach would therefore take 15!*2^15 = 42 quadrillion calculations, which is many orders of magnitude outside the time limit. We can reduce this number by using a recursive approach.

We want to write a function that will calculate the minimum time necessary to draw all the lines. This is equivalent to writing a function that figures out the best line to draw next, draws it, and then calls itself recursively to draw the rest of the lines. This function would need to know two things: our current location on the paper, and which of the lines we've already drawn. The current location will always be either (0,0) or one of the endpoints of the 15 lines, for a total of 31 possible locations. There are also 2^15 possibilities for which lines we've already drawn, which yields a maximum of 31*2^15 = 1015808 function calls. This number is easily reachable in the time limit.

Solution
First we should write a function to find the time it takes to move the pen from point A to point B. Whichever motor has to cover the longer distance (from X1 to X2 or from Y1 to Y2) should be running at top speed, so the time is simply the infinity "sup" distance between the two points.

int timeToDraw(point a, point b) { return abs(a.X - b.X) >? abs(a.Y - b.Y); }

Now to write the recursive function. The terminating case is when all the lines have already been drawn, and the plotter needs only to return to (0,0). Otherwise, it goes through the bitmask of lines, and for each line that hasn't been drawn, figures out the total time if that line were drawn next. This requires two lines of code, since we attempt to plot the line in both directions. The function returns the best time found out of all the choices.

int bestTime(vector &lines, point location, int linesDrawn) { int ans(INF); for (int i(0); i < lines.size; ++i) { if (linesDrawn & (1 << i)) continue; // already drawn this line ans <?= timeToDraw(location, lines[i].X) + timeToDraw(lines[i].X, lines[i].Y)        + bestTime(lines, lines[i].Y, linesDrawn + (1 << i)); // forward ans <?= timeToDraw(location, lines[i].Y) + timeToDraw(lines[i].Y, lines[i].X)        + bestTime(lines, lines[i].X, linesDrawn + (1 << i)); // backward }  if (ans == INF) // no lines left to draw return timeToDraw(location, point(0,0)); return ans; }

The only step remaining is to memoize this function, and to write the wrapper function to parse the points.

Source code
using namespace std; typedef pair point; typedef pair line; int timeToDraw(point a, point b) { return (a.X-b.X) >? (a.Y-b.Y) >? (b.X-a.X) >? (b.Y-a.Y); } int bestTime(vector &lines, point location, int linesDrawn,             map, int> &mem) { pair key(location, linesDrawn); if (mem.find(key) != mem.end) return mem[key]; int ans(INF); for (int i(0); i < lines.size; ++i) { if (linesDrawn & (1 << i)) continue; ans <?= timeToDraw(location, lines[i].X) + timeToDraw(lines[i].X, lines[i].Y)        + bestTime(lines, lines[i].Y, linesDrawn + (1 << i), mem); ans <?= timeToDraw(location, lines[i].Y) + timeToDraw(lines[i].Y, lines[i].X)        + bestTime(lines, lines[i].X, linesDrawn + (1 << i), mem); }  if (ans == INF) return timeToDraw(location, point(0,0)); return mem[key] = ans; } int timeToPlot(vector lines) { int n(lines.size); vector l(n); for (int i(0); i < n; ++i) { stringstream ss(lines[i]); ss >> l[i].X.X >> l[i].X.Y >> l[i].Y.X >> l[i].Y.Y;  } map, int> mem; return bestTime(l, point(0,0), 0, mem); }
 * 1) include
 * 2) include
 * 3) include
 * 4) include
 * 1) define X first
 * 2) define Y second
 * 3) define INF 2000000000