Wednesday, January 28, 2004 Match summary This SRM seemed significantly easier than the last few for Division I coders, with seven coders within 50 points of leader tomek after the coding phase. After challenge phase, however, SnapDragon barely pulled ahead by picking up a successful challenge, and he won the match with an impressive 1629.45 points. On the Division II side of things, the success rates for the easy and medium problems were very high, but many coders were stopped by the hard. The winner of it all was RandySaint, who used his high score on the 950 to finish in first place in Division II by over 100 points over sanjay31.
The ProblemsKiloManUsed as: Division Two  Level One:
Projectiles coming at KiloMan? No worries, just dodge! Unfortunately, that's not as easy as it sounds. If the shots are coming at height above 2 and KiloMan jumps, then he gets hit, and if they are at height 2 or less and he stands still, then he is also hit. To put that in code, that turns out to be just a couple of if statements: hits = 0; for (int i = 0; i < pattern.size(); i++) { if (pattern[i] > 2 && jumps[i] == 'J') hits++; else if (pattern[i] <= 2 && jumps[i] == 'S') hits++; } return hits;CombinationLock Used as: Division Two  Level Two: Used as: Division One  Level One:
The algorithm given in the problem statement to open an arbitrary combination lock could basically be directly translated into code. The initialization step was to set the current rotation direction to counterclockwise, and set N to be the number of numbers in the combination. Rotate the wheel N full turns in the current rotation direction, and then keep rotating until the notch points to the next number in the combination. Then decrement N, reverse the rotation direction, and repeat until there are no more numbers in the combination to input. The high success rate for this problem in both divisions was pretty indicative of its straightforward nature. int N = combo.length, L = N; double ret = 0; int at = start; int dir = 1; //we're calling 1 counterclockwise and 1 clockwise for (int i = 0; i < L; i++, N) { int K; if (dir == 1) K = combo[i]  at; else K = at  combo[i]; if (K < 0) K += size; ret += 360.0 * (N + 1.0*K/size); at = combo[i]; dir = dir; } return ret;EngineersPrimes Used as: Division Two  Level Three:
What we want is the smallest nonprime that isn't divisible by any of the first N. The first thing to do is determine what the first N primes are. Since N is, at most, 100000, and the 100000th prime is 1299709, you can use a variety of methods: Eratosthenes' Sieve, simply caching the primes as you find them, or the brute force, divide by every number up to the square root method. At this point, what is the smallest number that isn't divisible by those first N primes that isn't prime, either? First, since it's not prime, it must have at least two prime divisors; for the smallest number possible, there must be only two such divisors, which must each be as small as possible. However, neither of those two numbers can be any of the first N primes, otherwise their product would be divisible by one of those primes. Thus, we take the (N+1)st prime and square it, thus forming the smallest number not divisible by the first N primes that is also not prime itself. Any lower number is either: (a) prime, or (b) divisible by at least one of the first N primes. (The proof is left as an exercise for the reader.) int p[100001]; int at = 0; bool prime (int k) { for (int i = 0; i < at && p[i] * p[i] <= k; i++) if (k % p[i] == 0) return false; p[at++] = k; return true; } long long smallestNonPrime (int N) { p[at++] = 2; int count = 1; long long last = 1; for (int i = 3; at <= N; i += 2) if (prime(i)) last = i; return last * last; }TrueFalse Used as: Division One  Level Two:
As various match editorial writers have said in the past, the constraints should tell you something about the intended algorithm for the problem. Something like 15 to 20 should scream out "BRUTE FORCE!" And indeed, this problem was very bruteforceable. What it asked for was the lexicographically earliest answer key that was consistent with the test papers. What many people skimmed over while reading was the stipulation that each question was answered correctly at least once, and the admin room received plenty of "example 2 doesn't work!" messages. But past that, it was a pretty simple loop over all possible answer keys  the maximum number possible was 2^16, or 65536. For each of the possible keys, you just check two things: that the number of right answers by each student is correct, and that the answer to each question appears in some students' answer to that question. KiloManXUsed as: Division One  Level Three:
I just said that 15 to 20 should ring alarm bells, but here's one place that's not going to work. At first glance, it may seem that the best way to do things is a bruteforce search over all N! boss orders, but at N = 15, that's not going to run in time. Instead, there's a massive time reduction that can be made by noticing that at any given point, you will have acquired some number of boss weapons, and that beating boss 1 and then boss 2 is exactly the same in terms of state as beating boss 2 and then boss 1. The only difference, perhaps, is the number of shots it takes. Looking at it in this way, the O(2^n) DP solution is the way to go. Our desired state is the situation where we have all N weapons. To get there, we could have beaten any of those N bosses with any of the other N  1 weapons, or with the KiloBuster, and we will pick the option that requires the least shots. This process is recursive, so we can just recurse until we get to the base case, which is possessing no weapons. There are a whole lot of overlapping subproblems, or subsets of weapons, so we can cache these efficiently using bitmasks. This one turned out to be essentially a textbook demonstration of DP, as evidenced by the extremely high scores on this problem, not to mention the high success rate by those who did submit. zint N; int shots[16][16], health[15]; int best[32768]; int recurse(int x) { if (x == 0) return 0; if (best[x] != 0) return best[x]; int ret = 2000000000; for (int i = 0; i < N; i++) { if ((x & (1 << i)) != 0) { int s = health[i]; for (int j = 0; j < N; j++) { if (i != j && (x & 1 << j) != 0 && shots[j+1][i+1] < s) s = shots[j+1][i+1]; } int q = recurse(x ^ (1 << i)) + s; if (q < ret) ret = q; } } return ret; } int leastShots(vector<string> damageChart, vector<int> bossHealth) { N = damageChart.size() for (int i = 0; i < N; i++) shots[0][i+1] = health[i] = bossHealth[i]; for (int i = 0; i < N; i++) for (int j = 0; j < N; j++) { if (i == j  damageChart[i][j] == '0') shots[i+1][j+1] = 1000000; else shots[i+1][j+1] = (health[j] + damageChart[i][j]  '1') / (damageChart[i][j]  '0'); } return recurse((1 << N)1); } It should be noted that this problem is in fact just the same as the problem of constructing a minimal spanning tree on a directed graph, starting from a specified node, called the root. If I had insisted stubbornly on keeping N = 50, which was the original constraint size when I originally proposed the problem, then DP would also time out and a more sophisticated algorithm would have been necessary. While the undirected MST algorithm can be solved using a greedy algorithm, that doesn't quite work when the edges become directed, as one of the example cases demonstrated. However, various algorithms have been discovered that solve the directed case of the Minimal Spanning Tree problem. More on such algorithms can be found here. Implementations of the ChuLiu/Edmonds Algorithm run in O(E log V) on average and O(V^2) for dense graphs, certainly well in time to solve this problem, although with N = 15, it's overkill. 
