Thursday, March 25, 2004 Match summary SRM 188 had a host of difficult problems, testing coders' skills with a surprisingly tricky precision problem, parsing and formatting tasks, and a recursive graph problem that hurt many people's brains. radeye had the DivisionI lead after the coding phase, finishing all three problems in 55 minutes. After picking up 50 points in the challenge phase and surviving the system tests, he held on to his lead to finish with 1268 points, over 300 points ahead of secondplace finisher bladerunner. wleite's three successful challenges were enough to catapult himself to a thirdplace finish, with SnapDragon and Eryx rounding out the top five. Only three solutions (from radeye, bladerunner, and hauser) of the eight submitted for the 1000point problem passed the system tests. In DivisionII, dzetkulict finished with 1146 points, and even with a failed challenge, managed to secure a 15point lead over secondplace finisher Hilary.Duff. Filling out the rest of the top five positions were tmyklebu, Vovka, and Ascription, all with correct solutions to all three problems. In all, 19 DivisionII coders finished the 1000point problem.
The ProblemsMagicSquareUsed as: Division Two  Level One:
This problem presented the coders with too much information. They were given a 3x3 magic square with one number missing, and asked to return the missing number. With only one number missing, there are still two complete rows and two complete columns. One only had to identity one complete row or column to compute the magic sum. Subtracting the other two numbers from either the row or column with the missing number from this magic sum revealed the answer. For particularly efficient implementations, see the solutions of fame and dzetkulict. An alternative solution is to simply try all numbers from 1 to 100 in the missing spot, and test if the resulting square is magic. This was the technique used by tmyklebu. PercentsUsed as: Division Two  Level Two: Used as: Division One  Level One:
This problem was the ambush of the evening, with only 45.81% of DivisionI and 20.54% of DivisionII submissions passing. If you wish to see the inspiration for this problem, look no further than the statistics immediately above this paragraph... Coders were asked to identify the minimum possible denominator of a fraction that could result in the given percentage, rounded to two decimal places. Rounding correctly was the key to solving this problem. Many coders used floatingpoint arithmetic, and by doing so, they were just begging to run in to precision issues. The problem is, floatingpoint numbers do not represent all real numbers exactly. Bulletproof solutions to problems like this require integer arithmetic. The dangerous way to round the fraction a/b to the nearest integer is: int c = floor( (double) a / (double) b + 0.5); This involves a floatingpoint divide and a floatingpoint addition, each which will approximate the value you may expect, but not equal it exactly. However, notice what we can do with a little algebraic manipulation, putting the terms over a common denominator: int c = floor( (double) (2*a + b) / (double) 2*b ); This result is precisely what you get with integer division: int c = (2*a + b) / 2*b; No floatingpoint arithmetic, no precision errors, just the correct result every time. The above formula can be made to round to the nearest hundredth of a percent by first multiplying the numerator by 100, and again by 100 to convert from a fraction to a percentage. To turn this into a complete solution, test each denominator starting at 1. Estimate the numerator (as hinted in the nodes of the problem statement), compute the rounded percentage, and compare it to the given value. If they match, return that denominator. Here is radeye's elegant solution: public class Percents { public int minSamples(String percent) { int i, j, k ; int t = (int)Math.floor(0.2+100*Double.parseDouble(percent.substring(0, 5))) ; for (i=1; i<=10000; i++) { int f = t * i / 10000 ; if ((20000 * f + i) / (2 * i) == t  (20000 * (f + 1) + i) / (2 * i) == t) return i ; } return 1 ; } }PolynomialMultiplier Used as: Division Two  Level Three:
This problem was more an exercise in parsing and formatting output than a math problem. Coders were asked to parse two polynomials, multiply them together, simplify the product, and output the result. Successful solutions to this problem stored the polynomial as an array of integers, with each element representing the coefficient of a particular power of x. Terms in each polynomial were parsed one by one to determine the coefficient and power, and the array element indexed by the power of each term was incremented by the corresponding coefficient. Multiplying these two polynomials can be done with two nested loops, multiplying the coefficients of each pair of powers in the two arrays. The products are stored in a third array, indexed by the sum of the two powers. Storing the polynomials in this fashion takes care of the simplifying step. The polynomial is returned by looping over all values in the array (from greatest to least), and outputting the nonzero terms according to the formatting instructions. Care must be taken to identify the correct case in order to format the the result properly. PartialUsed as: Division One  Level Two:
This problem, similar to the DivisionII hard, was more an exercise in parsing and output formatting than a math problem. Coders were asked to parse a function of 3 variables, compute a mixed partial derivative of that function, and then output the result according to specific formatting instructions. Successful solutions to this problem stored the function as a 3dimensional array of coefficients, with the three indices corresponding to the powers of x, y, and z. Parsing the function involved first separating the string by the " + " strings to identify each term, and then separating the terms by the '*' character to identify each factor. Looping over each factor, the powers of variables present in the term are accumulated, along with a coefficient if present. Variables not appearing in the term have a power of zero, and if no constant factor is present, the coefficient is 1. The coefficient of each term is added to the array element indexed by the three powers. Computing the required partial derivatives is straightforward. For each term in the array, multiply the coefficient by the power of the selected variable and store the result in a new array with the index of the selected power decremented by one. Multiplying by zero takes care of removing terms that are eliminated as the result of differentiating. Outputting the terms in the correct order can be done with 3 nested loops: first over all possible degrees (from greatest to least), then over powers of x (from greatest to least), and finally over powers of y (also from greatest to least). Note that there is no need to loop over z, as it is fixed by the degree, x, and y. RecursiveGraphUsed as: Division One  Level Three:
RecursiveGraph stumped many coders, with only eight submitting solutions, and only three of those surviving the challenge phase and system tests. This problem asked coders to find the shortest path through a graph with an infinite number of nodes and edges, defined recursively. The graph contains up to 9 copies of itself, with edges connecting toplevel nodes to nodes in the recursive copies. The graph was defined solely in terms of toplevel edges and edges connecting to its children. Several coders had trouble wrapping their brain around the problem, not fully realizing the distinction between multiple child graphs replicated at a given level, and recursive copies appearing at successively deeper levels. If anyone is still confused, I would encourage them to go over the example in the problem with a pencil and paper, and see how the graph in the figure is defined by just 5 edges. Fortunately, we don't need to consider an infinite number of nodes and edges, because the weights of the edges are divided by two (and rounded down) at each level. With a maximum weight of 1000 at the toplevel, all recursive copies below the 10th level have only zeroweight edges. This is the key to the solution of this problem. The first step is to find the connectivity of all nodes in the graph, and use this to "flatten" all levels of the graph after level 10. At this stage, we just want to determine, for each node, which nodes it can reach. Note that in the graph { "A A1 1", "B B1 1" }, there is no path from A to B, regardless of how far down you recurse. Once you've found the connectivity, the shortest path can be determined by working up from the bottom. First compute the weights of the edges at level 10 (where all weights are either 0 or 1), and insert the connectivity graph in place of all recursive copies. Compute the distance between each pair of nodes, and insert these results in place of the children in level 9. Continue in this fashion, using the solution at each level in place of the recursive copies in the level above until you reach the top. At each level, you need only consider 100 nodes, the 10 nodes for that level and the 9*10 nodes of its children. The deepest graph in the system tests recurses down 19 levels deep: edges = { "A A1 0", "J J1 0", "A B1 1000", "B C1 1000", "C D1 1000", "D E1 1000", "E F1 1000", "F G1 1000", "G H1 1000", "H I1 1000", "I J1 1000" } start = 'A' end = 'J' The shortest path first travels down the "A A1 0" edges 10 times to get to the realm of zeroweight edges. It then follows the "A B1 1000", "B C1 1000", etc. edges to make its way to node J, recursing one level deeper at each step. It then follows nineteen "J J1 0" edges back up to the top. The length of this path is zero. The problem statement contained a potentially misleading constraint that the solution should return 1 if the shortest path is greater than one billion. I don't believe a path greater than 10 million is possible, but without being able to prove this, this constraint was left in. I would be interested in hearing from anyone who proves a lowerbound on this problem. 
