From 0428413b7690547808c47b3c312db74d3246f3a8 Mon Sep 17 00:00:00 2001 From: Laco Papay Date: Wed, 12 Jul 2023 18:02:05 +0000 Subject: [PATCH] Add Distributed Code Jam This includes all problem statements and all available analyses. --- .../2015_finals/analysis_intro.html | 24 + .../2015_finals/kolakoski/analysis.html | 11 + .../2015_finals/kolakoski/statement.html | 61 ++ .../2015_finals/necklace/analysis.html | 7 + .../2015_finals/necklace/statement.html | 45 ++ .../2015_finals/rocks/analysis.html | 145 +++++ .../2015_finals/rocks/statement.html | 35 ++ .../2015_finals/shipping/analysis.html | 522 ++++++++++++++++++ .../2015_finals/shipping/statement.html | 61 ++ .../2015_online/almost_sorted/analysis.html | 15 + .../2015_online/almost_sorted/statement.html | 67 +++ .../2015_online/analysis_intro.html | 49 ++ .../highest_mountain/analysis.html | 320 +++++++++++ .../highest_mountain/mountains.png | Bin 0 -> 2945 bytes .../highest_mountain/statement.html | 81 +++ .../2015_online/johnny/analysis.html | 19 + .../2015_online/johnny/statement.html | 56 ++ .../2015_online/mutexes/analysis.html | 38 ++ .../2015_online/mutexes/statement.html | 97 ++++ .../2015_practice/analysis_intro.html | 27 + .../2015_practice/load_balance/analysis.html | 31 ++ .../2015_practice/load_balance/statement.html | 45 ++ .../2015_practice/majority/analysis.html | 23 + .../2015_practice/majority/statement.html | 42 ++ .../2015_practice/sandwich/analysis.html | 11 + .../2015_practice/sandwich/statement.html | 39 ++ .../2015_practice/shhhh/analysis.html | 15 + .../2015_practice/shhhh/statement.html | 48 ++ .../2016_finals/air_show/analysis.html | 141 +++++ .../2016_finals/air_show/statement.html | 121 ++++ .../2016_finals/analysis_intro.html | 67 +++ .../2016_finals/encoded_sum/analysis.html | 46 ++ .../2016_finals/encoded_sum/statement.html | 93 ++++ .../2016_finals/gold/analysis.html | 70 +++ .../2016_finals/gold/statement.html | 102 ++++ .../toothpick_sculpture/analysis.html | 109 ++++ .../toothpick_sculpture/statement.html | 127 +++++ .../toothpick_sculpture/toothpicks.png | Bin 0 -> 26101 bytes .../2016_r1/analysis_intro.html | 53 ++ .../2016_r1/crates/analysis.html | 128 +++++ .../2016_r1/crates/statement.html | 94 ++++ .../2016_r1/oops/analysis.html | 85 +++ .../2016_r1/oops/statement.html | 159 ++++++ distributed_codejam/2016_r1/rps/analysis.html | 36 ++ .../2016_r1/rps/rps_example.png | Bin 0 -> 8726 bytes .../2016_r1/rps/statement.html | 89 +++ .../2016_r1/winning_move/analysis.html | 75 +++ .../2016_r1/winning_move/statement.html | 58 ++ .../2016_r2/again/analysis.html | 63 +++ .../2016_r2/again/statement.html | 153 +++++ .../2016_r2/analysis_intro.html | 49 ++ .../2016_r2/asteroids/analysis.html | 16 + .../2016_r2/asteroids/statement.html | 166 ++++++ .../2016_r2/gas_stations/analysis.html | 49 ++ .../2016_r2/gas_stations/statement.html | 79 +++ .../2016_r2/lisp_plus_plus/analysis.html | 35 ++ .../2016_r2/lisp_plus_plus/statement.html | 98 ++++ .../2017_finals/analysis_intro.html | 56 ++ .../2017_finals/baby_blocks/analysis.html | 45 ++ .../2017_finals/baby_blocks/statement.html | 75 +++ .../2017_finals/lemming/analysis.html | 82 +++ .../2017_finals/lemming/statement.html | 102 ++++ .../2017_finals/lispp3/analysis.html | 164 ++++++ .../2017_finals/lispp3/statement.html | 111 ++++ .../2017_finals/median/analysis.html | 145 +++++ .../2017_finals/median/statement.html | 145 +++++ .../2017_r1/analysis_intro.html | 50 ++ .../2017_r1/pancakes/analysis.html | 32 ++ .../2017_r1/pancakes/statement.html | 111 ++++ .../2017_r1/query_of_death/analysis.html | 84 +++ .../2017_r1/query_of_death/statement.html | 110 ++++ .../2017_r1/todd_and_steven/analysis.html | 94 ++++ .../2017_r1/todd_and_steven/statement.html | 112 ++++ .../2017_r1/weird_editor/analysis.html | 77 +++ .../2017_r1/weird_editor/statement.html | 83 +++ .../2017_r2/analysis_intro.html | 55 ++ .../2017_r2/broken_memory/analysis.html | 99 ++++ .../2017_r2/broken_memory/statement.html | 92 +++ .../2017_r2/flagpoles/analysis.html | 76 +++ .../2017_r2/flagpoles/statement.html | 82 +++ .../2017_r2/nanobots/analysis.html | 79 +++ .../2017_r2/nanobots/statement.html | 109 ++++ .../2017_r2/number_bases/analysis.html | 94 ++++ .../2017_r2/number_bases/statement.html | 118 ++++ .../2018_finals/analysis_intro.html | 48 ++ .../2018_finals/dodge/analysis.html | 80 +++ .../2018_finals/dodge/statement.html | 141 +++++ .../2018_finals/khazaddum/kd_example2.png | Bin 0 -> 5310 bytes .../2018_finals/khazaddum/solution.cpp | 255 +++++++++ .../2018_finals/khazaddum/statement.html | 130 +++++ .../2018_finals/klingon/solution.cpp | 357 ++++++++++++ .../2018_finals/klingon/statement.html | 116 ++++ .../2018_finals/virus/solution.cpp | 252 +++++++++ .../2018_finals/virus/statement.html | 101 ++++ .../2018_r1/analysis_intro.html | 62 +++ .../2018_r1/kenneth/analysis.html | 141 +++++ .../2018_r1/kenneth/statement.html | 89 +++ .../2018_r1/one_more_time/analysis.html | 89 +++ .../2018_r1/one_more_time/statement.html | 146 +++++ .../2018_r1/stones/analysis.html | 107 ++++ .../2018_r1/stones/statement.html | 70 +++ .../2018_r1/towels/analysis.html | 70 +++ .../2018_r1/towels/statement.html | 93 ++++ 103 files changed, 9124 insertions(+) create mode 100644 distributed_codejam/2015_finals/analysis_intro.html create mode 100644 distributed_codejam/2015_finals/kolakoski/analysis.html create mode 100644 distributed_codejam/2015_finals/kolakoski/statement.html create mode 100644 distributed_codejam/2015_finals/necklace/analysis.html create mode 100644 distributed_codejam/2015_finals/necklace/statement.html create mode 100644 distributed_codejam/2015_finals/rocks/analysis.html create mode 100644 distributed_codejam/2015_finals/rocks/statement.html create mode 100644 distributed_codejam/2015_finals/shipping/analysis.html create mode 100644 distributed_codejam/2015_finals/shipping/statement.html create mode 100644 distributed_codejam/2015_online/almost_sorted/analysis.html create mode 100644 distributed_codejam/2015_online/almost_sorted/statement.html create mode 100644 distributed_codejam/2015_online/analysis_intro.html create mode 100644 distributed_codejam/2015_online/highest_mountain/analysis.html create mode 100644 distributed_codejam/2015_online/highest_mountain/mountains.png create mode 100644 distributed_codejam/2015_online/highest_mountain/statement.html create mode 100644 distributed_codejam/2015_online/johnny/analysis.html create mode 100644 distributed_codejam/2015_online/johnny/statement.html create mode 100644 distributed_codejam/2015_online/mutexes/analysis.html create mode 100644 distributed_codejam/2015_online/mutexes/statement.html create mode 100644 distributed_codejam/2015_practice/analysis_intro.html create mode 100644 distributed_codejam/2015_practice/load_balance/analysis.html create mode 100644 distributed_codejam/2015_practice/load_balance/statement.html create mode 100644 distributed_codejam/2015_practice/majority/analysis.html create mode 100644 distributed_codejam/2015_practice/majority/statement.html create mode 100644 distributed_codejam/2015_practice/sandwich/analysis.html create mode 100644 distributed_codejam/2015_practice/sandwich/statement.html create mode 100644 distributed_codejam/2015_practice/shhhh/analysis.html create mode 100644 distributed_codejam/2015_practice/shhhh/statement.html create mode 100644 distributed_codejam/2016_finals/air_show/analysis.html create mode 100644 distributed_codejam/2016_finals/air_show/statement.html create mode 100644 distributed_codejam/2016_finals/analysis_intro.html create mode 100644 distributed_codejam/2016_finals/encoded_sum/analysis.html create mode 100644 distributed_codejam/2016_finals/encoded_sum/statement.html create mode 100644 distributed_codejam/2016_finals/gold/analysis.html create mode 100644 distributed_codejam/2016_finals/gold/statement.html create mode 100644 distributed_codejam/2016_finals/toothpick_sculpture/analysis.html create mode 100644 distributed_codejam/2016_finals/toothpick_sculpture/statement.html create mode 100644 distributed_codejam/2016_finals/toothpick_sculpture/toothpicks.png create mode 100644 distributed_codejam/2016_r1/analysis_intro.html create mode 100644 distributed_codejam/2016_r1/crates/analysis.html create mode 100644 distributed_codejam/2016_r1/crates/statement.html create mode 100644 distributed_codejam/2016_r1/oops/analysis.html create mode 100644 distributed_codejam/2016_r1/oops/statement.html create mode 100644 distributed_codejam/2016_r1/rps/analysis.html create mode 100644 distributed_codejam/2016_r1/rps/rps_example.png create mode 100644 distributed_codejam/2016_r1/rps/statement.html create mode 100644 distributed_codejam/2016_r1/winning_move/analysis.html create mode 100644 distributed_codejam/2016_r1/winning_move/statement.html create mode 100644 distributed_codejam/2016_r2/again/analysis.html create mode 100644 distributed_codejam/2016_r2/again/statement.html create mode 100644 distributed_codejam/2016_r2/analysis_intro.html create mode 100644 distributed_codejam/2016_r2/asteroids/analysis.html create mode 100644 distributed_codejam/2016_r2/asteroids/statement.html create mode 100644 distributed_codejam/2016_r2/gas_stations/analysis.html create mode 100644 distributed_codejam/2016_r2/gas_stations/statement.html create mode 100644 distributed_codejam/2016_r2/lisp_plus_plus/analysis.html create mode 100644 distributed_codejam/2016_r2/lisp_plus_plus/statement.html create mode 100644 distributed_codejam/2017_finals/analysis_intro.html create mode 100644 distributed_codejam/2017_finals/baby_blocks/analysis.html create mode 100644 distributed_codejam/2017_finals/baby_blocks/statement.html create mode 100644 distributed_codejam/2017_finals/lemming/analysis.html create mode 100644 distributed_codejam/2017_finals/lemming/statement.html create mode 100644 distributed_codejam/2017_finals/lispp3/analysis.html create mode 100644 distributed_codejam/2017_finals/lispp3/statement.html create mode 100644 distributed_codejam/2017_finals/median/analysis.html create mode 100644 distributed_codejam/2017_finals/median/statement.html create mode 100644 distributed_codejam/2017_r1/analysis_intro.html create mode 100644 distributed_codejam/2017_r1/pancakes/analysis.html create mode 100644 distributed_codejam/2017_r1/pancakes/statement.html create mode 100644 distributed_codejam/2017_r1/query_of_death/analysis.html create mode 100644 distributed_codejam/2017_r1/query_of_death/statement.html create mode 100644 distributed_codejam/2017_r1/todd_and_steven/analysis.html create mode 100644 distributed_codejam/2017_r1/todd_and_steven/statement.html create mode 100644 distributed_codejam/2017_r1/weird_editor/analysis.html create mode 100644 distributed_codejam/2017_r1/weird_editor/statement.html create mode 100644 distributed_codejam/2017_r2/analysis_intro.html create mode 100644 distributed_codejam/2017_r2/broken_memory/analysis.html create mode 100644 distributed_codejam/2017_r2/broken_memory/statement.html create mode 100644 distributed_codejam/2017_r2/flagpoles/analysis.html create mode 100644 distributed_codejam/2017_r2/flagpoles/statement.html create mode 100644 distributed_codejam/2017_r2/nanobots/analysis.html create mode 100644 distributed_codejam/2017_r2/nanobots/statement.html create mode 100644 distributed_codejam/2017_r2/number_bases/analysis.html create mode 100644 distributed_codejam/2017_r2/number_bases/statement.html create mode 100644 distributed_codejam/2018_finals/analysis_intro.html create mode 100644 distributed_codejam/2018_finals/dodge/analysis.html create mode 100644 distributed_codejam/2018_finals/dodge/statement.html create mode 100644 distributed_codejam/2018_finals/khazaddum/kd_example2.png create mode 100644 distributed_codejam/2018_finals/khazaddum/solution.cpp create mode 100644 distributed_codejam/2018_finals/khazaddum/statement.html create mode 100644 distributed_codejam/2018_finals/klingon/solution.cpp create mode 100644 distributed_codejam/2018_finals/klingon/statement.html create mode 100644 distributed_codejam/2018_finals/virus/solution.cpp create mode 100644 distributed_codejam/2018_finals/virus/statement.html create mode 100644 distributed_codejam/2018_r1/analysis_intro.html create mode 100644 distributed_codejam/2018_r1/kenneth/analysis.html create mode 100644 distributed_codejam/2018_r1/kenneth/statement.html create mode 100644 distributed_codejam/2018_r1/one_more_time/analysis.html create mode 100644 distributed_codejam/2018_r1/one_more_time/statement.html create mode 100644 distributed_codejam/2018_r1/stones/analysis.html create mode 100644 distributed_codejam/2018_r1/stones/statement.html create mode 100644 distributed_codejam/2018_r1/towels/analysis.html create mode 100644 distributed_codejam/2018_r1/towels/statement.html diff --git a/distributed_codejam/2015_finals/analysis_intro.html b/distributed_codejam/2015_finals/analysis_intro.html new file mode 100644 index 00000000..0ada22a3 --- /dev/null +++ b/distributed_codejam/2015_finals/analysis_intro.html @@ -0,0 +1,24 @@ +

bmerry is the first ever Distributed Code Jam Champion!

+ +

The first ever Distributed Code Jam finals are over, and they were a pretty exciting event! With ten participants fighting for the title, we knew the race was going to be pretty intense. The contest was opened by bmerry submitting a solution to the small input for Necklace, followed closely by MiSawa submitting a solution to the small input for Kolakoski, both before the half-hour mark. At this point, the solutions started pouring in - after an hour, all but one contestants had at least one small input to their name, and half of them also having a submission to the large input of Necklace.

+ +

The submissions to Necklace large and Kolakoski small continued streaming in. The next breakthrough came at one hour and 40 minutes, when Shik took the lead with a submission to Kolakoski large. He didn't hold it for long, though, as just four minutes later bmerry submitted a solution to Shipping small, followed almost immediately by a submission to Shipping large, netting him a huge advantage over the rest of the contestants.

+ +

The other contestants had no way of knowing that while bmerry's small submission was fully correct, the large submission was just a bluff - and a very successful one, as it seems to have focused others on the Shipping problem, instead of the significantly easier Rocks. The solutions to the two easier problems continued streaming in (in particular bmerry strengthened his lead by submitting a solution to Kolakoski large a bit before three hours were up), as did incorrect submissions to Shipping-small.

+ +

The two contestants to attack the rocks problem were MiSawa, with a small submission three and a half hours into the contest, but without an attempt on the 53 points to be gained from the large, and bmerry, who submitted the small around two and a half hours in, and tried a slightly too slow submission for the large just before the end of the contest. Meanwhile, most contestants were attacking Shipping, with Marcin.Smulewicz being the only successful one - his successful submission for the small input gave him, in the end, the second place, after bmerry. Shik came in third, with correct large submissions for Kolakoski and Necklace, followed by MiSawa (who had Necklace large, Kolakoski small and Rocks small) and ZbanIlya with Necklace large and Kolakoski small).

+ +

Congratulations to the medalists and all the finalists, and hope to see you next year!

+ +
+Cast
+ +Problem B. Kolakoski. Written by David Spies and Onufry Wojtaszczyk, prepared by David Spies and Onufry Wojtaszczyk.
+ +Problem C. Necklace. Written by John Dethridge, Chieu Nguyen and Onufry Wojtaszczyk, prepared by Joachim Bartosik, Tomek Kulczyński and Onufry Wojtaszczyk.
+ +Problem D. Rocks. Written by Onufry Wojtaszczyk and Chieu Nguyen, prepared by Onufry Wojtaszczyk.
+ +Problem E. Shipping. Written by John Dethridge and Chieu Nguyen, prepared by Onufry Wojtaszczyk.
+ +Platform development and support: Onufry Wojtaszczyk, Andi Purice, Maciek Klimek, Jarek Przybyłowicz, Joachim Bartosik, Bartek Janiak, David Spies, Neo Liu and many others.
\ No newline at end of file diff --git a/distributed_codejam/2015_finals/kolakoski/analysis.html b/distributed_codejam/2015_finals/kolakoski/analysis.html new file mode 100644 index 00000000..ef518c91 --- /dev/null +++ b/distributed_codejam/2015_finals/kolakoski/analysis.html @@ -0,0 +1,11 @@ +

There are three, very different, potential solutions to this problem, and we will go over all of them.

+ +

Let's begin with the naive, single node solution (which doesn't run in time even for the small input). We can construct the Kolakoski sequence as we go, in an array, keeping two pointers into the array - the last constructed element, and the element that was used to describe the group containing the last constructed element. For instance, if we constructed the sequence 1,2,2,1,1,2,1,2,2 so far, then the last constructed element is the 9th (1-indexed), which is a 2, and the element that describes it is the 6th (which is also a 2, and describes the "2,2" run at places 8 and 9). So, we can construct the whole sequence up to GetIndex() this way, and then multiply each element by the appropriate GetMultiplier(). Since the input can go up to 3 billion, and we have just 700MB of memory, we need to store the sequence efficiently - so store one value per bit. Again, as noted, this will not run in time even for the small input. One thing we should remember from this is that to calculate the next elements of the sequence all we need is the elements that describe it, and the information what was the last element before our elements.

+ +

One way to speed it up is to use a bit-calculation trick of some sort, to calculate more than one value of the sequence in a single operation. For instance, we can precalculate for each possible sequence of 20 elements and each possible previous element (221 choices in all) what will they describe (as a bitmask), and use this to consume 20 elements of the describing sequence at a time. This will speed up the calculation of the sequence roughly 10x, so we will be able to compute the Kolakoski sequence up to 3 billion on one node. We still need to call GetMultiplier 3 billion times, which is too many to do on a single node, but this can be trivially distributed: we have each of the 100 nodes calculate the whole sequence, and then each node actually does the dot-product for only its own shard of the sequence.

+ +

A different approach is to try and shard the "expanding". Imagine we have a part of the sequence in hand in our node. This describes a later part of the sequence. This later part describes in turn an even later part, and so on. If we have each node calculate, say, up to 107 first elements of the sequence, and then we take the suffix of those 107 that describe later elements; and we shard this suffix into 100 parts, we can have each node do its own expanding. There are two pieces of information we need to effectively expand such a part of the sequence - what the first element of the expanded sequence should be (1 or 2), and what the index of the first element in the whole sequence is (so we know what to multiply by).

+ +

One way to get this data is to precalculate and hardcode. We can write code on our machine that actually calculates these numbers for each node and for each expansion in a few minutes; and hardcode the values into our solution. Having those, we can easily expand the sequence on each node as needed. Other similar hardcoding-based approaches are also possible, based on the intuition that only log(N) state is needed to expand the sequence up to the Nth element.

+ +

The last possible approach is the most "distributed" one. Note that we can expand a sequence without knowing the two bits of information - we will just expand it into a sequence of digits, but we will not know which digit is a "1" and which one is a "2". So, we can have all the nodes do a single expansion in parallel. Once they're done, we can do a message-passing phase, where, starting from the first node, each node receives information about what the first element in its sequence and its index are, in constant time calculates the first element and the index of the next node's sequence, and passes that along. After that is done, each node can calculate the dot product and do another expansion (without knowing the first element and offset) in parallel.

\ No newline at end of file diff --git a/distributed_codejam/2015_finals/kolakoski/statement.html b/distributed_codejam/2015_finals/kolakoski/statement.html new file mode 100644 index 00000000..0afad4ac --- /dev/null +++ b/distributed_codejam/2015_finals/kolakoski/statement.html @@ -0,0 +1,61 @@ +

Problem

+

+The Kolakoski sequence is defined as follows, where A(i) is the i-th term in the sequence: +

+This completely and uniquely defines the sequence. +

+

+The first twenty terms of the sequence are as follows, where the lines mark the alternating runs of 1's and 2's:

+1 2 2 1 1 2 1 2 2 1 2 2 1 1 2 1 1 2 2 1
+_ ___ ___ _ _ ___ _ ___ ___ _ ___ ___ _
+1  2   2  1 1  2  1  2   2  1  2   2  1

+By collecting the lengths of each run, we obtain the same sequence again. +

+

+You become mystified contemplating the elegance of the Kolakoski sequence and after staring at its 1's and 2's for far too long, you begin to wonder if maybe you should spice it up a little and introduce some more numerical variety to the terms. +

+ +

So you decide to assign an arbitrary coefficient to each index in a manner such as the following:

+C(0)=1
+C(1)=3
+C(2)=1
+C(3)=5
+C(4)=2
+C(5)=2
+
+By multiplying the first 6 terms each by their coefficient and summing, we get

+1*1 + 3*2 + 1*2 + 5*1 + 2*1 + 2*2 = 20. +

+ +

+Given a mapping from index to coefficient, find the dot product of the first N terms of the Kolakoski sequence and their respective coefficients. +

+ +

Input

+The library "kolakoski" will contain two functions: + +A single call to GetMultiplier will take approximately 0.005 microseconds. + +

Output

+Output one number: the weighted sum of the elements of the Kolakoski sequence. + +

Limits

+Each node will have access to 700MB of RAM.
+Your solution will run on 100 nodes in both inputs.
+ +

Small input

+GetMultiplier(i) will always return 1, for all the inputs.
+1 ≤ GetIndex() ≤ 109
+Each node will have a time limit of 10 seconds.
+ + +

Large input

+1 ≤ GetMultiplier(i) ≤ 50 for all i
+1 ≤ GetIndex() ≤ 3 × 109
+Each node will have a time limit of 12 seconds.
diff --git a/distributed_codejam/2015_finals/necklace/analysis.html b/distributed_codejam/2015_finals/necklace/analysis.html new file mode 100644 index 00000000..c75bc9d8 --- /dev/null +++ b/distributed_codejam/2015_finals/necklace/analysis.html @@ -0,0 +1,7 @@ +

This problem has the simplest sharding model of all the finals problems. We simply assign a piece of the necklace to each node, and calculate a sub-answer for this piece of the necklace in each node, then merge them together.

+ +

The sub-problem we solve for each piece of the necklace is: for each position in the message string, calculate the longest substring of the message beginning at this position that is a subsequence of our part of the necklace. This is O(|message|) information we need to ship out of each node to some master. Once we have this information from each node, calculating the final answer is easy - for each possible starting position in the message string, we check how long a substring beginning at this position can is a subsequence of the whole string - first cover as much as possible by the first node, then (starting from where the first node finished) cover as much as possible by the second node, and so on.

+ +

How do we solve the sub-problems on a single node? For the small input, we can do a DP, where the state DP[position in necklace][position in message] is "how much of the message have we already covered up to this point". The runtime is O(|message| |necklace| / NumberOfNodes()), with a pretty trivial extension rule. This is enough to solve the small input.

+ +

To solve the large input, we can't afford to touch all |necklace| positions for each character in the message. One way to avoid that is to reorganize the necklace - for each charcter, store the ordered list of positions on which the character appears. This takes O(|necklace| / NumberOfNodes()) to build. Then, for each starting position in the message, we can greedily append characters. If the last character we appended was at position, say, X, and the next character to append is some c, then we can binary search for the first occurrence of c after X. Doing all these binary searches will run in O(|message|2 log |necklace|) time, which will be fast enough.

\ No newline at end of file diff --git a/distributed_codejam/2015_finals/necklace/statement.html b/distributed_codejam/2015_finals/necklace/statement.html new file mode 100644 index 00000000..5a2c9412 --- /dev/null +++ b/distributed_codejam/2015_finals/necklace/statement.html @@ -0,0 +1,45 @@ +

Problem

+

+You've come up with the coolest idea ever for a new fashion trend: customizable necklaces made out of strings with beads that display letters and other characters! The beads appear only on the front of the necklace and read only in one direction, so the string of characters is not circular and irreversible. By itself, this is not really a new idea. The awesome new feature you have in mind is to add a button that lights up some of the beads so that they display a secret message consisting of characters that form a subsequence of the main string of characters. This will have so many applications... just think of the possibilities! And it's so shiny! People are going to love it! Everyone will want their own! +

+ +

+So you announce this product, allowing people to place orders for necklaces by specifying the string of characters to be displayed on the necklace as well as the secret message to be lit up when they press the button. The orders come pouring in! Your idea is even more popular than you expected! How exciting! +

+ +

+Unfortunately, after examining a few orders, you realize that you forgot to check the crucial constraint that the secret message has to be a subsequence of the main necklace string. Without that, the secret message can't always be lit up entirely. +

+ +

+You don't want to disappoint your customers by just telling them that it is impossible to light up their secret messages in the chosen necklace strings. So you decide to offer them an alternative message by finding a substring of their secret message that forms a subsequence of their necklace string, in case they would be satisfied with this shorter version. You want to maximize the length of such a substring. +

+ +

+Given a necklace string N and a secret message string M, find the maximum length of a substring of M that is also a subsequence of N. +

+ + +

Input

+The input library is called "necklace"; see the sample inputs below for examples in your language. It defines four methods: + + +A single call of GetNecklaceElement or GetMessageElement will take up to 0.02 microseconds. + +

Output

+Output one integer - the maximum length of a substring of Message that is also a subsequence of Necklace. + +

Limits

+0 ≤ GetNecklaceElement(i), GetMessageElement(i) ≤ 10,000
+1 ≤ GetNecklaceLength() ≤ 109
+Each node will have access to 256MB of RAM and a time limit of 5 seconds.
+Your solution will run on 100 nodes (both for the small and the large input).
+ +

Small input

+1 ≤ GetMessageLength() ≤ 100
+ +

Large input

+1 ≤ GetMessageLength() ≤ 3000
diff --git a/distributed_codejam/2015_finals/rocks/analysis.html b/distributed_codejam/2015_finals/rocks/analysis.html new file mode 100644 index 00000000..fd8d25bc --- /dev/null +++ b/distributed_codejam/2015_finals/rocks/analysis.html @@ -0,0 +1,145 @@ +

Imagine we entered some cell by going, say, up. Then there is some number of rocks we pushed in front of us. The first observation is that if we know this number, the state of the rest of the board is irrelevant. This is because we will never go down or left, and we had no way of impacting anything that's to the right of us, and in our row or higher.

+ +

The second observation, rising from the first, is that if we're to reach a given cell (x, y), going from the right, it's always better to have gone to the right as little as possible (because then we have fewer rocks in front of us).

+ +

This gives rise to a single-node DP solution in O(N^3). We start in (0, 0), and have pushed 0 rocks in front of us up, and 0 rocks to the right. For each cell, and each direction in (right, up), we store whether the field is reachable from this direction, and if so, what is the minimum number of rocks we're pushing in front of us (or equivalently, from which field in the same row/column we're pushing rocks). When processing a given point, if it is reachable from, say, down, from y', we mark (x+1, y) as reachable from the left from x (unless there are K+1 rocks in a row in front of us), and we mark (x, y+1) as reachable from y', unless there are X rocks in (y', y] and K-X+1 rocks in a row starting from y'.

+ +

The improvement to O(N^2) is to precalculate, for each cell, how far can we push from a given position. This is easy to do, iterating with two pointers (one being K rocks away from the other). If we have this, we can check whether we can move forward from (x, y) in constant time.

+ +

The first thing to notice (or rather, to learn from the Mutexes problem in the online round) is that an O(N^2) DP can be sharded. Each node is responsible for an (N x N/NumNodes()) strip of the board. You calculate in N/NumNodes() x N/NumNodes() chunks, and once you have the result for a given chunk, you send the values from the edge to the node in front of you, so it can start calculating. So, effectively, you proceed in "diagonals", and in 2NumNodes() steps of size O(N/NumNodes() * N/NumNodes()) you will have calculated everything.

+ +

To shard the DP, we will phrase the information in each state as "how many rocks from below (or from the cell we're standing on) are we pushing in front of us already?", and we need to precalculate, for each cell, "what is the maximum number of rocks in front of us with which we'll be able to take one step forward". The number "how many rocks from below are we pushing" is easy to calculate when moving forward - it's the number from the previous field, plus one if there's a rock on the field we're moving on.

+ +

So, first, we precalculate "max number of rocks with which we can take a step forward". Let K' be equal to min(K + 1, distance from the wall). Then if there are X rocks in the next K' fields, the number we're looking for is K' - X - 1. Now, this number can be calculated through a DP going down: from the node above, we pass in the number of rocks X in the next K' fields, and to update this number going one field down to row x, we will (possibly) need to read whether there is a rock in x + K + 1 (in order to remove it from the sum, if it exists). So, we'll perform a total of O(N x N/M) extra reads, which is fine.

+ +

Since no-one managed to solve this problem during the contest, we provide a sample solution for reference:

+
+#include <cstdio>
+#include <utility>
+#include <algorithm>
+
+#include "rocks.h"  // NOLINT
+#include "message.h"  // NOLINT
+
+using namespace std;  // NOLINT
+
+// We're going to shard vertically. That is, each node gets a range of y values
+// it is responsible for.
+#define MAXN 10000
+// can_carry_up[i][j] == X means that it is possible to enter (i, j) from below
+// when pushing up to X rocks from rows up to and including j.
+// This is equivalent to the fact that the next K squares above
+// (i, j) contain exactly X empty spaces.
+int can_carry_up[MAXN][MAXN / 10];
+int can_carry_rt[MAXN][MAXN / 10];
+// need_carry_up[i][j] == X means that to enter (i, j) from below, we need to be
+// pushing at least X rocks. If need_carry_up[i][j] + can_carry_up[i][j] > K,
+// the field can't be entered from below.
+int need_carry_up[MAXN][MAXN / 10];
+int need_carry_rt[MAXN][MAXN / 10];
+// A cache of the input.
+bool rock[MAXN][MAXN / 10];
+
+int pass_through[MAXN / 10];
+
+int beg_y;
+int end_y;
+int NoN;
+
+int main() {
+  int N = GetN();
+  int K = GetK();
+  if (MyNodeId() == 0) fprintf(stderr, "N = %d, K = %d\n", N, K);
+  // Let's ensure that every range we deal with has at least one element.
+  int NoN = NumberOfNodes();
+  if (NoN > N) NoN = N;
+  if (MyNodeId() >= NoN) return 0;
+
+  beg_y = (MyNodeId() * N) / NoN;
+  end_y = ((MyNodeId() + 1) * N) / NoN;
+  int ry = end_y - beg_y;
+
+  for (int x = 0; x < N; ++x) for (int y = 0; y <= ry; ++y) {
+    rock[x][y] = (y + beg_y < N) ? IsRock(x, y + beg_y) : true;
+    need_carry_up[x][y] = need_carry_rt[x][y] = K + 1;
+  }
+  if (MyNodeId() == 0) need_carry_up[0][0] = need_carry_rt[0][0] = 0;
+
+  // Calculating "can_carry_rt" is fully local to our rows, so let's calculate
+  // it.
+  for (int y = ry - 1; y >= 0; --y) {
+    int empty = 0;
+    for (int x = N - 1; x >= 0; --x) {
+      can_carry_rt[x][y] = empty;
+      if (!rock[x][y]) empty += 1;
+      if (x + K < N && !rock[x + K][y]) empty -= 1;
+    }
+  }
+
+  // Calculating "can_carry_up" is more tricky. We need "rock" values from
+  // outside our range, and we need the DP values from the range above us. So,
+  // we'll do a standard 2D DP.
+  for (int block = 0; block < NoN; ++block) {
+    if (MyNodeId() != NoN - 1) {
+      Receive(MyNodeId() + 1);
+    }
+    int beg_x = (block * N) / NoN;
+    int end_x = ((block + 1) * N) / NoN;
+    for (int x = beg_x; x < end_x; ++x) {
+      int empty = (MyNodeId() == NoN - 1) ? 0 : GetInt(MyNodeId() + 1);
+      for (int y = ry - 1; y >= 0; --y) {
+        can_carry_up[x][y] = empty;
+        if (!rock[x][y]) empty += 1;
+        if (y + beg_y + K < N && !IsRock(x, y + beg_y + K)) empty -= 1;
+      }
+      if (MyNodeId() != 0) PutInt(MyNodeId() - 1, empty);
+    }
+    if (MyNodeId() != 0) {
+      Send(MyNodeId() - 1);
+    }
+  }
+  // Now each node has "can_carry_*" calculated for it's range. We run a
+  // standard 2D DP (in the other direction) to calculate "need_carry_*".
+  for (int block = 0; block < NoN; ++block) {
+    if (MyNodeId() != 0) {
+      Receive(MyNodeId() - 1);
+    }
+    int beg_x = (block * N) / NoN;
+    int end_x = ((block + 1) * N) / NoN;
+    for (int x = beg_x; x < end_x; ++x) {
+      // need_carry_up[x][0] will always come from below.
+      if (MyNodeId() != 0) {
+        need_carry_up[x][0] = GetInt(MyNodeId() - 1);
+      }
+      for (int y = 0; y < ry; ++y) {
+        if (need_carry_up[x][y] <= can_carry_up[x][y]) {
+          if (x + 1 < N) need_carry_rt[x+1][y] = rock[x+1][y] ? 1 : 0;
+          if (y + beg_y + 1 < N && need_carry_up[x][y + 1] == K + 1) {
+            need_carry_up[x][y + 1] =
+                need_carry_up[x][y] + (rock[x][y + 1] ? 1 : 0);
+          }
+        }
+        if (need_carry_rt[x][y] <= can_carry_rt[x][y]) {
+          if (y + beg_y + 1 < N) need_carry_up[x][y+1] = rock[x][y+1] ? 1 : 0;
+          if (x + 1 < N && need_carry_rt[x+1][y] == K + 1) {
+            need_carry_rt[x + 1][y] =
+                need_carry_rt[x][y] + (rock[x + 1][y] ? 1 : 0);
+          }
+        }
+        if (y + 1 == ry && MyNodeId() != NoN - 1) {
+          PutInt(MyNodeId() + 1, need_carry_up[x][ry]);
+        }
+      }
+    }
+    if (MyNodeId() != NoN - 1) {
+      Send(MyNodeId() + 1);
+    }
+  }
+  if (MyNodeId() == NoN - 1) {
+    bool win =
+      need_carry_up[N - 1][ry - 1] == 0 || need_carry_rt[N - 1][ry - 1] == 0;
+    printf("%s\n", win ? "YES" : "NO");
+  }
+  return 0;
+}
+
\ No newline at end of file diff --git a/distributed_codejam/2015_finals/rocks/statement.html b/distributed_codejam/2015_finals/rocks/statement.html new file mode 100644 index 00000000..63d94ed6 --- /dev/null +++ b/distributed_codejam/2015_finals/rocks/statement.html @@ -0,0 +1,35 @@ +

Problem

+

You own an almond farm in a region which for the last few years has been experiencing an extreme, record-breaking drought. Reservoirs are drying up, mandatory water rationing is in effect, and you are facing pressure to do something about this farm of yours that is consuming 4 liters of water per almond.

+ +

As you survey the regular grid-like arrangement of your farm, a brilliant idea strikes you. You should turn the land into a giant board where people can play life-sized versions of their favorite grid-based board games! So you take out all the almond trees and turn your farm into an amusement park by sectioning your land into a square N-by-N grid.

+ +

Unfortunately, the region you are in is also known for its earthquakes, and soon after you convert your land, an earthquake strikes, triggering a landslide that drops giant rocks onto your grid, each rock conveniently occupying the space of exactly one of the cells. No cell contains more than one rock.

+ +

You are now trapped in the bottom left corner (the cell with coordinates (0, 0)), and you need to get to the exit at the top right corner (the cell with coordinates (N-1, N-1)). You can move only up or to the right 1 cell at a time and cannot squeeze past any rocks or climb over them, so if you want to move into a cell that is occupied by a rock, you have to push it into next cell in the same direction as you are moving. If there is already a rock in that cell, it will also get pushed in the same direction, and so on, until finally there is a square without rocks. After every push, each rock occupies exactly one cell. You can push up to K rocks at the same time in this manner, but you cannot push rocks off the grid. Is it possible to reach the exit?

+ + +

Input

+

+The input library is called "rocks"; see the sample inputs below for examples in your language. It defines three methods: +

+A single call to IsRock() will take approximately 0.05 microseconds. +
+ +

Output

+Output one line containing the word "YES" if it is possible to reach the exit, or the word "NO" if it is impossible. + +

Limits

+Each node will have access to 600MB of RAM, and a time limit of 4 seconds.
+IsRock(0, 0) and IsRock(GetN() - 1, GetN() - 1) will return false.
+0 ≤ GetK() ≤ GetN()
+Your solution will run on 100 nodes in both inputs.
+ +

Small input

+2 ≤ GetN() ≤ 2,000
+ +

Large input

+2 ≤ GetN() ≤ 10,000
diff --git a/distributed_codejam/2015_finals/shipping/analysis.html b/distributed_codejam/2015_finals/shipping/analysis.html new file mode 100644 index 00000000..01d79097 --- /dev/null +++ b/distributed_codejam/2015_finals/shipping/analysis.html @@ -0,0 +1,522 @@ +

In the small input, we can take advantage of the fact that the villages downstream have smaller indices to shard the input simply by village index. Each node can read a range of villages into memory and store the appropriate part of the tree (including color information). Then, processing a single query begins on node holding the most upstream villages.

+ +

A query consists of two branches, both going downstream, until they meet. So, each node will pass to the next node either the information that the branches already met, and no more processing needs to be done, or the state of each of the two branches. The state contains the following information: what village are we currently at, what is the longest consecutive sequence of same-faction villages we passed, and how long is the current string of consecutive same-faction villages. When getting a query, each node can easily extend it by traversing the tree upwards. This will work fast enough for the small number of queries we have.

+ +

For the large input, we will need to shard the tree of villages differently. We'll need to do something similar to the Shhhh problem in the practice online round - randomly pick, say, 10^5 key villages (including all query endpoints), and for each of those calculate what is the next key village downstream (or, if we reach the ocean, remember that there's no key village downstream). Additionally, we will want to know precalculated information about the part of the river between the two key villages - what is the longest single-faction string in it, how long is the single-faction prefix and single-faction suffix of the part of the river, and are all the villages on this part of the river controlled by the same faction.

+ +

A similar probability calculation as we did for Shhhh leads to the following conclusions: a) it's better to dynamically assign the key villages to nodes (so that we don't get one node getting only long paths to traverse), and b) it's highly unlikely, if we do dynamic assignment, that a single node will get significantly more than O(NumberOfVillages / NumberOfNodes) nodes to traverse.

+ +

Once we have all this information on a master node, we can precalculate most of the answer for a single query by traversing the tree of key villages. Since query endpoints are also key villages, we can go up the tree from them till we reach a common key village. Then, we should take the two villages that were the previous villages on each branch, and go up the real tree of villages until the paths meet (since the first common key village might be downstream from the first common village).

+ +

Doing a direct traversal of the tree of key villages for each query will be too slow - O(|key villages| x |queries|), we can speed it up by caching shortcuts in the tree of key villages. For instance, for each key village, we can cache the path (and all the information we store about a path) to the key village downstream, key village two away, key village four away, etc. - with this information we can pre-process all the queries in O(|queries| x log |key villages|) time.

+ +

Once a query is pre-processed, we need to calculate the final answer by traversing, on average, O(NumberOfVillages / |key villages|) nodes up the tree for a single query. We can distribute that among nodes, each node getting O(NumberOfShipments / NumberOfNodes) queries to process. Processing a query is simply going up the tree with both branches until they meet.

+ +

One more implementational note is that we will also store the depth of each node in both trees (the input tree of villages, and the tree of key villages), to allow us to know, when we have two branches, which branch should we move up to make them meet.

+ +

Since no-one managed to solve the large input during the contest, we provide the code of the sample solution for reference.

+ +
+#include "shipping.h"
+#include "message.h"
+
+using namespace std;
+
+// The overall design of the solution is as follows:
+// 1) First, observe that if for a number of paths we have precalculated the
+// values "length-of-same-color-prefix", "length-of-same-color-suffix",
+// "max-internal-same-color-sequence-length" and "is-all-of-same-color", we can
+// easily glue such precalculated data together. The solution, on the highest
+// level, will be based on such precalculations. We will also calculate the
+// length of each such path.
+// 2) In the whole tree, we mark a certain set of key vertices. We intend to
+// precalculate the paths between key vertices. The set of key vertices will
+// contain all the query endpoints, and a number of other random enough points.
+// Each node will calculate this set independently (but consistently).
+// Calculation is O(K log K).
+// 3) We will then do the precalculation. There are two ways of doing it -
+// first, we can statically assign key vertex ranges to worker nodes; second, we
+// can calculate dynamically, each node signing in with a master when it
+// finishes calculating a block of key vertices to get a new block. We implement
+// both, I believe the dynamic version to be significantly faster. The
+// precalculation is pretty trivial going up the tree; we check (in log-time)
+// whether we reached another key vertex at each step. In the dynamic version,
+// the load will spread equally if we have at least log N blocks per node, in
+// the static version there will be an extra log N factor.
+// This is O(N / M) reads, O(N log N / M) operations (the long pole is checking
+// whether it's a known vertex), and O(M log N) messages sent to/from the
+// master.
+// 4) The master obtains all the precalculated values, and effectively
+// constructs a tree with edges being the precalculated paths. This tree can
+// still be a pretty long path, so we do log-compression on the paths in the
+// tree (that is, for each node, we store precomputed paths to ancestors of
+// levels 1, 2, 4, 8, etc.). This is O(K log K), where K is the number of key
+// vertices, in terms of CPU and memory. We could skip this step (and avoid
+// paying the K log K), but then the next step is O(K Q) on the master.
+// 5) Then, the master tackles the queries one-by-one. For each query, the
+// master first finds the least-common-ancestor of the endpoints. If it happens
+// to be one of the query vertices, the answer can be fully computed locally
+// (remember all query vertices are key vertices). Otherwise, we go one step
+// down (find the last two key vertices on the two paths to the common
+// ancestor). This step is O(Q log K) on the master.
+// 6) We have, on average, N / K nodes between these two last vertices and the
+// real LCA. We ship the pair (precalculated path from query start to last key
+// vertex, precalculated path from query end to last key vertex) to one of the
+// workers, and move on to the next query. The worker (in the precalculated
+// paths) gets the depths to the nearest key ancestor, so it can go up the tree
+// with the deeper vertex first, and then with both, step by step, to find the
+// real LCA. This will be N / K reads on average, so it we will do O(N Q / K M)
+// reads for all the queries. To avoid unnecessary network traffic, we can batch
+// the requests in larger groups (so we send a total of ~10 batches of requests
+// to each worker node).
+//
+// The total costs (assuming K is at least M log N).
+// - CPU on the master: O(K log K + Q log K)
+// - CPU on the worker: O(K log K + N log N / M + N Q / K M)
+// - Reads on the worker: O(N / M + N Q / K M)
+// - Messages: roughly 2 log N messages between master and each node.
+//
+// It seems optimal to push K up to Q, and there doesn't seem to be a real gain
+// from going much higher.
+//
+// The protocols:
+// A) For part 1 (processing the tree):
+// Master request:
+// [ Char(0), Int(num of key verts to process), Int(index of the first one) ]
+// Worker response:
+// [ Int(number of key verts processed), Int(index of the first one) [...] ]
+// and then #key_verts_processed blocks of the form:
+// [ Int(nearest key ancestor, or -1 if none), Int(distance to the ancestor),
+//   Int(length of the same-color prefix), Int(length of same-color suffix),
+//   Int(length of the longest same-color internal path),
+//   Char(0 if there are multiple colors inside, 1 the path is one color) ]
+// If the first int is -1, the other fields are still there, and carry the
+// semantics of the path to the root of the whole tree.
+//
+// B) For part 2 (answering queries):
+// Master request:
+// [ Char(1), Int(num of queries to process), Int(index of the first query)
+//   [...] ]
+// and then #queries_to_process block of the form:
+// [ Int(first_key_vertex), Int(longest same-color path up to first key),
+//   Int(length of same-color suffix up to first-key),
+//   Int(depth of first key in the real tree)
+//   + the same four entries for the second key ]
+// Worker response:
+// [ Int(num queries processed), Int(index of the first one), [...] ]
+// and then #queries_processed entries of Int(answer for the query).
+//
+// C) A Master request [ Char(2) ] means "Terminate", no response is required.
+
+#define KEY_VERTS 100000
+#define BATCH_COUNT 4
+#define MAX_Q 20000
+
+int N;
+int Q;
+int NoN;
+int key_vertices[KEY_VERTS + 10];
+int NKV;  // Number of key vertices.
+
+// Prepare the list of "key" vertices
+void ListCrucialVertices() {
+  set<int> kverts;
+  if (N <= KEY_VERTS) {
+    for (int i = 0; i < N; ++i) {
+      key_vertices[i] = i;
+      NKV = N;
+    }
+    key_vertices[NKV] = N + 10;
+    return;
+  }
+  for (int i = 0; i < Q; ++i) {
+    kverts.insert(GetShipmentSource(i));
+    kverts.insert(GetShipmentDestination(i));
+  }
+  int candidate = 0;
+  while (kverts.size() < KEY_VERTS) {
+    candidate += 1500450271;  // Just a random prime.
+    candidate %= N;
+    kverts.insert(candidate);
+  }
+  NKV = 0;
+  for (auto i : kverts) {
+    key_vertices[NKV++] = i;
+  }
+  key_vertices[NKV] = N + 10;
+}
+
+void Init() {
+  N = NumberOfVillages();
+  Q = NumberOfShipments();
+  NoN = NumberOfNodes();
+  ListCrucialVertices();
+}
+
+struct Path {
+  Path() {}
+  explicit Path(int index)
+      : from(index),
+        to(index),
+        length(1),
+        prefix(1),
+        suffix(1),
+        internal(1),
+        whole(true) {}
+
+  int from;
+  int to;  // We use "-1" as a special value to mean "root".
+  // The lengths here are given in vertices.
+  int length;
+  int prefix;
+  int suffix;
+  int internal;
+  bool whole;
+};
+
+// Joining two paths.
+Path Join(const Path &A, const Path &B) {
+  Path result;
+  result.from = A.from;
+  assert(A.to == B.from);
+  result.to = B.to;
+  result.length = A.length + B.length - 1;
+  result.prefix = A.whole ? A.prefix + B.prefix - 1 : A.prefix;
+  result.suffix = B.whole ? A.suffix + B.suffix - 1 : B.suffix;
+  result.whole = A.whole && B.whole;
+  result.internal = max(max(A.internal, B.internal), A.suffix + B.prefix - 1);
+  return result;
+}
+
+// The precalculated upward paths for all the key vertices. tree[v][d] is the
+// precalculated path from key vertex v to it's ancestor on level (1 << d) - so,
+// tree[v][0] is the path to the nearest ancestor, tree[v][1] the path to the
+// grandfather, tree[v][2] to great-great-grandfather, and so on.
+//
+// The reason we dynamically allocate this is so we can allocate it for the
+// master only. Since the size of this array is some 60MB, allocating it in
+// every single worker would take 6GB, and might hurt our ability to run local
+// tests.
+Path (* tree)[25];
+// This is the depth in the key-vertex forest. 0 means that the vertex has no
+// key ancestor.
+int depths[KEY_VERTS];
+// This is the depth in the real tree. 0 means the key vertex is the root.
+int real_depths[KEY_VERTS];
+
+void MasterDistributePathCalculation() {
+  // We want to send roughly BATCH_COUNT batches to each node. This means
+  // roughly NKV / (NoN * BATCH_COUNT) key vertices per batch.
+  int batch_size = NKV / (NoN * BATCH_COUNT) + 1;
+  int current = 0;  // The beginning of the next batch to send.
+  int in_flight = 0;  // Number of in-flight requests.
+  queue<int> nodes;  // Nodes not currently processing anything.
+  for (int node = 1; node < NoN; ++node) {
+    nodes.push(node);
+  }
+  while (current < NKV || in_flight > 0) {
+    if (nodes.empty() || current >= NKV) {
+      // We receive if we have no ready workers, or if we already pushed out all
+      // the work for processing.
+      int sending_node = Receive(-1);
+      nodes.push(sending_node);
+      in_flight -= 1;
+      int num = GetInt(sending_node);
+      int vert = GetInt(sending_node);
+      for (int i = 0; i < num; ++i) {
+        tree[vert][0].from = vert;
+        tree[vert][0].to = GetInt(sending_node);
+        tree[vert][0].length = GetInt(sending_node);
+        tree[vert][0].prefix = GetInt(sending_node);
+        tree[vert][0].suffix = GetInt(sending_node);
+        tree[vert][0].internal = GetInt(sending_node);
+        tree[vert][0].whole = GetChar(sending_node);
+        vert += 1;
+      }
+    } else {
+      // Otherwise, we send the next block for processing.
+      int num = min(batch_size, NKV - current);
+      int processor = nodes.front();
+      nodes.pop();
+      in_flight += 1;
+      PutChar(processor, 0);
+      PutInt(processor, num);
+      PutInt(processor, current);
+      current += num;
+      Send(processor);
+    }
+  }
+}
+
+void WorkerCalculatePath() {
+  int num = GetInt(0);
+  int first = GetInt(0);
+  PutInt(0, num);
+  PutInt(0, first);
+  for (int key_vert = first; key_vert < first + num; key_vert++) {
+    int vert = key_vertices[key_vert];
+    int ccolor = VillageFaction(vert);
+    int length = 0;
+    int prefix = 1;
+    int suffix = 1;
+    int internal = 1;
+    bool whole = true;
+    // Go up the tree.
+    while (true) {
+      int parent = VillageImmediatelyDownstream(vert);
+      if (parent == vert) {
+        PutInt(0, -1);
+        break;
+      }
+      vert = parent;
+      int ncolor = VillageFaction(vert);
+      length += 1;
+      if (ncolor != ccolor) {
+        whole = false;
+        suffix = 1;
+      } else {
+        suffix += 1;
+        internal = max(suffix, internal);
+        if (whole) prefix += 1;
+      }
+      ccolor = ncolor;
+      auto key_index = lower_bound(&key_vertices[0], &key_vertices[NKV], vert);
+      if (*key_index == vert) {
+        PutInt(0, key_index - &key_vertices[0]);
+        break;
+      }
+    }
+    PutInt(0, length);
+    PutInt(0, prefix);
+    PutInt(0, suffix);
+    PutInt(0, internal);
+    PutChar(0, whole);
+  }
+  Send(0);
+}
+
+void MasterCompressPaths() {
+  // This will be overridden when we end compression for a given vertex - that
+  // is, when it has a ancestor of level X, but this ancestor doesn't have
+  // another ancestor of level X (so we don't have an ancestor of level X+1).
+  // This implies we never override it for vertices with no ancestor, so we
+  // initialize it to zero for them.
+  for (int v = 0; v < NKV; ++v) depths[v] = 0;
+
+  bool still_compressing;
+  int depth_log = 1;
+  do {
+    still_compressing = false;
+    for (int v = 0; v < NKV; ++v) {
+      Path &previous = tree[v][depth_log - 1];
+      int parent = previous.to;
+      if (parent != -1) {
+        tree[v][depth_log] =
+            Join(previous, tree[parent][depth_log - 1]);
+        if (tree[parent][depth_log - 1].to == -1) {
+          depths[v] = (1 << (depth_log - 1)) + depths[parent];
+        }
+        still_compressing = true;
+      } else {
+        real_depths[v] = tree[v][depth_log - 1].length;
+        tree[v][depth_log] = tree[v][depth_log - 1];
+      }
+    }
+    depth_log += 1;
+  } while (still_compressing);
+}
+
+void PreSolveAndPutQuery(int processor, int query) {
+  int first_real_value = GetShipmentSource(query);
+  int second_real_value = GetShipmentDestination(query);
+  int first =
+      lower_bound(&key_vertices[0], &key_vertices[NKV], first_real_value) -
+      &key_vertices[0];
+  int second =
+      lower_bound(&key_vertices[0], &key_vertices[NKV], second_real_value) -
+      &key_vertices[0];
+  int fdepth = depths[first];
+  int sdepth = depths[second];
+
+  // Assume the end of the query is deeper than the beginning.
+  if (fdepth > sdepth) {
+    swap(fdepth, sdepth);
+    swap(first, second);
+  }
+
+  Path fpath(first);
+  Path spath(second);
+
+  // Go up the tree with the second vertex, until we are at the same depth as
+  // the first vertex.
+  int diff = sdepth - fdepth;
+  int cjump = 0;
+  while (diff > 0) {
+    if (diff & 1) {
+      spath = Join(spath, tree[spath.to][cjump]);
+    }
+    diff /= 2;
+    cjump += 1;
+  }
+
+  // Now jump up with both vertices, to get to the two key vertices on the paths
+  // with a common ancestor.
+  cjump = 0;
+  while (tree[fpath.to][cjump].to != tree[spath.to][cjump].to) cjump++;
+  for (int jump = cjump - 1; jump >= 0; --jump) {
+    if (tree[fpath.to][jump].to != tree[spath.to][jump].to) {
+      fpath = Join(fpath, tree[fpath.to][jump]);
+      spath = Join(spath, tree[spath.to][jump]);
+    }
+  }
+  PutInt(processor, fpath.to);
+  PutInt(processor, fpath.internal);
+  PutInt(processor, fpath.suffix);
+  PutInt(processor, real_depths[fpath.to]);
+  PutInt(processor, spath.to);
+  PutInt(processor, spath.internal);
+  PutInt(processor, spath.suffix);
+  PutInt(processor, real_depths[spath.to]);
+}
+
+void MasterSolveQueries() {
+  int answers[MAX_Q + 10];
+
+  // We want to send roughly BATCH_COUNT batches to each node. This means
+  // roughly Q / (NoN * BATCH_COUNT) queries per batch.
+  int batch_size = Q / (NoN * BATCH_COUNT) + 1;
+  int current = 0;  // The beginning of the next batch to send.
+  int in_flight = 0;  // Number of in-flight requests.
+  queue<int> nodes;  // Nodes not currently processing anything.
+  for (int node = 1; node < NoN; ++node) nodes.push(node);
+  while (current < Q || in_flight > 0) {
+    if (nodes.empty() || current >= Q) {
+      // We receive if we have no ready workers, or if we already pushed out all
+      // the work for processing.
+      int sending_node = Receive(-1);
+      nodes.push(sending_node);
+      in_flight -= 1;
+      int num = GetInt(sending_node);
+      int query = GetInt(sending_node);
+      for (int i = 0; i < num; ++i) {
+        answers[query] = GetInt(sending_node);
+        query += 1;
+      }
+    } else {
+      // Otherwise, we send the next block for processing.
+      int num = min(batch_size, Q - current);
+      int processor = nodes.front();
+      nodes.pop();
+      in_flight += 1;
+      PutChar(processor, 1);
+      PutInt(processor, num);
+      PutInt(processor, current);
+      for (int i = 0; i < num; ++i) {
+        PreSolveAndPutQuery(processor, current + i);
+      }
+      current += num;
+      Send(processor);
+    }
+  }
+  for (int i = 1; i < NoN; ++i) {
+    PutChar(i, 2);
+    Send(i);
+  }
+  for (int i = 0; i < Q; ++i) {
+    if (i) printf(" ");
+    printf("%d", answers[i] - 1);
+  }
+  printf("\n");
+}
+
+struct Suffix {
+  int end;
+  int internal;
+  int suffix;
+  int color;
+  int depth;
+};
+
+void Extend(Suffix *s) {
+  int parent = VillageImmediatelyDownstream(s->end);
+  int ncolor = VillageFaction(parent);
+  if (s->color == ncolor) {
+    s->suffix += 1;
+    s->internal = max(s->suffix, s->internal);
+  } else {
+    s->suffix = 1;
+    s->color = ncolor;
+  }
+  s->end = parent;
+  s->depth -= 1;
+}
+
+void WorkerHandleQuery() {
+  int num = GetInt(0);
+  PutInt(0, num);
+  PutInt(0, GetInt(0));  // Bounce the first query number.
+  for (int query = 0; query < num; ++query) {
+    Suffix f;
+    f.end = key_vertices[GetInt(0)];
+    f.internal = GetInt(0);
+    f.suffix = GetInt(0);
+    f.color = VillageFaction(f.end);
+    f.depth = GetInt(0);
+
+    Suffix s;
+    s.end = key_vertices[GetInt(0)];
+    s.internal = GetInt(0);
+    s.suffix = GetInt(0);
+    s.color = VillageFaction(s.end);
+    s.depth = GetInt(0);
+
+    while (f.depth > s.depth) {
+      Extend(&f);
+    }
+    while (s.depth > f.depth) {
+      Extend(&s);
+    }
+    while (f.end != s.end) {
+      Extend(&f);
+      Extend(&s);
+    }
+    int result = max(max(f.internal, s.internal), f.suffix + s.suffix - 1);
+    PutInt(0, result);
+  }
+  Send(0);
+}
+
+void Master() {
+  tree = new Path[KEY_VERTS][25];
+  MasterDistributePathCalculation();
+  MasterCompressPaths();
+  MasterSolveQueries();
+}
+
+void Worker() {
+  while (true) {
+    Receive(0);
+    char command = GetChar(0);
+    switch (command) {
+      case 0:
+        WorkerCalculatePath();
+        break;
+      case 1:
+        WorkerHandleQuery();
+        break;
+      default:
+        return;
+    }
+  }
+}
+
+int main() {
+  Init();
+  if (MyNodeId() == 0) {
+    Master();
+  } else {
+    Worker();
+  }
+  return 0;
+}
+
\ No newline at end of file diff --git a/distributed_codejam/2015_finals/shipping/statement.html b/distributed_codejam/2015_finals/shipping/statement.html new file mode 100644 index 00000000..d60461f4 --- /dev/null +++ b/distributed_codejam/2015_finals/shipping/statement.html @@ -0,0 +1,61 @@ +

Problem

+

+In a certain country that shall remain unnamed, people live in N villages along river valleys separated by mountain ranges. Due to the geography of the country, it is extremely difficult to cross over mountains. The only practical way to get from one village to another is by following rivers. All of the rivers are part of the same system and each river connects to another river downstream, until eventually all water in the system passes through a single point to enter the sea. There is a village at every point where two rivers join and at each river's source, and there is one village at the mouth of the entire river system, where it meets the sea. There may be also be villages at other points along rivers. No two villages are at the same location along a river, and there is exactly one path between any two villages. In other words, the villages and the paths between them form a tree. +

+ +

+You are in charge of managing a shipping company that delivers packages between villages by transporting them on boats along the rivers. Since there is only one path between any two villages, you fortunately do not have to worry about finding the shortest path to use. Unfortunately, however, the country is in the middle of a civil war, with K rival factions battling for control. Each village is under the control of exactly one of the factions. Luckily, conditions are stable at the moment and no village is going to change factions anytime soon. +

+ +

+As a neutral company, you are able to send your boats through every village, but only under the condition that each of the factions is allowed to use your shipping services for free, by loading additional packages onto any of your boats that pass between villages under the control of that faction. Every time one of your boats passes through a village (including the village at which your boat begins its route), you may be given an additional package occupying 1 unit of capacity on your boat, to be transported to another village further along on your boat's current route that is occupied by the same faction. Because the factions do not want their packages to be intercepted by other factions, you will be given a package to transport between two villages only if every village on the path between them (including the destination) is occupied by the same faction. Once you deliver a package, you can reuse the space for another package later on, but multiple packages at the same time require multiple units of capacity. +

+ +

+You are now faced with the problem of guaranteeing enough extra capacity on your boats to transport these extra packages. You have Q shipments, each of which has a source village and a destination village. You will use a different boat for each shipment. For each shipment, determine the number of units of capacity to reserve on your boat in order to carry all of the additional packages for the various factions in the worst case. +

+ + +

Input

+

+The input library is called "shipping"; see the sample inputs below for examples in your language. It defines 6 methods: +

+ +Villages and shipments are both zero-indexed. VillageImmediatelyDownstream returns the index of the village that lies immediately downstream, except for the village at the mouth of the river system, whose return value is the index of this village (since no other village is downstream, just the sea).
+ +A single call of VillageImmediatelyDownstream will take approximately 0.04 microseconds. A single call of VillageFaction will take approximately 0.02 microseconds. +

+ +

Output

+ +

+Output a space-separated list of integers, where the i-th integer is the minimum number of units of capacity needed for the boat delivering the i-th shipment to carry all additional packages in the worst case. +

+ +

Limits

+

+Each node will have access to 256MB of RAM, and a time limit of 5 seconds.
+1 ≤ NumberOfVillages() ≤ 108
+0 ≤ VillageFaction(i) ≤ 106 for all villages
+0 ≤ GetShipmentSource(i), GetShipmentDestination(i) < NumberOfVillages() for all shipments
+The VillageImmediatelyDownstream method will describe a tree (that is, there will be only one path between any two villages).
+Your solution will run on 100 nodes in both inputs.
+

+ +

Small Input

+

+1 ≤ NumberOfShipments() ≤ 10
+VillageImmediatelyDownstream(i) ≤ i for all villages
+

+ +

Large input

+

+1 ≤ NumberOfShipments() ≤ 20,000
+

diff --git a/distributed_codejam/2015_online/almost_sorted/analysis.html b/distributed_codejam/2015_online/almost_sorted/analysis.html new file mode 100644 index 00000000..704fdbaa --- /dev/null +++ b/distributed_codejam/2015_online/almost_sorted/analysis.html @@ -0,0 +1,15 @@ +

+The natural approach to this problem is to have each node take ownership of some range [A, B] of the files, sort them, and calculate the checksum for these files. Then, all the checksums would get shipped to a designated master node, which would sum all the checksums. The first idea might be to simply sort the files that begin in our range – but that idea does not work, since some of the files that begin in this range might not end up in this range after sorting. +

+ +

+The next idea should be to extend the range somewhat, sort the extended range, and hope that files from the smaller range now got correctly sorted. How much do we need to extend the range for this to work (and does it even work at all)? +

+ +

+Let's denote MaxDistance() by K. It turns out that if we sort the range [A - K, B + K], the files that land in [A, B] will be in the correct places. Take any file with identifier X that lands in the range [A, B] after sorting the whole range. We will prove it lands in the same place after sorting [A - K, B + K]. If it lands in place Y, it means there are exactly Y files with smaller identifiers. For X to land on a position smaller than Y after sorting [A - K, B + K], one of these Y smaller files would either have to land after Y within the range [A - K, B + K] (impossible, since this range is sorted, and we assume X landed on a position smaller than Y), or be outside of this range. In this situation it would mean it is on the right of B + K which contradicts the problem statement because the initial index of a file which has to end up in the interval [A, B] was placed more than K positions away. +

+ +

+So, the correct solution is to split the range into equal parts, assign one part to each node, extend each part by MaxDistance() in either direction, sort and send the checksum of the files that landed in our part to some master node for summing up. +

diff --git a/distributed_codejam/2015_online/almost_sorted/statement.html b/distributed_codejam/2015_online/almost_sorted/statement.html new file mode 100644 index 00000000..e992e0de --- /dev/null +++ b/distributed_codejam/2015_online/almost_sorted/statement.html @@ -0,0 +1,67 @@ +

Problem

+ +

+As a very important director of a very important company, you have +a lot of files to keep track of. You do this by carefully keeping them sorted. +However, you took a well-deserved vacation last week, and when you came back, +you discovered to your horror that someone has put the files out of place! +

+ +

+They are not very much out of place, in fact they're still almost sorted. +More precisely, the file that should be in position i if the files were +sorted is now at most K positions away — that is, somewhere between +position i – K and i + K, inclusive.
+However, you can't work like this. So, you ask your assistants to put the files +into their correct places. Each file has an identifier, and files with a larger +identifier should be placed after those with the smaller identifier. +They will not change the relative order of files with the same identifier.
+To verify the files are sorted correctly, you will ask your assistants to +calculate a simple checksum — for each file multiply that file's +identifier by its position (beginning from 0), and sum this for all files, +modulo 220. +

+ +

+Unfortunately, to make use of the checksum, you have to know what it's value +should be. So, write a program that will output the expected checksum +after sorting the files. +

+ +

Input

+

+The input library will be called "almost_sorted"; see the sample inputs +below for examples in your language. It will define three methods: NumberOfFiles(), which +will return the number of files, MaxDistance() — the maximum difference between +the current and desired position of any file, and Identifier(i), which will +return the value of the identifier of the file that's currently standing on +position i, for 0 ≤ i < NumberOfFiles().
+A single call to Identifier() will take approximately 0.04 microseconds.
+

+ +

Output

+

+Output one number — the value of the checksum for the sorted sequence of +files, modulo 220. +

+ +

Limits

+

+Each node will have access to 128MB of RAM, and a time limit of 3 seconds.
+0 ≤ Identifier(i) ≤ 1018, for +0 ≤ i < NumberOfFiles(). +

+ +

Small input

+

+Your solution will run on 10 nodes.
+0 ≤ MaxDistance() < NumberOfFiles() ≤ 1000.
+

+ +

Large input

+

+Your solution will run on 100 nodes.
+1 ≤ NumberOfFiles() ≤ 108.
+0 ≤ MaxDistance() < NumberOfFiles().
+0 ≤ MaxDistance() ≤ 106.
+

diff --git a/distributed_codejam/2015_online/analysis_intro.html b/distributed_codejam/2015_online/analysis_intro.html new file mode 100644 index 00000000..699e2ae6 --- /dev/null +++ b/distributed_codejam/2015_online/analysis_intro.html @@ -0,0 +1,49 @@ +

+The problems on the first Distributed Code Jam online round proved much tougher than we initially expected; in particular no-one managed to successfully tackle the tricky Highest Mountain problem. Solving Almost Sorted and Let Johnny Win with a good time and with some extra points from the smalls was enough to qualify for the finals. +

+ +

+Almost Sorted was a warmup problem, as far as any of the problems in the round can be named easy. Over a hundred correct solutions for the large were submitted. +Mutexes was a two-dimensional DP, with the extra lesson that two-dimensional DPs can be parallelized effectively. +Let Johnny Win was a graph problem, where the crucial realization was that the degrees of the nodes tell almost the whole story. +Highest Mountain required figuring out how to parallelize the convex hull algorithm effectively - a surprisingly complicated task! +

+

+ +

+
+

+Cast +

+

+The cast will be somewhat more robust than usually, as a lot of people were involved in making Distributed Code Jam a reality. In no particular order, the team was: +

+ +A lot of people supported us throughout the almost two years from idea to launch: +Christina Hill, +David Spies, +Dina Garmash, +Dominika Rogozińska, +Elizabeth Sapiro, +Hackson Leung, +Igor Naverniouk, +Jarek Kuśmierek, +John Dethridge, +John Wilkes, +Kacper Nowicki, +Marcin Bilkowski, +Marcin Gawlik, +Piotr Witusowski, +Prasanjit Barua, +Tomek Błaszczyk, + and many many others. +

\ No newline at end of file diff --git a/distributed_codejam/2015_online/highest_mountain/analysis.html b/distributed_codejam/2015_online/highest_mountain/analysis.html new file mode 100644 index 00000000..3ba29f75 --- /dev/null +++ b/distributed_codejam/2015_online/highest_mountain/analysis.html @@ -0,0 +1,320 @@ +

+The first part of the problem is to understand what will the list we're asked about actually contain. It turns out it is the "upper convex hull" - that is, the set of points that would be in the convex hull if we added one extra point with height of minus infinity. +

+ +

+The description of the list says that we exclude any peak A such that there exists a line connecting two peaks B and C, one to each side of A, going above A, and A is visible from B. If such a line exists, then - indeed - we know A is not the highest peak (because we can see it from B, but we also see C as higher).
+One of the definitions of an upper convex hull is that we exclude any peak A such that there exists a line connecting two peaks B and C, one to each side of A, going above A (without the extra visibility condition). So, if we exclude a peak from our list we obviously exclude it from the convex hull as well. We have to understand why a peak excluded from the convex hull will also be excluded from our list. +

+ +

+To see this, consider some peak A that is excluded from the convex hull. Out of all the possible lines BC that would cause it to be excluded from the convex hull, take the one in which B is as close to A as possible (horizontally). Then, we claim A is visible from B. If it weren't, there would be some other peak D between A and B, lying above the line AB - but then the line DC would also lie above A, and - as D is closer horizontally to A than B - contradicting our choice of B. Thus, every peak excluded from the convex hull is also excluded from our list - so our list simply is the upper convex hull. +

+ +

+The problem is far from over at this point, however. While calculating the upper convex hull of a set of points is a standard problem on one machine (solvable in O(N), if the points are given sorted by the x-coordinate, as they are in this problem), we need to parallelize this, as the input is too large to read in on one machine. +

+ +

+Let's begin by splitting the input evenly between nodes, each node getting a range of consecutive points, and calculating the upper convex hull of these points. One tempting option is to try shipping all these convex hulls to one node and have it merge them all. Unfortunately, this doesn't work - the convex hull calculated on each node can have over 2 million points (even after some compressions), and sending nearly 800MB of data to one node will not be fast enough. +

+ +

+Fortunately, when we have two upper convex hulls on separate ranges of points, merging them can be done much faster (and using much less data) than in linear time. To merge two upper convex hulls, we need to find a common tangent line to both; which can be done through a nested ternary search. For a point on one of the hulls, we can find the line through this point and tangent to the other hull by a ternary search on the other hull. And now we can ternary-search on the first hull for the point in which this tangent line to the second hull will be tangent to the first hull as well. +

+ +

+A naive implementation of this nested ternary search between two nodes would send a lot of messages back and forth (a single ternary search sends queries for around 80 points - with 5ms for a single turnaround this would mean around 0.4s for merging two nodes). To speed this up, we can "prefetch" the points to query. Instead of sending two points as a single ternary-search step, we can send, say, 1000 points, and have the addressee do the ternary search locally. This way, in two or three message exchanges we will merge two convex hulls. +

+ +

+Now we can merge the convex hulls one by one. We also need to accommodate the possibility that one of the convex hulls will fully disappear from the result. In this case, we need to forget about it, and merge the previous one with the other convex hull from the pair. +

+ +

+Calculating the convex hull in a given node is dominated by reading the input, which will take roughly 0.4 seconds. Merging two convex hulls requires sending at most three messages back and forth, taking around 20ms, which gives a total runtime of below 3s for obtaining the whole answer. +

+ +

+As nobody managed to solve this problem successfully during the contest, we present the C++ code we used in the judge solution: +

+ +
+#include "highest_mountain.h"
+#include "message.h"
+#include <cstdio>
+#include <vector>
+#include <utility>
+
+typedef long long ll;
+using namespace std;
+vector<pair<int, ll>> convex;
+int cbeg;
+int cend;
+int prev_elements;  // 0
+
+const char kQuery = 'q';
+const char kBack = 'b';
+const char kForward = 'f';
+const char kEnd = 'e';
+
+#define CHUNK_SIZE 1000
+
+// The goal of the "Drive" method is to reconcile our convex hull with the
+// convex hull of the guy after us ("next"). We do this by trying to find a
+// tangent line to both our hulls. If this line goes through the "warden" (that
+// is, it bypasses all our points), we will relinquish control to "prev" and let
+// our predecessor do the driving. If not, we let our successor drive. This
+// operation can cause some points to drop out from our set from the right
+// (which we represent by decreasing "cend").
+void Drive(int prev, int next) {
+  // To merge two convex hulls, we do a moral equivalent of a ternary search.
+  // So, in an ideal (zero-time-message-passing) world, we'd send a pair of
+  // points to our successor, and eliminate a third of the interval.
+  // However, the message passing is not zero-cost, so we optimize by sending a
+  // group of points (an equidivision of our set of points), and we will get
+  // back the information "out of these points, _this_ one is the one the
+  // tangent line passes through". This means the real answer is in one of the
+  // intervals adjacent to this point. If there is more than one candidate point
+  // at this moment, rinse and repeat.
+  vector<int> chunk;
+  // Now, push the elements in our convex hull at the other guy.
+  int cleft = cbeg;
+  int cright = cend;
+  // In each loop, we know the answer is in [cleft, cright).
+  while (cright - cleft > 1) {
+    chunk.clear();
+    // We can just send all of our points.
+    if (cright - cleft <= CHUNK_SIZE) {
+      for (int i = cleft; i < cright; ++i) {
+        chunk.push_back(i);
+      }
+    } else {
+      // Equally spaced points, containing the cleft and cright - 1.
+      for (int i = 0; i < CHUNK_SIZE; ++i) {
+        int pos = cleft + ((cright - cleft - 1) * (ll) i) / (CHUNK_SIZE - 1);
+        chunk.push_back(pos);
+      }
+    }
+    PutChar(next, kQuery);
+    PutInt(next, chunk.size());
+    for (int i = 0; i < chunk.size(); ++i) {
+      PutInt(next, convex[chunk[i]].first);
+    }
+    Send(next);
+    // The guy after us will now tell us which of our points is the last one in
+    // our common convex hull.
+    Receive(next);
+    int pos_in_chunk = GetInt(next);
+    // So, we know the real point of tangency is somewhere between the left and
+    // right neighbour of the point we got back (it's neither of them).
+    if (pos_in_chunk != 0) {
+      cleft = chunk[pos_in_chunk - 1] + 1;
+    }
+    if (pos_in_chunk != chunk.size() - 1) {
+      cright = chunk[pos_in_chunk + 1];
+      // Also, as an aside, we can throw out the points to the right from the
+      // game.
+      cend = cright;
+    }
+  }
+  // One case is we're left with the warden in hand. In this case, none of
+  // our points will make it to the convex hull. We're out of the game, and
+  // our predecessor gets to drive.
+  if (cleft == cbeg && MyNodeId() != 0) {
+    PutChar(prev, kBack);
+    PutInt(prev, next);
+    Send(prev);
+    return;
+  }
+  // Otherwise, we still have some of our convex hull elements in the game.
+  // Driving moves on to the guy after us. We tell him how many elements of the
+  // convex hull are before him, and what's the last element of the convex hull
+  // before him.
+  PutChar(next, kForward);
+  PutInt(next, prev_elements + cend - cbeg - 1);
+  PutInt(next, convex[cleft].first);
+  Send(next);
+  return;
+}
+
+// Positive means P0 is under (to the right) of the P1->P2 line.
+ll Dist(const pair<int, ll> &P1, const pair<int, ll> &P2,
+        const pair<int, ll> &P0) {
+  return P0.first * (P2.second - P1.second) -
+         P0.second * (P2.first - P1.first) + P2.first * P1.second -
+         P2.second * P1.first;
+}
+
+// Does the line P1, P2 cut our polygon, or is it tangent/outside it?
+bool Cuts(const pair<int, ll> &P1, const pair<int, ll> &P2) {
+  int left = cbeg;
+  int right = cend;
+  while (right - left > 2) {
+    int c1 = (2 * left + right) / 3;
+    int c2 = (left + 2 * right) / 3;
+    ll d1 = Dist(P1, P2, convex[c1]);
+    ll d2 = Dist(P1, P2, convex[c2]);
+    if (d1 < 0 || d2 < 0) return true;
+    if (d1 > d2) {
+      left = c1;
+    } else {
+      right = c2;
+    }
+  }
+  for (int i = left; i < right; ++i) {
+    if (Dist(P1, P2, convex[i]) < 0) return true;
+  }
+  return false;
+}
+
+// The query method is the "answer" part of the drive method. The "who" node
+// sends us a set of points, and wants to know which one of them will be a part
+// of the common tangent line.
+// We assume we can retrieve the whole matrix (in the format "size, elements")
+// from the message.
+void Query(int who) {
+  int hleft = 0;
+  int hright = GetInt(who);
+  vector<pair<int, ll>> h;
+  for (int i = 0; i < hright; ++i) {
+    int x = GetInt(who);
+    h.push_back(make_pair(x, (ll)GetHeight(x)));
+  }
+  // Right now, I have two convex hulls, and I want to merge them. Observe a
+  // line formed by two adjacent points in the "input" polygon. If this line
+  // cuts the right polygon, we know the right of the two adjacent points is
+  // below the convex hull. Otherwise, we know the left point is below the
+  // tangent line.
+  while (hright - hleft > 1) {
+    int hmed = (hright + hleft - 2) / 2;
+    if (Cuts(h[hmed], h[hmed + 1])) {
+      hright = hmed + 1;
+    } else {
+      hleft = hmed + 1;
+    }
+  }
+  PutInt(who, hleft);
+  Send(who);
+}
+
+// Calculate my own convex hull.
+void DoConvex(ll mybeg, ll myend) {
+  cbeg = 1;
+  cend = 1;
+  prev_elements = 0;
+  // A placeholder for a "warden" from the previous batch.
+  convex.push_back(make_pair(-1, -1LL));
+  if (myend - mybeg < 2) {
+    for (int i = mybeg; i < myend; ++i) {
+      convex.push_back(make_pair(i, GetHeight(i)));
+      cend++;
+    }
+  } else {
+    convex.push_back(make_pair(mybeg, GetHeight(mybeg)));
+    convex.push_back(make_pair(mybeg + 1, GetHeight(mybeg + 1)));
+    cend = 3;
+    for (int i = mybeg + 2; i < myend; ++i) {
+      ll h = GetHeight(i);
+      while (cend > 2) {
+        int x1 = convex[cend - 1].first - convex[cend - 2].first;
+        int x2 = i - convex[cend - 2].first;
+        ll y1 = convex[cend - 1].second - convex[cend - 2].second;
+        ll y2 = h - convex[cend - 2].second;
+        if (x1 * y2 > x2 * y1) {
+          cend -= 1;
+          convex.pop_back();
+        } else {
+          break;
+        }
+      }
+      cend += 1;
+      convex.push_back(make_pair(i, h));
+    }
+  }
+}
+
+void InsertWarden(int warden_pos) {
+  auto warden = make_pair(warden_pos, (ll)(GetHeight(warden_pos)));
+  int left = cbeg;
+  int right = cend - 2;
+  if (Dist(warden, convex[left], convex[left + 1]) >= 0) {
+    convex[--cbeg] = warden;
+    return;
+  }
+  if (Dist(warden, convex[right], convex[right + 1]) < 0) {
+    convex[right] = warden;
+    cbeg = right;
+    return;
+  }
+  while (right - left > 1) {
+    int med = (left + right) / 2;
+    if (Dist(warden, convex[med], convex[med + 1]) < 0) {
+      left = med;
+    } else {
+      right = med;
+    }
+  }
+  convex[left] = warden;
+  cbeg = left;
+}
+
+int main() {
+  ll N = NumberOfPeaks();
+  // Just to make sure everyone has a non-empty slice.
+  if (N < 1000) {
+    if (MyNodeId() != 0) {
+      return 0;
+    } else {
+      DoConvex(0, N);
+      printf("%zd\n", convex.size() - 1);
+      return 0;
+    }
+  }
+  ll mybeg = N * MyNodeId() / NumberOfNodes();
+  ll myend = N * (MyNodeId() + 1) / NumberOfNodes();
+  DoConvex(mybeg, myend);
+  int previous_node = -1;
+  int next_node = MyNodeId() + 1;
+
+  if (MyNodeId() != 0) {
+    while (true) {
+      int source = Receive(-1);
+      char c = GetChar(source);
+      if (c == kQuery) {
+        Query(source);
+      } else {
+        assert(c == kForward);
+        previous_node = source;
+        prev_elements = GetInt(source);
+        int warden = GetInt(source);
+        InsertWarden(warden);
+        break;
+      }
+    }
+  }
+  // If we're the last node, the whole convex hull is calculated at this point.
+  if (MyNodeId() == NumberOfNodes() - 1) {
+    for (int i = 0; i < NumberOfNodes() - 1; ++i) {
+      PutChar(i, kEnd);
+      Send(i);
+    }
+    printf("%d\n", prev_elements + cend - cbeg);
+    return 0;
+  }
+  Drive(previous_node, next_node);
+  // At this point, we either told someone in front to drive, or we told someone
+  // in the back to drive and then skip us. Regardless, the next request will
+  // come from the front, and will either be a request to drive, or a request to
+  // end.
+  while (true) {
+    int source = Receive(-1);
+    char c = GetChar(source);
+    if (c == kEnd) return 0;
+    assert(c == kBack);
+    next_node = GetInt(source);
+    Drive(previous_node, next_node);
+  }
+  return 0;
+}
+
diff --git a/distributed_codejam/2015_online/highest_mountain/mountains.png b/distributed_codejam/2015_online/highest_mountain/mountains.png new file mode 100644 index 0000000000000000000000000000000000000000..c7151386f1081b8eda609d73b6ec08831dd65b9d GIT binary patch literal 2945 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 SaKQILV3Xfg-`WkNbN>M};~sqg literal 0 HcmV?d00001 diff --git a/distributed_codejam/2015_online/highest_mountain/statement.html b/distributed_codejam/2015_online/highest_mountain/statement.html new file mode 100644 index 00000000..ba92be99 --- /dev/null +++ b/distributed_codejam/2015_online/highest_mountain/statement.html @@ -0,0 +1,81 @@ +

Problem

+ +

+You were born and live in a small town in a remote mountain range extending +east to west. In this mountain range there is a peak every kilometer, and there +are no intermediate peaks. Recently, you checked on the Internet what the +highest peak in the range is, and were suprised — from your town a +different peak seems to be the highest. And when you went for a walk to the +nearest mountain top, yet another peak appeared to be highest. You're not sure +the Internet data is correct (they write all sorts of stuff on the Internet!), +so you decided to compile your own list of potentially highest peaks in the +range. + +

+Your list will contain every peak B with the following property: +if B is visible from some other peak A, no peak beyond B +is visible from A. +Formally, this means that if B lies, say, to the east of A, +then all peaks between A and B are below the line connecting +A and B, and all the peaks to the east of B are below or +on that line. +

+ +
+ +

+In this example, the fourth peak is the last one you see to the east from +the first and third peaks. From the second peak, to the east you only see the +third peak. Your list will include only peaks 1 and 4: peak 2 is visible +from peak 3, but is not the farthest (1 is), and peak 3 is visible from +peak 1, but is not the farthest (4 is). +

+ +

+The rationale for this criterion is that you figure that if from some peak +A you can see peak C, and you can also see some other peak +D that lies in the same direction and is more distant, then C is +obviously not the highest peak in the range (because either A, or +D, is higher). You don't trust your intuition any more, so even if the +highest visible peak in +any direction appears to be much lower than the one you're standing on (for +instance, you are standing on a peak of height 1000, and the next and last peak +to the east is of height 1), you will consider the peak of height 1 to be a +candidate for your list. +

+ +

Input

+

+The input library will be called "highest_mountain"; see the sample +inputs below for examples in your language. It will define two methods: +NumberOfPeaks(), +which will return the number of peaks in the range, and GetHeight(i), +which will return the height of the ith peak from the west, for +0 ≤ i < NumberOfPeaks().
+One call to GetHeight will take approximately 0.1 microseconds.
+

+ +

Output

+

+Output one number: the total number of the peaks that will be included in your +list. +
+

+ +

Limits

+

+Each node will have access to 128MB of RAM, and a time limit of 6 seconds.
+0 ≤ GetHeight(i) ≤ 109 for all i with 0 ≤ i < NumberOfPeaks().
+

+ +

Small input

+

+Your solution will run on 10 nodes.
+1 ≤ NumberOfPeaks() ≤ 1000.
+

+ +

Large input

+

+Your solution will run on 100 nodes.
+1 ≤ NumberOfPeaks() ≤ 4 " 108.
+

diff --git a/distributed_codejam/2015_online/johnny/analysis.html b/distributed_codejam/2015_online/johnny/analysis.html new file mode 100644 index 00000000..43d4ba32 --- /dev/null +++ b/distributed_codejam/2015_online/johnny/analysis.html @@ -0,0 +1,19 @@ +

+This is a graph problem - we have a directed graph with exactly one edge between every pair of vertices (such a graph is called a tournament), and we are supposed to split its vertices into two groups, so that every edge between the first and the second group is directed from the first into the second group (additionally maximizing the size of the first group). +

+ +

+One approach that some contestants tried (and some even succeeded) is to do some variant of topological sorting on the graph. This would be easy on a single node, but is hard to parallelize efficiently. +

+ +

+The simplest approach to parallelization is degree-based. Observe that if we have a correct split of the graph, with the first set containing K vertices, and the second set containing N-K, then the out-degree of every vertex in the first group has to be at least N-K, and the out-degree of every vertex in the second group has to be at most N-K-1. So, if we calculate the out-degree of each vertex, we can order the vertices by degree, and we know that the desired split (if it exists) will be obtained by taking some prefix of the ordered list. +

+ +

+Calculating the out-degrees parallelizes trivially, we can do this in O(N2 / NumberOfNodes()) time, and then ship the O(N) vertex degree values to one master node, and sort them in O(N log N) time. +

+ +

+Moreover, a split will be correct if and only if there are exactly K(N-K) edges leaving the first group. Notice that there will be always K(K-1) / 2 edges within the first group - exactly one for every pair of vertices. Thus, the master node can iterate over the vertices, keeping track of the sum of the out-degrees of already visited vertices; and return the largest group size that satisfies the degree condition. +

diff --git a/distributed_codejam/2015_online/johnny/statement.html b/distributed_codejam/2015_online/johnny/statement.html new file mode 100644 index 00000000..65534f41 --- /dev/null +++ b/distributed_codejam/2015_online/johnny/statement.html @@ -0,0 +1,56 @@ +

Problem

+ +

+You and Johnny play a very simple card game. Both players have a deck of cards. +Both draw a card at random from their deck, and the player with the better card +wins. "Better" is a complex concept, but for any two cards, you (and +Johnny) know which one is better, with no ties allowed. Note that this is not +necessarily transitive: if card A is better than B, and B is better than C, it +is possible that C is better than A. +

+

+Johnny is very, very unhappy when he loses. So, you would like to make sure that +he will win. You have N cards, and you want to split them into two +non-empty decks (with no cards left over), one for you and one for Johnny, +so that whatever card you draw from your deck and whatever card Johnny draws +from his, Johnny will win. +

+

+If it is possible to do this in multiple ways, you want Johnny's +deck to be as large as possible (as long as your deck is not empty). +

+ +

Input

+

+The input library will be called "johnny"; see the sample inputs +below for examples in your language. It will define two methods: NumberOfCards(), which +will return the number of cards, and IsBetter(i, j), which will return true +if card i is better than card j, for 0 ≤ i, +j < NumberOfCards(). +If called with i=j, it will return false. +One call to IsBetter will take approximately 0.025 microseconds.
+

+ +

Output

+

+If it is possible to split the cards so that Johnny will always win, output one +number: the largest possible size of Johnny's deck. If it is impossible to split +the cards in such a way, output the word IMPOSSIBLE. +

+ +

Limits

+

+Each node will have access to 256MB of RAM, and a time limit of 3 seconds.
+

+ +

Small input

+

+Your solution will run on 10 nodes.
+2 ≤ NumberOfCards() ≤ 1000.
+

+ +

Large input

+

+Your solution will run on 100 nodes.
+2 ≤ NumberOfCards() ≤ 2 × 104.
+

diff --git a/distributed_codejam/2015_online/mutexes/analysis.html b/distributed_codejam/2015_online/mutexes/analysis.html new file mode 100644 index 00000000..d379b860 --- /dev/null +++ b/distributed_codejam/2015_online/mutexes/analysis.html @@ -0,0 +1,38 @@ +

+The optimal and correct approach for this problem is to use dynamic programming to find out if it is possible to end up in a deadlock. +

+ +

+The program keeps track of which program states can be reached. We define a program state to be a pair of: the number of steps thread 1 performed, and the number of steps thread 2 performed. Note that if one of the threads didn't perform any steps, the state is reachable (because one thread will never deadlock by itself). +

+ +

+A deadlock is, by definition, a state that the program can reach, but in which neither thread can perform a step. +

+ +

+A state is reachable if one of the previous states (that is, ones where one of the threads performed a step less) is reachable, and after the thread performs the extra step, no mutex is acquired by both of the threads. Note that the condition "no mutex is acquired by both of the threads" is independent from which thread is actually performing the step - it's simply checking whether the sets of held mutexes is disjoint. +

+ +

+We shard the DP by the steps taken by the first thread. That is, each node is assigned a range of steps for the first thread, and processes those. From the previous node, we only need the information about the reachability of the states just one step before for the first thread (and all possible values for the second thread). +

+ +

+Note that such a two-dimensional DP is relatively easy to parallelize. We can organize each node iterating on the steps of the second thread in the outer loop, and on the steps of the first thread in the inner loop. Then, after O(N/M) calculations, the first node will have the first reachability value available for the second node. In the first approximation, we can imagine each node shipping each value immediately to the next node when it is available. Then after O(M) iterations each node will have something to process, and so the total runtime of the algorithm will be O(N2 / NumberOfNodes()). In reality, sending the values one-by-one is too expensive, and they have to be shipped in chunks (for instance, N/M values in a single chunk), which still gives us a complexity estimate of O(N2 / NumberOfNodes()). +

+ +

+To calculate the set of held mutexes, we want to access the information "is any mutex double-held" in constant time. What we do is as follows:
+

    +
  • We assume for a moment that any mutex can be held by both threads.
  • +
  • When processing a state, we want to know for each mutex by how many threads + is it held.
  • +
  • Additionally, we want to know how many mutexes are double-held.
  • +
+Updating this data when performing a single step is simple. The initial state of the program is also simple (and getting to the initial state at the beginning of our chunk of thread one steps is also simple). After each full range of thread one steps, we get back to the known state simply by reversing the operations taken, in reverse order. +

+ +

+Some of the solutions that passed our tests neglected checking the reachability condition, and yet still passed (because the test-cases we prepared didn't cover this, unfortunately). Solutions that would have passed a fuller set of test cases have been submitted, e.g., by Eryx and Lovro. +

diff --git a/distributed_codejam/2015_online/mutexes/statement.html b/distributed_codejam/2015_online/mutexes/statement.html new file mode 100644 index 00000000..181017ae --- /dev/null +++ b/distributed_codejam/2015_online/mutexes/statement.html @@ -0,0 +1,97 @@ +

Problem

+ +

+In writing multi-threaded programs one of the big problems is to prevent +concurrent access to data. One of the more common mechanisms for doing this is +using mutual exclusion locks (also called mutexes). A mutex +is something that can be acquired by only one thread at a time. If one thread +has already acquired the mutex, and a second thread tries to acquire it, the +second thread will wait until the first thread releases the mutex, and only then +will it proceed (with acquiring the mutex and doing whatever it planned on +doing next). +

+ +

+A danger when using mutexes is deadlock — a situation where some +threads block each other and will never proceed. A deadlock occurs when each one +thread has already acquired mutex A, and now tries to acquire mutex +B, while another thread has already acquired mutex B and tries to +acquire mutex A (more complex scenarios with more threads are also +possible, but we will only be concerned with the two-thread situation). +

+ +

+You are now analyzing a two-threaded program, and trying to determine whether it +will deadlock. You know the exact set of mutex operations (acquire and release) +each of the two threads will perform, in order. However, you do not know how +quickly each thread will perform each of its operations — it is possible, +for instance, for one thread to perform almost all of its operations, then for +the other thread to catch up, and then for the first thread to proceed. +

+ +

+You are interested in determining whether it is possible that the two threads +will deadlock. Initially all the mutexes are released. We assume that when a +thread has finished all of its operations, it releases all the mutexes it +still has acquired. +

+ +

Input

+

+The input library will be called "mutexes"; see the sample inputs +below for examples in your language. It will define two methods: +NumberOfOperations(i), which will return the number of operations thread +i performs (i has to be 0 or 1), and GetOperation(i, index), +which will report what the indexth operation performed by thread i +is (where i is 0 or 1, and +0 ≤ index < NumberOfOperations(i)). This will +be a positive number X if the indexth operation is to acquire +mutex X, and a negative number -X if the indexth +operation is to release mutex X.
+The sequence of operations for a single thread will always be valid, that is, +a given thread will never try to acquire a lock it has already acquired (and not +yet released), or release a lock it has already released (and not yet acquired) +or has never acquired in the first place. A thread's first operation on a lock +(if any) will always be an acquire operation.
+One call to GetOperation will take approximately 0.005 microseconds, with the +exception of the first call, which will cache the input values and might take +up to 100 milliseconds.
+

+ +

Output

+

+Output the smallest total number of operations the two threads can perform +before deadlocking (including the last two acquire operations), if a deadlock +is possible, or the word OK if a deadlock can't happen. +

+ +

Limits

+

+Each node will have access to 256MB of RAM, and a time limit of 4 seconds.
+-105 ≤ GetOperation(i, index) ≤ 105 for all valid +i and index. GetOperation will never return 0.
+

+ +

Small input

+

+Your solution will run on 10 nodes.
+1 ≤ NumberOfOperations(i) ≤ 1000 for both possible values of i. +
+

+ +

Large input

+

+Your solution will run on 100 nodes.
+1 ≤ NumberOfOperations(i) ≤ 4 ×104 for both possible +values of i. +
+

+ +

+The fastest way to deadlock in the third example is for the first thread to +perform the first three operations (ending up with mutexes 1, 2 and 3), then +for the first thread to perform the first operation (acquiring mutex 4). At +this point both threads try to perform one operation more (the first thread +trying to acquire mutex 4, the second thread trying to acquire mutex 3) and +deadlock. +

diff --git a/distributed_codejam/2015_practice/analysis_intro.html b/distributed_codejam/2015_practice/analysis_intro.html new file mode 100644 index 00000000..0f6da07f --- /dev/null +++ b/distributed_codejam/2015_practice/analysis_intro.html @@ -0,0 +1,27 @@ +

+The practice round for the Distributed Code Jam has ended. While the point was mostly to familiarize everybody with the system, we will provide standard editorials for the problems we used. +

+ +

+Sandwich was a relatively standard DP problem, with a rather simple distributed algorithm. Majority involved an adaptation of the Boyer-Moore majority algorithm, which parallelizes surprisingly well. Shhhh was an example how difficult graph problems become when you need to shard the solution. Finally, Load Balance involved taking a meet-in-the-middle algorithm and sharding it to run fast enough through local optimization or clever hashing. +

+ +
+

+Cast +

+

+Problem A. Sandwich written and prepared by Onufry Wojtaszczyk. +

+ +

+Problem B. Majority written and prepared by Onufry Wojtaszczyk. +

+ +

+Problem C. Shhhh written by Onufry Wojtaszczyk, based on ideas by Robert Obryk and Adam Polak, prepared by Onufry Wojtaszczyk. +

+ +

+Problem D. Load Balance written by Maciek Klimek and Onufry Wojtaszczyk, prepared by Onufry Wojtaszczyk. +

\ No newline at end of file diff --git a/distributed_codejam/2015_practice/load_balance/analysis.html b/distributed_codejam/2015_practice/load_balance/analysis.html new file mode 100644 index 00000000..9e249225 --- /dev/null +++ b/distributed_codejam/2015_practice/load_balance/analysis.html @@ -0,0 +1,31 @@ +

+The problem asks, given a set of numbers, is it possible to split them into two sets with equal sums. This is a case of the classic Subset Sum problem. +

+ +

+Let's begin by describing two single-node solutions for this problem. The first one is a naive solution that simply calculates all the possible sums (all 2N of them, each in O(N) operations), and check whether any of them is equal to half of the total sum. This can be optimized, by iterating over the sets in a smarter order, to run in O(2N) time, without the O(N) factor. +

+ +

+A much faster solution is meet-in-the-middle. This works as follows: we split the N numbers into two parts. For each part, we calculate all the 2N/2 sums, and then sort them. Then, we try to find a pair - one sum from the first part and one sum from the second - that adds up to the required half of the total. We do this by sorting both sets of sums, and then holding an iterator on each of the sorted sets. The iterator on the first set begins on the highest number, the iterator on the second one begins on the lowest number. If the total sum pointed to the two iterators is higher than what we are looking for, we increment the second iterator, if the total is lower, we decrement the first. This runs in O(2N/2 N), where the O(N) factor is for sorting. Again, this can be optimized to run in O(2N/2) time, and with enough optimizations, it was possible to solve this problem on a single node (which is why we used it in a practice contest, and not in the real one). +

+ +

+Now let's talk about sharding this problem. The first idea that comes to mind is to take the first log(M) elements of the set, and assign each of the nodes a particular set of these elements, that we assume will go into the final sum. This effectively decreases N by log(M), and so has a runtime of O(N 2(N - log(M)) / 2) = O(N 2N/2 / sqrt(M)), again with optimizations possible to get rid of the N factor. However, one feels that this is suboptimal - ideally, having M nodes we should be able to speed up the solution M times. Can we do this? +

+ +

+Let's begin with an approach that doesn't really work, but shows what we should be trying to achieve. Let's do almost the same thing as we did in the classical meet-in-the-middle solution, but assign each node a remainder modulo M. A node with remainder K assigned will be looking for solutions where the sum in the left part equals K mod M (and so the sum in the right part equals (half of total - K) mod M). If we were able to generate the list of such sums which add up to K mod M fast, and if these lists were of roughly equal length for each K, then each list would be of length O(2N/2 / M), and we would have a O(N 2N/2 / M), possibly with optimizations to get rid of the N factor, if we could generate them sorted. +

+ +

+First, how to generate the lists. The problem boils down to: we have X numbers (X = N / 2), and we want to generate all the sums of these X numbers that equal K mod M. One simple way to do it is to split X in half again, generate all sums of the X/2 numbers in each half, bucket them by remainder mod M, and then join the appropriate buckets. We have 2X/2 sums in each half, which is less than O(2X / M) numbers we want to generate, so we can easily afford this. +

+ +

+Unfortunately, the M lists we generate have no reason to be of equal size. First, because even a random split would introduce somewhat unequal lists, and second, because the test data might be (and was) crafted to defeat such an approach. To solve this, instead of doing the split modulo M, we can do the split modulo some random number around 10M. We also have to take care to deduplicate the sums we generate, in order to deal with the case where all the numbers in the input are equal (and so would land in one bucket). +

+ +

+We leave the question of whether it is possible to get rid of the N factor in this solution to the reader. The tests are crafted to allow the O(N 2N/2 / M) solution pass. A well-crafted, single-node implementation of the O(2N/2) solution might fit in time (and memory) as well, which is why we used this problem in the practice contest, and not in the real one! +

diff --git a/distributed_codejam/2015_practice/load_balance/statement.html b/distributed_codejam/2015_practice/load_balance/statement.html new file mode 100644 index 00000000..6ed25567 --- /dev/null +++ b/distributed_codejam/2015_practice/load_balance/statement.html @@ -0,0 +1,45 @@ +

Problem

+ +

+At your flat, you take turns in bringing the groceries in. Each day, one person +goes out and does the shopping for everybody who lives in the whole building. +Today it's your turn, you did the shopping, and all these bags are heavy!
+You can't do much about the fact the bags are heavy — people depend on you +to bring the bags home. But they will be easier to carry if you distribute the +load equally between the left and the right hand. So, you look at all the bags +you have, and wonder whether it's possible to split them so that the weight of +the ones you'll carry in your left hand will be equal to the weight of those you +will carry in your right hand.
+

+ +

Input

+

+The input library will be called "load_balance", see the sample inputs +below for examples in your language. It will define two methods: GetN(), which +will return the number of bags you have to carry, and GetWeight(i), which will +return the weight of the ith bag, for 0 ≤ i < N.
+

+ +

Output

+

+Output one string: "IMPOSSIBLE" if it is impossible to split the load +equally, or "POSSIBLE" if it is possible (quotes are for clarity only).
+

+ +

Limits

+

+Each node will have access to 512MB of RAM, and a time limit of 4 seconds.
+1 ≤ GetWeight(i) ≤ 1015 for all i with 0 ≤ i < GetN().
+

+ +

Small input

+

+Your solution will run on 10 nodes.
+1 ≤ GetN() ≤ 30.
+

+ +

Large input

+

+Your solution will run on 100 nodes.
+1 ≤ GetN() ≤ 52.
+

diff --git a/distributed_codejam/2015_practice/majority/analysis.html b/distributed_codejam/2015_practice/majority/analysis.html new file mode 100644 index 00000000..b48b106e --- /dev/null +++ b/distributed_codejam/2015_practice/majority/analysis.html @@ -0,0 +1,23 @@ +

+The intended solution for this problem was an adaptation of the Boyer-Moore algorithm for calculating the majority. It's a fun algorithm, and it's nice to know it. +

+ +

+The way it works (on one node) is it does two passes over the input. First, it finds a candidate we suspect of having a majority, with a guarantee that if anybody has a majority, it is the suspect. Once we have a suspect, we can do a second pass to count how many votes the suspect got and verify whether she or he has a majority. +

+ +

+The way to find the suspect is to notice that if there is a global majority, then removing two votes for different candidates does not cause it to stop being a global majority. So, we will continue removing votes for different candidates, until only votes for one candidate are left; and we will treat this candidate as a suspect. The way to do this is to process the votes one by one, and keep track of the current suspect (the only person with votes, after we remove some pairs of votes for different candidates), and the number of votes for that person. When processing a vote, and we have a suspect with a positive number of votes, we either add one vote, or remove it (depending on whether the vote is for the current suspect or for someone else). When processing a vote with no current suspect, we take the candidate for whom the vote is as the suspect, with one vote. +

+ +

+Now, how to shard this solution into multiple nodes? It turns out this algorithm adapts really well to sharding. When we reduce the set of votes in each node to one suspect; we send the id of the candidate and the number of votes to one central node. From this calculating one global candidate (again by removing pairs of votes for different candidates) is easy. Once we have a global suspect, we send this to every node, every node counts the number of votes for the global suspect, and verify whether this is indeed a majority candidate. +

+ +

+There are slower solutions that can pass as well. One such solution is simply to calculate the majority candidate in each node, and then, in each node, count the number of votes for each of these NumberOfNodes candidates. Note that if there is a global majority, it has to be a majority on at least one of the nodes. Another slower, but workable (with care) approach is to do the local calculations simply by counting votes for each candidate (instead of doing the Boyer-Moore algorithm). +

+ +

+Finally, there are a number of incorrect or randomized solutions that are very hard to eliminate by tests. For instance, to identify the set of candidates everybody should count, we can simply select 100 random votes. If there is a majority, the chances we won't hit it in random 100 votes are below 2-100. These reasons eliminated this problem from use in the real contest. +

diff --git a/distributed_codejam/2015_practice/majority/statement.html b/distributed_codejam/2015_practice/majority/statement.html new file mode 100644 index 00000000..d89f31a6 --- /dev/null +++ b/distributed_codejam/2015_practice/majority/statement.html @@ -0,0 +1,42 @@ +

Problem

+

+Your country is electing its president, and you are in charge of the new +electronic voting system. The citizens have voted, and now you +have to check if any of the candidates obtained a majority — that +is, if there is a candidate for whom more than half of the citizens voted.
+

+ +

Input

+

+The input library will be called "majority", see the sample inputs +below for examples in your language. It will define two methods: GetN(), which +will return the number of voting citizens N, and GetVote(i), +which will (for 0 ≤ i < N) return the identifier of the candidate for whom +citizen i voted.
+

+ +

Output

+

+If any candidate obtained a majority of the votes, output the identifier of that +candidate. Otherwise, output the string "NO WINNER" (quotes for +clarity only).
+A single call to GetVote(i) will take approximately 0.025 microseconds.
+

+ +

Limits

+

+Each node will have access to 128MB of RAM, and a time limit of 3 seconds.
+0 ≤ GetVote(i) ≤ 109 for all i with 0 ≤ i < N.
+

+ +

Small input

+

+Your solution will run on 10 nodes.
+1 ≤ GetN() ≤ 1000.
+

+ +

Large input

+

+Your solution will run on 100 nodes.
+1 ≤ GetN() ≤ 109.
+

diff --git a/distributed_codejam/2015_practice/sandwich/analysis.html b/distributed_codejam/2015_practice/sandwich/analysis.html new file mode 100644 index 00000000..aeb0a4b1 --- /dev/null +++ b/distributed_codejam/2015_practice/sandwich/analysis.html @@ -0,0 +1,11 @@ +

+Instead of finding the beginning and end with the best tastes, we will try to find the "middle" - a continuous piece of the sandwich with the worst taste possible, and subtract that from the whole sandwich. +

+ +

+We will split the sandwich into NumberOfNodes equal, continuous parts. For each part, we will calculate four numbers: the total taste of this part, the taste of the least-tasty continuous piece of this part, the taste of the least-tasty prefix of this part, and the taste of the least tasty suffix of this part. Once we have those 4 numbers from each node, we can send them to one selected master node. The least tasty continuous piece is either going to be contained fully in one of the parts (in which case it's simply the least-tasty continuous piece of this part), or it's going to be a suffix of some part, a number (possibly zero) of parts in the middle that are taken as a whole, and the prefix of some other part. Checking all the possibilities is easy to do in O(M2), and can be done in O(M). +

+ +

+Finding the four numbers for one part is also relatively easy. We process the part, piece by piece. One trick, to avoid processing the sandwich both ways, is to look for the least-tasty prefix and the most-tasty prefix; and then to get the least-tasty suffix we subtract the most-tasty prefix from the whole sandwich. +

diff --git a/distributed_codejam/2015_practice/sandwich/statement.html b/distributed_codejam/2015_practice/sandwich/statement.html new file mode 100644 index 00000000..87addfd2 --- /dev/null +++ b/distributed_codejam/2015_practice/sandwich/statement.html @@ -0,0 +1,39 @@ +

Problem

+ +

+Your friends made a brave attempt to beat the world record for making the longest sub sandwich ever. They failed or succeeded (it doesn't really matter), and proposed to you to eat as much of the sandwich as you want to.
+The sandwich is composed of N parts. Each part has a taste value for you, which might be positive (meaning you want to eat it) or negative (meaning you would prefer not to). Ideally, you would just eat the tasty parts, but it's rude to break out the middle of the sandwich. So, instead you want to eat some part from the beginning, and some part from the end; and to make the total taste of what you eat as large as possible. The total taste is the sum of tastes of all the parts you have eaten.
+Note: it's OK to eat the whole sandwich, or to eat nothing at all. Output the largest total taste of what you can eat.
+

+ +

Input

+

+The input library will be called "sandwich", see the sample inputs +below for examples in your language. It will define two methods: GetN(), which +will return the number of parts of the sandwich, and GetTaste(i), which will +return the taste of the ith part of the sandwich, for 0 ≤ i < N.
+A single call to GetTaste(i) will take approximately 0.01 microseconds.
+

+ +

Output

+

+Output one number: the maximum possible total taste of the parts you will eat.
+

+ +

Limits

+

+Each node will have access to 128MB of RAM, and a time limit of 3 seconds.
+-109 ≤ GetTaste(i) ≤ 109 for all i with 0 ≤ i < GetN().
+

+ +

Small input

+

+Your solution will run on 10 nodes.
+1 ≤ GetN() ≤ 1000.
+

+ +

Large input

+

+Your solution will run on 100 nodes.
+1 ≤ GetN() ≤ 5 × 108.
+

diff --git a/distributed_codejam/2015_practice/shhhh/analysis.html b/distributed_codejam/2015_practice/shhhh/analysis.html new file mode 100644 index 00000000..8642b525 --- /dev/null +++ b/distributed_codejam/2015_practice/shhhh/analysis.html @@ -0,0 +1,15 @@ +

+The problem is deceptively simple - find the distance between two nodes in a cycle. However, the input is too large to process it in one node. The natural hope would be to split the cycle into 100 equal parts, and have each node process one part. However, it's not obvious how to do that! +

+ +

+One idea that comes to mind is to randomize – have each node start from a random vertex, and travel forward. The first question is "when to stop"? Since you don't know how long do you have to travel, one possibility is for all the 100 nodes to know the set of their starting points (including vertices zero and one), and to calculate the distance to the next starting point. This allows carries an overhead of O(log M) for checking, at each vertex, whether it is one of the starting points. This would be good enough if all the intervals we traversed were of equal length. Unfortunately, they are not — on average, when randomly dividing a circle into K parts, the longest of those parts is of length log(K) / K of the whole circle. And an overhead of log(M)2 is very likely to be too high to fit within the time limit. +

+ +

+The way around this problem is to identify more of the "special" points than just N - for instance, M log(M) of them. Then, the average length of the traversed interval is O(log(M log M) / (M log M)) = O(1 / M). We have more points to check, but checking whether a point is a special point is still done in O(log (M log M)) = O(log M) operations. However, splitting these points equally between the nodes doesn't really solve the problem (because each node will get O(log M) intervals, and we're back to where we started). Instead, we need a system where a node that got a short interval can dynamically decide to pick up a new interval. +

+ +

+One way to handle this is to designate one node to be the master, and to handle distributing the intervals to calculate. This will also be the node that will output the answer. When any other node, going around the cycle from one special point, finds another special point, it will report the distance between them to the master. The master will then tell the reporting node what is the next special point it should start from (or that the calculation is finished, and it should exit). This way, if the longest interval is of length O(N/M), each node will do at most O(N / M) steps in the graph, and so the total complexity will be O(N log M / M). +

diff --git a/distributed_codejam/2015_practice/shhhh/statement.html b/distributed_codejam/2015_practice/shhhh/statement.html new file mode 100644 index 00000000..8e8ea893 --- /dev/null +++ b/distributed_codejam/2015_practice/shhhh/statement.html @@ -0,0 +1,48 @@ +

Problem

+

+You are at a reception. Along with a (huge) number of other people, you are seated at a round table, and listen to the speaker speak... and speak... and speak. You would like the speech to end, but that's not likely to happen, so you'd at least like to tell your friend — who sits somewhere else at the same table — how badly bored you are.
+However, it's rude to speak loudly. So, you'll whisper to one of your two neighbours, asking them to pass the message along. They'll then whisper to the other neighbour, and so on, until the message reaches your friend. You now need to decide which neighbour to choose so the distance traveled by the message is as short as possible, and how long is it going to go.
+

+ +

Input

+

+Each person at the table has a unique integer assigned, from 0 to N-1, +where N is the number of people at the table (including you and your +friend). You are assigned the identifier 0, and your friend is assigned the +identifier 1.
+The input library will be called "shhhh", see the sample inputs +below for examples in your language. +It will define three methods: GetN(), which +will return the number N of people at the table, +GetLeftNeighbour(i) for 0 ≤ i < N, which will return the +identifier of the left neighbour of the person with identifier i, and +GetRightNeighbour(i) for 0 ≤ i < N, which will return the +identifier of the right neighbour of the person with identifier i.
+A single call to GetLeftNeighbour(i) or GetRightNeighbour(i) will take approximately 0.015 microseconds.
+

+ +

Output

+

+Output one line, containing one word and one number, separated by a single space. The word should be "LEFT" if it is faster to pass the message to your left +neighbour, "RIGHT" if it is faster to pass the message to your right neighbour, +or "WHATEVER" if the distance is the same in both directions (quotes around all +words are for clarity only). The number should be the distance the message will +have to travel (that is, the number of people who will hear the message, including your friend, but not including you). +

+ +

Limits

+

+Each node will have access to 128MB of RAM, and a time limit of 4 seconds.
+

+ +

Small input

+

+Your solution will run on 10 nodes.
+2 ≤ GetN() ≤ 107.
+

+ +

Large input

+

+Your solution will run on 100 nodes.
+2 ≤ GetN() ≤ 109.
+

diff --git a/distributed_codejam/2016_finals/air_show/analysis.html b/distributed_codejam/2016_finals/air_show/analysis.html new file mode 100644 index 00000000..c22371cf --- /dev/null +++ b/distributed_codejam/2016_finals/air_show/analysis.html @@ -0,0 +1,141 @@ +

Air Show: Analysis

+

Math, the whole math and nothing but the math

+

+We are given two polylines in 3D, along with a time for each segment. Let + p0, p1, ..., pN to the list of transition +points for airplane 0, with corresponding times t0, t1, ..., +tN - 1, such that its i-th move takes ti time and flies from +point pi to point pi + 1. Let +q0, q1, ..., qN be the list of points +for airplane 1 with corresponding times u0, u1, ..., +uN - 1. Given a point pi, we want to find a corresponding +flight segment of the other airplane [qj, qj + 1] such that +while airplane 0 is at pi airplane 1 is flying that segment. Formally, +that means:
+u0 + u1 + ... + uj - 1 ≤ +t0 + t1 + ... + ti - 1 ≤ +u0 + u1 + ... + uj - 1 + uj.
+If we locate such j for each i, we can check whether pi should be +counted in the final result by checking that the distance between pi and the point +the point q corresponding to the other plane's location at the same time. We can +get q by linearly interpolating between qj and qj + 1. +
+t = t0 + t1 + ... + ti - 1 - + (u0 + u1 + ... + uj - 1)
+q = qj + + (qj + 1 - qj) t / uj.
+While the equations above imply some non-integer division, and the distance checking involves +square roots, it's not hard to manipulate the expressions by squaring and multiplying both sides +to get a pure integer arithmetic expression to check whether point i needs to be counted or not. +That expression does require 128 bit arithmetic, as suggested by the statement. +Of course, checking for a point of airplane 1 is symmetric. +

+

Single-machine solutions

+

+The layout in the previous section lets us write a pretty simple cubic solution: for every point, +check every segment of the other airplane until you find one that matches its timeframe. Since +the number of pairs is quadratic, and our equations above have a linear size sum (to get the +times), the more naive implementation of this approach takes cubic time. The following pseudocode +shows how to get the result for one airplane; the other one is symmetric. +

+result = 0
+for i = 1 to GetNumSegments() - 1:
+  for j = 0 to GetNumSegments() - 1:
+    sum_t = SumTimes(0, i)
+    sum_u = SumTimes(1, j)
+    if sum_u ≤ sum_t ≤ sum_u + GetTime(1, j):
+      if (check using the math above):
+        result = result + 1
+      break
+

+where SumTimes is the accumulated time for a given airplane up to a given point, as +follows: +

+SumTimes(a, x)
+  result = 0
+  for i = 0 to x - 1: result = result + GetTime(a, i)
+  return result
+

+It's easy to take this to quadratic by keeping a running total of times instead of calling +SumTimes all the time: +

+result = 0
+sum_t = 0
+for i = 1 to GetNumSegments() - 1:
+  sum_t = sum_t + GetTimes(0, i - 1)
+  sum_u = 0
+  for j = 0 to GetNumSegments() - 1:
+    sum_u = sum_u + GetTimes(0, j - 1)
+    if sum_u ≤ sum_t ≤ sum_u + GetTime(1, j):
+      if (check using the math above):
+        result = result + 1
+      break
+

+And furthermore, noticing that the times are increasing, which means the pairs (point, segment) +are sorted by both coordinates simultaneously, we can pick up the search for a segment where the +last point left off, making the algorithm run in linear time. +

+result = 0
+sum_t = 0
+last_j = 0
+for i = 1 to GetNumSegments() - 1:
+  sum_t = sum_t + GetTimes(0, i - 1)
+  sum_u = 0
+  for j = last_j to GetNumSegments() - 1:
+    sum_u = sum_u + GetTimes(0, j - 1)
+    if sum_u ≤ sum_t ≤ sum_u + GetTime(1, j):
+      if (check using the math above):
+        result = result + 1
+      last_j = j
+      break
+
+

A distributed solution

+

+Now that we have a linear time solution, all that is left is to distribute it. Giving each machine +a range of indices for the segments and points to consider won't work. Consider a case where + GetTime(0, 0) = GetTime(1, GetNumSegments() - 1) = 1012 +and all the other times are 1. You need to compare almost all points from airplane 0 with +the last segment of airplane 1. Of course, most of your machines won't know what to do because they +don't have that segment assigned. Since the pairing of points and segments is time-based, the +solution is giving each machine a range of times instead of indices. However, this can't be done +uniformly, otherwise, a test case with +GetTime(0, 0) = GetTime(1, 0) = 1012 +and all other times equal to 1 will timeout, because a single machine will be assigned almost all +point/segment pairs. +

+The solution is to adjust the time ranges to contain roughly the same amount of point/segment pairs. +Or, to simplify, to be able to bound the number of point/segment pairs that a single machine +processes by something close to GetNumSegments() / NumberOfNodes(). +Divide the segments of each plane into NumberOfNodes() / 2 parts of roughly equal +number of segments each +and give each machine one of those ranges to add up the times of all those segments. Then, send +all the partial results to a single machine to get aggregates. In this way, we can calculate +the aggregated time of a particular airplane up to point i for +GetNumSegments() / 2 different values of i that are (roughly) uniformly +distributed between 0 and GetNumSegments(). Two of the numbers are going to be equal +to the total time of the show, so discard those and use the remaining values to separate the total +time into smaller time windows. Note that each window is guaranteed to span no more than +2 GetNumSegments() / NumberOfNodes(), only twice the theoretical +optimum amount. As an implementation detail, we should also couple each window cutting point with +an airplane and an index. That way, we can get a small interval of indices to consider on each +machine: just start at the closest index on the left you have for each airplane. By construction, +this ensures you never iterate more than +2 GetNumSegments() / NumberOfNodes() indices per plane, since after that +many consecutive indices, there is surely a new cutting point. There are other implementation +details to fill in, but we leave those to the reader. +

+

A quick word on the Small dataset

+

+This problem had a Small that was different than usual. Solving it did not provide much of a +stepping stone, if any, toward solving the Large. Since all times are equal, we know that +airplane 0 will be at its i-th transition point at the same time airplane 1 is at its own +i-th transition point. That means +the answer for both airplanes is the same, and we just need to check whether each of those pairs +of points are closer than the minimum allowed distance. That is easier to do with pure-integer math +than what we outlined above for the general case, and more importantly, it makes things easier to +distribute. This is because the pathological cases (designed to disallow giving each machine a +range of segments or times) that we mentioned can't exist under the highly uniform conditions of +the Small dataset. So, to write a Small-only solution, just give each machine a roughly equal +subset of points from each plane, iterate through them, and count how many pairs are at a dangerous +distance, sending all partial results to a master machine to add up and print out the final result. +

diff --git a/distributed_codejam/2016_finals/air_show/statement.html b/distributed_codejam/2016_finals/air_show/statement.html new file mode 100644 index 00000000..d158666f --- /dev/null +++ b/distributed_codejam/2016_finals/air_show/statement.html @@ -0,0 +1,121 @@ +

Problem

+

Air Show

+

+We are planning an awesome air show. The most impressive act features two airplanes doing acrobatic +moves together in the air. The flight plan for each plane is already finalized. These plans may +cause the planes to get very close to each other, which is dangerous; you have been hired to assess +the extent of this risk. +

+A flight plan for one plane is a sequence of N timed segments. Within each segment a +plane flies straight at a constant speed. However, a plane may change direction and speed +dramatically when changing segments. Formally, the flight plan consists of an ordered list of +N + 1 3-dimensional points +P0, P1, ..., PN and an ordered list of N transition +times T0, T1, ..., TN - 1. As its i-th move, for each i in +{0, 1, ..., N - 1}, +the plane following this plan flies from Pi to Pi + 1 at a constant +speed in exactly Ti seconds. Since the planes are working together as a single act, +the sum of the times for each segment must be equal for both planes. +

+A transition instant is a time in between two consecutive moves (even when two consecutive moves +happen to require no change in speed or direction). That is, the N - 1 transition instants +for a plane with the flight plan above happen exactly at times +T0, T0 + T1, T0 + T1 + T2, ..., +T0 + T1 + ... + TN - 2. The starting and finishing times +are not considered transition instants. +

+Transition instants are the most dangerous times for pilots. Given a minimum safe distance +D, for each plane p, we ask you to count the number of transition instants +when p is at a distance strictly less than D from the other plane. You may consider +each plane to be a single point. If both planes occupy the same point at the same instant, no +collision occurs; the planes just pass through each other and continue. +

+The arithmetic for this problem for our official solution requires integers with more than 64 bits. +__int128 is available in our C++ installation and BigInteger is available +for Java. +

+ +

Input

+

+The input library is called "air_show"; see the sample inputs below for examples in your +language. It defines four methods: +

    +
  • GetSafeDistance(): +
      +
    • Takes no argument.
    • +
    • Returns a 64-bit integer: the minimum safe distance D.
    • +
    • Expect each call to take 0.5 microseconds.
    • +
    +
  • +
  • GetNumSegments(): +
      +
    • Takes no argument.
    • +
    • Returns a 64-bit integer: the number of segments N of each flight plan.
    • +
    • Expect each call to take 0.5 microseconds.
    • +
    +
  • +
  • GetTime(a, i): +
      +
    • Takes two 64-bit integers in the ranges 0 ≤ a < 2, + 0 ≤ i < GetNumSegments().
    • +
    • Returns a 64-bit integer: the time used for move i of plane a + (shown as Ti above). +
    • +
    • Expect each call to take 0.5 microseconds.
    • +
    +
  • +
  • GetPosition(a, i): +
      +
    • Takes two 64-bit integers in the ranges 0 ≤ a < 2, 0 ≤ + i ≤ GetNumSegments().
    • +
    • Returns a 64-bit integer: an encoding of point i of the flight plan of plane a + (shown as Pi above). Point (x, y, z), with each coordinate ranging between + 0 and 220-1, is encoded as the integer + x × 240 + y × 220 + z. +
    • +
    • Expect each call to take 2.5 microseconds.
    • +
    +
  • +
+

+ +

Output

+

+Output a single line with two integers r0 and r1 +separated by a single space, where ri is the number of dangerous moments +for plane i in which it is strictly closer than GetSafeDistance() to the other plane. +

+ +

Limits

+

+Number of nodes: 100. (Notice that the number of nodes is the same for both the Small and + Large datasets.)
+Time limit: 14 seconds.
+Memory limit per node: 512 MB.
+Maximum number of messages a single node can send: 1000.
+Maximum total size of messages a single node can send: 128 KB. (Notice that this is less than + usual.)
+1 ≤ GetSafeDistance() < 220.
+2 ≤ GetNumSegments() ≤ 108.
+

+ +

Small dataset

+

+0 ≤ GetPosition(a, i) < 240, for all a and i. (The x-coordinate of all points is +0, while coordinates y and z range between 0 and 220 - 1.)
+GetTime(a, i) = 1, for all a and i.
+

+ +

Large dataset

+

+0 ≤ GetPosition(a, i) < 260, for all a and i. (All coordinates range between 0 + and 220 - 1.)
+1 ≤ GetTime(a, i) < 109, for all a and i.
+ GetTime(0, 0) + GetTime(0, 1) + ... + GetTime(0, GetNumSegments() - 1) = +GetTime(1, 0) + GetTime(1, 1) + ... + GetTime(1, GetNumSegments() - 1). (The sum of all the values +of GetTime(a, _) for each a is the same.)
+GetTime(0, 0) + GetTime(0, 1) + ... + GetTime(0, GetNumSegments() - 1) ≤ 1012. + (The total time of a flight plan does not exceed 1012.)
+

+ +Note that the last 2 sample cases would not appear in the Small dataset. diff --git a/distributed_codejam/2016_finals/analysis_intro.html b/distributed_codejam/2016_finals/analysis_intro.html new file mode 100644 index 00000000..1872a78f --- /dev/null +++ b/distributed_codejam/2016_finals/analysis_intro.html @@ -0,0 +1,67 @@ +

+In our second-ever Distributed Code Jam final round, our contestants contended +with four problems. Encoded Sum was solvable by evenly dividing the work among nodes in a +couple stages, in a manner reminiscent of +Rearranging Crates +and Lisp++ from our +online rounds. +Air Show required some geometrical knowledge, and special planning on how to do the +distribution, as simple even division didn't cut it. In Toothpick Sculpture, the challenge +lay not in the distribution (since the input came pre-sharded), but in how to combine the results. +Gold was our first "interactive" distributed problem; it was not possible to read all of the input, +and contestants had to issue queries strategically based on information seen so far, and manage +resources dynamically. +Each problem presented a different distribution challenge, and many entailed more traditional +algorithmic challenges as well. +

+Early on in the 4-hour round, we saw many Small solutions trickle in, and +quite a few contestants picked up the Large dataset for Encoded Sum. As the +contest wore on, we saw a surprising number of solutions to Gold, a very +tough problem; Air Show and Toothpick Sculpture also garnered some attempts. +At the end of the round, our reigning champion bmerry towered above the field, +with all eight datasets submitted. DCJ judging is even more dramatic than GCJ +judging, though, and many Large solutions to Air Show, Toothpick Sculpture, +and Gold fell. Four contestants (bmerry, sevenkplus, fhlasek, and mnbvmar) +were left with 65 points (everything but the Larges for Air Show and Toothpick +Sculpture), and bmerry won via time penalty. There were many resubmissions of +Larges in the last two hours, and their effects on the time penalty proved +decisive! +

+Congratulations to bmerry for repeating as Distributed Code Jam champion; he +is undefeated in the Finals in the history of the contest! eatmore also +deserves a shout-out for being the only contestant to fully solve Air Show. +Toothpick Sculpture, which featured the same finicky artist Cody-Jamal from the +GCJ Finals' + +Gallery of Pillars problem, got no full solutions. +

+We'll see you in 2017 for another Distributed Code Jam. We have many +improvements planned, and we look forward to producing another set of +perplexing parallel programming problems! +

+
+

+Cast +

+Problem A (Testrun): Written and prepared by Alan Smithee. +

+Problem B (Encoded Sum): Written by Pablo Heiber. Prepared by Won-seok +Yoo. +

+Problem C (Air Show): Written and prepared by Pablo Heiber. +

+Problem D (Toothpick Sculpture): Written and prepared by Pablo Heiber. +

+Problem E (Gold): Written and prepared by Onufry Wojtaszczyk. +

+Solutions and other problem preparation and review by Ian Tullis and Yerzhan +Utkelbayev. +

+Analysis authors: +

+
    +
  • Encoded Sum: Won-seok Yoo
  • +
  • Air Show: Pablo Heiber
  • +
  • Toothpick Sculpture: Pablo Heiber
  • +
  • Gold: Yerzhan Utkelbayev
  • +
diff --git a/distributed_codejam/2016_finals/encoded_sum/analysis.html b/distributed_codejam/2016_finals/encoded_sum/analysis.html new file mode 100644 index 00000000..c125d873 --- /dev/null +++ b/distributed_codejam/2016_finals/encoded_sum/analysis.html @@ -0,0 +1,46 @@ +

Encoded Sum: Analysis

+

+The problem is about finding an bijective function from ten characters(A..J) to ten digits(0..9) which maximizes the sum of the two numbers created by replacing characters with digits. Trying to calculate the sum for all possible 10! combinations is impractical, and even if calculated, finding the best one can be problematic. In this analysis, we explain a greedy approach. +

+We have two strings X and Y, with fixed length L. Let's define Xi and Yi as the i-th character in X and Y, respectively, and the i-th pair Pi as (Xi, Yi). Also, for each character, let's define Ci,j as the number of appearances of character i in Pj and Ci as the concatenation of all Ci,js, in increasing order of j. Then Ci can be regarded as a number. Let Di be the digit assigned to character i. +

+If the two strings are ACEGIBCEIG and ADFHJDBHFJ (slightly different from the last example case given in the problem), the Cis are as follows: +

CA: 2000000000
+CB: 0000011000
+CC: 0100001000
+CD: 0100010000
+CE: 0010000100
+CF: 0010000010
+CG: 0001000001
+CH: 0001000100
+CI: 0000100010
+CJ: 0000100001
+

+If each Ci is a number, the sum can be calculated as the sum of Ci × Di for all i. Now, it is clear that assigning larger values Di to characters with larger Ci yields a larger result. So, the problem comes down to sorting the Cis in non-decreasing order and using that order to assign the digits. +

+To determine the order, it's necessary to start from the leftmost pair P0. For each pair Pi = (Xi, Yi), there are multiple cases. +

+[a] Xi ≠ Yi, +

    +
  1. If both Xi and Yi have not appeared before (let's call this "unexplored" status), we can assign them the largest remaining two digits. But, their relative final order can't be determined yet. Let's call these characters "entangled", as once one character is assigned its final digit (let's call this "fixed" status), the other character's digit is also decided. This status may be resolved when processing subsequent pairs.
  2. +
  3. If both Xi and Yi are entangled with each other, their order can't be resolved at this time. There is nothing new to do in this case.
  4. +
  5. If at least one of Xi or Yi is entangled and is not the case of (2), their status can be resolved. Resolve them by assigning Xi and/or Yi the largest digit assigned to their respective entangled pairs and mark all characters involved as fixed.
  6. +
  7. After (3), if both Xi and Yi are in the fixed status, there is nothing new to do in this case.
  8. +
  9. The only remaining case is that only one of Xi and Yi is fixed and the remaining one is unexplored. In this case, give the latter character the largest remaining digit and mark the character as fixed.
  10. +
+

+[b] Xi = Yi, +

    +
  1. If Xi is unexplored, give the character the largest remaining digit, and mark it as fixed.
  2. +
  3. If Xi is entangled, it can be resolved with the other entangled character, and mark them as fixed.
  4. +
  5. If Xi is fixed, there is nothing new to do in this case.
  6. +
+

+After processing all pairs, there can be still some characters in status other than "fixed". Each entangled pair can be resolved by assigning each member of the pair one of the possible digits arbitrarily, then unexplored characters can be assigned the remaining digits. The result doesn't change in this stage. +

+To distribute the process, the key observation is that only the first occurrence of each pair of characters is important, when processing from left to right, and subsequent occurrences won't change the assignment. So, the whole process is as follows: +

+

  • Each node finds unique pairs in a section of the input and sends them to the master node in their relative order of occurrence. Since we only need to send at most one instance of each of the 55 pairs, there are at most 55 pairs to send.
  • +
  • The master node decides the optimal digit order using the algorithm above on the at most 55 × 100 pairs.
  • +
  • The calculation of the result can be distributed to worker nodes again. After that, the master node gathers the pieces of the result, processes them modulo 109 + 7, and prints the sum.
  • +

    diff --git a/distributed_codejam/2016_finals/encoded_sum/statement.html b/distributed_codejam/2016_finals/encoded_sum/statement.html new file mode 100644 index 00000000..dbe758d4 --- /dev/null +++ b/distributed_codejam/2016_finals/encoded_sum/statement.html @@ -0,0 +1,93 @@ +

    Problem

    + +

    Encoded Sum

    + +

    +You are an archaeologist studying a lost civilization. This civilization used +a decimal (base 10) number system like our own: their numbers were made up of +digits from 0 through 9, with the most significant digit on the left. However, +this civilization used the ten letters A through J, in some order, to represent +the ten digits 0 through 9, in a one-to-one mapping. +

    + +

    +You have just discovered two scrolls from that civilization. Each contains a +number representing the population of one of the two regions of the +civilization. The numbers are of the same length. You do not know the +civilization's letter-to-digit mapping, but you know it is consistent across +the two documents. You want to know the maximal possible sum of those two +numbers. Please note that the resulting numbers can have leading zeros. +

    + +

    +Given these two scrolls, return the maximal possible sum. Since the output can +be a really big number, we only ask you to output the remainder of dividing +the result by the prime 109+7 (1000000007). +

    + +

    Input

    +

    +The input library is called "encoded_sum"; see the sample inputs below for +examples in your language. It defines three methods: + +

      + +
    • GetLength(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the length of the number on either scroll.
      • +
      • Expect each call to take 0.08 microseconds.
      • +
      +
    • +
    • GetScrollOne(i): +
        +
      • Takes a 64-bit number in the range 0 ≤ i < GetLength()
      • +
      • Returns an uppercase character (in the inclusive range A + through J) representing the i-th character in the first + scroll, counting starting from 0, starting from the left.
      • +
      • Expect each call to take 0.08 microseconds.
      • +
      +
    • +
    • GetScrollTwo(i): +
        +
      • Takes a 64-bit number in the range 0 ≤ i < GetLength()
      • +
      • Returns an uppercase character (in the inclusive range A + through J) representing the i-th character in the second + scroll, counting starting from 0, starting from the left.
      • +
      • Expect each call to take 0.08 microseconds.
      • +
      +
    • +
    +

    + +

    Output

    +

    +Output a single line with a single integer: the maximal sum modulo the prime +109+7 (1000000007), as described above.

    + +

    Limits

    +

    +Number of nodes: 100. (Notice that the number of nodes is the same for both the Small and Large + datasets.)
    +Time limit: 3 seconds.
    +Memory limit per node: 128 MB.
    +Maximum number of messages a single node can send: 1000.
    +Maximum total size of messages a single node can send: 8 MB.
    +1 ≤ GetLength() ≤ 109.
    +

    + +

    Small dataset

    +

    +GetScrollOne(i) is either uppercase A or uppercase +B, for all i.
    +GetScrollTwo(i) is either uppercase A or uppercase +B, for all i.
    +

    + +

    Large dataset

    +

    +GetScrollOne(i) is an uppercase letter between A and J, inclusive, + for all i.
    +GetScrollTwo(i) is an uppercase letter between A and J, inclusive, + for all i.
    +

    diff --git a/distributed_codejam/2016_finals/gold/analysis.html b/distributed_codejam/2016_finals/gold/analysis.html new file mode 100644 index 00000000..cd34df9c --- /dev/null +++ b/distributed_codejam/2016_finals/gold/analysis.html @@ -0,0 +1,70 @@ +

    Gold: Analysis

    +

    A binary search approach

    +

    +Let's try to solve the following subproblem: given two integers L and R (1 ≤ L ≤ R ≤ +RoadLength()), standing for the left and right ends of an interval, determine whether +there is at least one nugget at a position inside the interval [L, R], and if there is, find the +position of one of them. +

    +If one or both of L and R are X (that is, a nugget), we are done. If we find that one +or both are =, we can just move that side of the interval one position toward the +inside. Since there are never two =s in a row, that does not affect the overall +complexity. +

    +If L is > and R is <, it is clear that there is at least one nugget +in the interval. In this case, we can easily find a nugget by binary searching for its position. +Consider the position M = (L + R) / 2. If there is not a nugget at that position, at least one of +[L, M] or [M, R] has at least one nugget (and we know which direction(s) to look in), and so on, +recursively. The complexity is O(log(R - L + 1)). +

    +If L is not >, let's try to find the first > symbol in the interval. +If L is <, let's jump to the position L + 2, because we can be sure that there is +no nugget at L + 1 (otherwise, L would have been > or =). If L + 2 is +<, we can jump to L + 4. (Remember that if we encounter =, +we move one position towards the inside of the interval.) Generally, if we are at position L + p and +we see <, it means that there is no nugget in the interval [L, L+ p + p - 1], so we +can jump to position L + 2 * p, and proceed in this way, doubling our jump length each time, until +we find > or a nugget or reach +R (which means there is no nugget inside the interval). +

    +A symmetric strategy works for finding the first < starting from R and moving left. +So, combining all of the above, for any interval, we can determine whether there is at least one +nugget inside the interval, and the position of one nugget if so, in O(log(R - L + 1)) time. +

    +

    A Small solution

    +

    +Using the above algorithm, we can write and distribute a naive solution. We divide the total +RoadLength() into NumberOfNodes() intervals and have each node find the +XOR of all nuggets in its interval. +

    +On each node, we can keep a queue of intervals that initially holds [L, R]. If we find a nugget at +position N inside the interval [L, R], we should check the two intervals [L, N - 1] and [N + 1, R]. +For each nugget, 2 intervals will be added, so at most 2 * (number of nuggets actually present in +[L, R]) + 1 intervals will be examined. The maximum worst-case size of the queue is +NumberOfNuggets() + 1. The complexity of such a solution for a single node is +O(NumberOfNuggets() * log(RoadLength())). This solution passes the Small, +since the nuggets are approximately evenly distributed; the distribution reduces the complexity by +a factor close to NumberOfNodes(). However, this will be too slow for the Large +dataset, because, for example, the interval assigned to one node might contain all the nuggets. +

    +

    A Large solution

    +

    +Let's introduce the operation Reduce-Road-K which reduces the road at least K times. +Every node will process its interval and check whether there are at least +NumberOfNuggets() / NumberOfNodes() * K nuggets; it will return a special +value (meaning that the interval had too many nuggets) if so, or it will return the XOR of all +discovered nuggets otherwise. +

    +By the pigeonhole principle, at most NumberOfNodes() / K nodes will return this special +value for their corresponding intervals. So the total length we must examine is reduced +to at most RoadLength() / K after one operation of Reduce-Road-K. The intervals +that had too many nuggets, we must re-examine. A master node redistributes this remaining blocks +into the nodes again, for another pass, until all gold has been discovered. We need at most +logK(RoadLength()) steps of this process. +

    +The complexity of that deterministic solution for one node is +O(logK(RoadLength()) * NumberOfNuggets() +/ NumberOfNodes() * K * log(RoadLength())). +It is up to the solver to choose a good value of K; we got good results with K = 3. +

    + diff --git a/distributed_codejam/2016_finals/gold/statement.html b/distributed_codejam/2016_finals/gold/statement.html new file mode 100644 index 00000000..59b90018 --- /dev/null +++ b/distributed_codejam/2016_finals/gold/statement.html @@ -0,0 +1,102 @@ +

    Problem

    +

    +A long, long time ago, on an east-west road in southeastern Asia, an ancient +emperor was fleeing from the ruins of his fallen city, carrying a sack full of +his gold. At times, he glanced back over his shoulder and saw pursuers chasing +him, so he threw out a nugget of gold from his sack to the roadside, hoping +to lighten his load and provide a distraction. The story continues, but what +happened later is less important - the important part is that there is gold +lying by the roadside to be picked up! +

    + +

    +So, you took your trusty metal detector, and went to search for gold on the +road. You have a really fast car, but the detector itself is somewhat slow to +set up and operate, and also a bit inaccurate - it can only tell you in which +direction the nearest nugget of gold is, but not how far. Also, your car cannot +handle off-road driving, so you cannot triangulate; you will just need to +search a bit longer. +

    + +

    +We will represent the road as a straight line of length L. There +will be N nuggets of gold on the road, at integer positions, no more +than one nugget at each position. You will be able to set up the detector at +integer positions on the road. After setting up, the detector will provide one +of the four possible answers: +

      +
    • The nearest nugget is to the east (towards decreasing position + numbers),
    • +
    • The nearest nugget is to the west (towards increasing position + numbers),
    • +
    • The nearest nugget to the west and to the east are equally distant, + or
    • +
    • There is a nugget at this position.
    • +
    +

    + +

    Input

    +

    +The input library will be called "gold", see the sample inputs +below for examples in your language. +It will define three methods: +

      +
    • NumberOfNuggets(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of nuggets on the road.
      • +
      • Expect each call to take 0.2 microseconds.
      • +
      +
    • +
    • RoadLength(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of positions on the road.
      • +
      • Expect each call to take 0.2 microseconds.
      • +
      +
    • +
    • Search(i): +
        +
      • Takes one 64-bit integer argument in the range 0 ≤ i < + RoadLength().
      • +
      • Returns a character describing the output of the metal detector: + < if the nearest nugget is to the east, + > if it is to the west, = if the nearest + nuggest to the east and west are equally distant, or X if + there is a nugget as position i.
      • +
      • Expect each call to take 0.2 microseconds.
      • +
      +
    • +
    + +

    Output

    +

    +As printing all the nugget positions would require a lot of printing, you +should output one number - the bitwise XOR of the positions of all the nugget +positions - as a proof you found them all. +

    + +

    Limits

    +

    +Number of nodes: 100. (Notice that the number of nodes is the same for both the Small and Large + datasets.)
    +Time limit: 15 seconds. (There is a 10 second overhead of initializing the test data that is + not counted against this limit, so each reported time is 10 seconds more than the time your + solution executed, up to a maximum of 25.)
    +Memory limit per node: 512 MB.
    +Maximum number of messages a single node can send: 5000.
    +Maximum total size of messages a single node can send: 8 MB.
    +1 ≤ NumberOfNuggets() ≤ 107.
    +1 ≤ RoadLength() ≤ 1011.
    +

    + +

    Small input

    +

    +The positions of the nuggets will be generated using a pseudorandom number +generator such that the probability of any subset of positions being chosen is the same.
    +

    + +

    Large input

    +

    +No additional limits. +

    diff --git a/distributed_codejam/2016_finals/toothpick_sculpture/analysis.html b/distributed_codejam/2016_finals/toothpick_sculpture/analysis.html new file mode 100644 index 00000000..14ac7f80 --- /dev/null +++ b/distributed_codejam/2016_finals/toothpick_sculpture/analysis.html @@ -0,0 +1,109 @@ +

    Toothpick Sculpture: Analysis

    +

    +The statement asks us to select a subset of tootkpicks to stabilize. The subset must +be such that, for any pair of touching toothpicks, at least one of them is in this subset. This +means that in the set of toothpicks that are not chosen, no two toothpicks touch, making it an +independent set. +Moreover, minimizing the cost of the chosen set is equivalent to maximizing the cost of the +not-chosen set, making the problem equivalent to finding a maximum independent set, which is an +NP-complete problem in general graphs. This brings us to one major point of the problem: +the maximum independent set problem can be solved for a tree via a simple recursion, and of course, +a similar recursion solves the complementary problem that we are given. +

    A simple single-machine solution

    +

    +Like many tree problems, this one can be solved via a divide and conquer strategy. +For each subtree, we want two pieces of data: the minimum cost of a set including the root, and the +minimum cost of a set with no additional restrictions. +We need both of these +pieces of data because when combining information from the children of a given node, we +need to know if we are in a position to skip the cost of the root or not. This is better explained +by the following recursive pseudo-code: +

    +Solve(subtree):
    +  if subtree is empty, return (0, 0)
    +  result_child_1 = Solve(child1(subtree))
    +  result_child_2 = Solve(child2(subtree))
    +  best_with_root = Cost(root(subtree)) + result_child_1.best + result_child_2.best
    +  best_without_root = result_child_1.best_with_root + result_child_2.best_with_root
    +  best = min(best_with_root, best_without_root)
    +  return (best, best_with_root)
    +

    +Notice above that if we decide to pay the cost for the current root, we can just take the best set +for each child. However, if we decide to skip the cost of the current root, +we need to require a set from each child that contains that child. +

    +The first problem is that the size of the tree makes recursion impossible due to stack +limitations. You can just write your own stack to simulate the recursion, but a simpler and faster +way is to do a topological sort of +the nodes, and then solve them in reverse order, from leaves to root. +

    +

    Distributing the solution: Small dataset

    +

    +A single machine solution gets you nothing in the finals, because even Smalls are big. In this case, +the Small guaranteed the input is a path, which makes the solution be essentially the same +recursion described above, but with one of the children of each node missing. +However, this is a lot easier to distribute: first, +partition the path, then, have each machine compute the results of a single part of the path +relative to two variables: the results coming from the following piece of the +path that went to another machine. Since results are always linear in terms of the variables, +you can keep computing the results without the size of the representation growing too much. +

    +Partitioning paths is simple: just choosing k out of n random points yields an +expected maximum piece size of n log k / k, which is pretty close to the +optimal n / k. However, in this problem the input is nicely prepartitioned: +choosing toothpicks 0 through 999 as piece starting yields a perfect partition. This is more pieces +than there are machines, but we can have each machine compute several of them, either by assigning +each machine a range of pieces, or by having a master assign a new piece each time a worker +machine finishes the previous one. +

    +

    Distributing the solution: Large dataset

    +

    +The Large dataset, however, is a different monster to battle. We pre-partitioned the input for a +reason: partitioning trees well is hard (and, incidentally, one main focus of the +shipping problem from +the 2015 Distributed Code Jam Finals). For instance, if you have a complete binary tree and you +pick random nodes +to act as roots of each of the pieces, you are highly likely to end up with one giant piece +attached to the actual root and many tiny pieces, because in a complete binary tree, most of the +nodes are close to the leaves and far from the root, where big pieces should start. +Fortunately, the statement provides us with a nice partition. The question is what to do with it! +

    +The usual way to approach a problem like this is to have each piece report some result to the +master and have the master combine them into a final result. In this case, each piece is adjacent +to possibly many +other pieces, so we can't use the trick we used for the Small, since the value of the expression +representing the result grows exponentially with the number p of "pending" pieces (which +in a path, is just 1). Think about it this way: for each adjacent subtree, we have two results to +choose from, so we have 2p ways to combine its results. +

    +One way to deal with this issue is to stop the exponential explosion before it starts: proceed as +in the Small, representing results as linear expressions based on two variables identified with the +two results coming from a single piece. In some cases you will get integers as results, +because there is no other piece starting below in the current subtree. As soon as you need to +combine two linear expressions into a quadratic one, don't. Instead, create a new variable in the +current node and remember how to calculate it. Now, the result is back to linear. Since there are +1000 pieces, there are 999 merges and thus 999 virtual variables. When we finish everything, every +variable can be calculated from at most two others, so we have a new binary tree over which we can +calculate a similar recursion. +

    +There are two ways to implement this idea. One is to have a master assign both subtrees and virtual +variables and have the workers report back to it. Another is to assign each worker a range of +subtrees or pieces and a range of values to use for its necessary virtual variable calculation. +As a particular implementation detail, we can represent the result from each subtree with five +integers i, a, b, c, d representing best and best_with_root +above as follows: +

    +if i = -1: best = a = b, best_with_root = c = d
    +else: best = min(a + best_i, b + best_with_root_i),
    +      best_with_root = min(c + best_i, d + best_with_root_i)
    +

    +where best_i and best_with_root_i represent the specific result from +the piece or virtual variable i. +Notice that i = -1 represents a result with no pending variable. +The result is always one of the +possibilities from the pending result plus some overhead for the rest of the set. The integers +a, b, c, d represent that overhead in each of the four combinations. When we have to +combine two of these results with i ≠ -1, we just create a new variable and remember +which two results we have to use to calculate its value, once we have all the information to +calculate them. +

    diff --git a/distributed_codejam/2016_finals/toothpick_sculpture/statement.html b/distributed_codejam/2016_finals/toothpick_sculpture/statement.html new file mode 100644 index 00000000..5110a05d --- /dev/null +++ b/distributed_codejam/2016_finals/toothpick_sculpture/statement.html @@ -0,0 +1,127 @@ +

    Problem

    +

    Toothpick Sculpture

    +

    +Your friend Cody-Jamal is an artist. He is working on a new sculpture concept made of toothpicks. +Each toothpick has two ends. Each sculpture features a single foundational toothpick +resting horizontally on the ground. Every other toothpick rests horizontally with its midpoint +on one of the ends of exactly one other toothpick. This means each toothpick can have zero, one +or two other toothpicks resting on it, forming a binary tree of toothpicks, with the foundational +toothpick as the root. +Toothpicks do not touch otherwise. Let +t1 be any toothpick, and let t1 rest on t2, +t2 rest on t3, and so on; there is an eventual +tk that is the foundational toothpick. +

    +For instance, in the following picture you can see a sculpture with 5 toothpicks. The black +toothpick is the foundational toothpick, resting on the ground. The green and blue toothpicks are +resting on the black toothpick, while the red and yellow toothpicks are resting on the blue one. +
    + +

    +Cody-Jamal made exactly 1000 sculptures, each using exactly N toothpicks, that were +exhibited for 1000 days in the largest 1000 cities in the world. As the grand finale, he is +planning on placing the foundational toothpicks of 999 of his sculptures on open ends of toothpicks +of other sculptures, effectively creating a larger version of a sculpture with the same concept, +with 1000 × N total toothpicks. He plans on displaying it by placing the foundational +toothpick of the structure on top of the Burj Khalifa in Dubai, the tallest building in the world. +

    +Cody-Jamal is enthusiastically telling you his new plan, when your +engineering mind notices that the high winds at that altitude will make the structure unstable, so +you suggest gluing touching toothpicks together. Cody-Jamal's artistic vision, however, disagrees. +The fragility of the construction is part of the conceptual appeal of the piece, he says. +You two decide to compromise and use almost invisible carbon nanotube columns to support some +toothpicks. +

    +A toothpick (foundational or not) is individually stable if and only if it is +supported by a carbon nanotube column. Your math quickly shows that the +whole structure can be considered stable if and only if, for each toothpick t that is +not the foundational toothpick, either t is individually stable, or t rests on an +individually stable toothpick. +

    +Carbon nanotube columns are expensive, though. The cost of the column required to stabilize each +toothpick may vary depending on several factors. You decide to write a computer program to assist +in choosing a set of toothpicks good enough to make the structure stable while minimizing the sum +of the cost of the carbon nanotubes required to stabilize each toothpick. +

    + +

    Input

    +

    +Toothpicks are numbered with integers between 0 and 1000N - 1, inclusive. Integers between +0 and 999 identify the foundational toothpicks of the original sculptures, and 0 is also the +foundational toothpick +of the large combined sculpture. The numbering of all of the other toothpicks is arbitrary. +By way of construction, if you consider a graph made of all toothpicks with two toothpicks +being adjacent if they touch each other, the N toothpicks corresponding to any original +sculpture form a connected subgraph. +

    +The input library is called "toothpick_sculpture"; see the sample inputs below for examples in your +language. It defines three methods: +

      +
    • GetNumToothpicks(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number N of toothpicks in each original sculpture.
      • +
      • Expect each call to take 0.06 microseconds.
      • +
      +
    • +
    • GetCost(i): +
        +
      • Takes a 64-bit integer in the range 0 ≤ i < 1000 × GetNumToothpicks().
      • +
      • Returns a 64-bit integer: the cost of stabilizing toothpick i.
      • +
      • Expect each call to take 0.06 microseconds.
      • +
      +
    • +
    • GetToothpickAtEnd(i, e): +
        +
      • Takes two 64-bit integers in the ranges 0 ≤ i < 1000 × GetNumToothpicks(), + 0 ≤ e < 2.
      • +
      • Returns a 64-bit integer: the id of the toothpick resting on end e of toothpick i in the + large sculpture, or -1 if there is no toothpick resting there.
      • +
      • Expect each call to take 0.06 microseconds.
      • +
      +
    • +
    +

    + +

    Output

    +

    +Output a single line with a single integer: the minimum sum of the cost to build the necessary +carbon nanotubes and make the structure stable. +

    +Recursive solutions, beware! The process stack size is limited to 8 MB and the JVM thread stack + size is limited to 1MB. Attempting to change this programatically will result in a Rule + Violation. +

    + +

    Limits

    +

    +Number of nodes: 100. (Notice that the number of nodes is the same for both the Small and Large + datasets.)
    +Time limit: 6 seconds.
    +Memory limit per node: 512 MB.
    +Maximum number of messages a single node can send: 5000.
    +Maximum total size of messages a single node can send: 8 MB.
    +1 ≤ GetNumToothpicks() ≤ 106.
    +1 ≤ GetCost(i) < 109, for all i.
    +-1 ≤ GetToothpickAtEnd(i, e) < GetNumToothpicks() × 1000, for all i, e.
    +GetToothpickAtEnd(i, e) ≠ 0, for all i, e.
    +Let G be the graph with toothpicks as nodes and two nodes connected if and only if +the corresponding toothpicks touch in the sculpture:
    +G is a connected tree.
    +Removing the edges between toothpicks 1 through 999 and the toothpicks they are resting on +results in exactly 1000 connected components of GetNumToothpicks() nodes each.
    +

    + +

    Small dataset

    +

    +GetToothpickAtEnd(i, 1) = -1, for all i.
    +

    + +

    Large dataset

    +

    +No additional limits. +

    + +

    +Note that the last sample case would not appear in the Small dataset. +

    diff --git a/distributed_codejam/2016_finals/toothpick_sculpture/toothpicks.png b/distributed_codejam/2016_finals/toothpick_sculpture/toothpicks.png new file mode 100644 index 0000000000000000000000000000000000000000..e03bcef77cb58ee0d723960cbaef5a72d3742b55 GIT binary patch literal 26101 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 literal 0 HcmV?d00001 diff --git a/distributed_codejam/2016_r1/analysis_intro.html b/distributed_codejam/2016_r1/analysis_intro.html new file mode 100644 index 00000000..96d21f01 --- /dev/null +++ b/distributed_codejam/2016_r1/analysis_intro.html @@ -0,0 +1,53 @@ +

    +This year's Distributed Code Jam Round 1 was a new addition to the beginning +of the DCJ track, so the problems were easier on the whole than last year's DCJ +Round 1 problems... but writing distributed code is inherently tricky, so it +was no cake walk! The round was open to all GCJ Round 2 qualifiers, and we had +over four times as many participants on the scoreboard as last year. With this +growth came a few bumps, and time constraints precluded some of the changes we +wanted to make for this year; we hope that most of you had a smooth experience, +and we will continue to improve and polish the contest. +

    +Oops presented some poorly written distributed code and asked contestants +to decipher and rewrite it. Remarkably Parallel Scenario was a callback +to GCJ Round 2's rock-paper-scissors problem, although this one took a different +angle and was more straightforward. For Crates, it was perhaps a bit tricky +even to come up with a single-node solution, and distributing it took some extra +work. Finally, The Only Winning Move was a somewhat experimental problem, +and the first DCJ problem that required linear communication; we intended a +MapReduce-style solution. It was worth a lot, and it was easy to miss it by +using too much memory (e.g. from a doubling vector), but as with some of our Qual Round D problems, it did not substantially influence advancement. About 50 contestants solved the Large. +

    +simonlindholm, one of our ten DCJ finalists from 2015, submitted an eighth correct +answer at the nifty time of 1:11:11, attaining the first perfect score with a total +penalty time of 1:15:11. simonlindholm was followed by tomconerly, eatmore, cgy4ever, +and 2015 DCJ champion bmerry. There were enough competitors that advancing was no sure +thing; provisionally, the cutoff is around 33 points (B-Large, C-Large, E-Small) plus a bit of speed. As usual, official emails will follow within a few days. +

    +We'll see our top 500 in two weeks for the second of our two online DCJ rounds on June 12. +

    +
    +

    +Cast +

    +Problem A (Oops): Written by Pablo Heiber and Onufry Wojtaszczyk. +

    +Problem B (Remarkably Parallel Scenario): Written by Ian Tullis. +

    +Problem C (Rearranging Crates): Written by Pablo Heiber. +

    +Problem D (The Only Winning Move): Written by Ian Tullis and Onufry +Wojtaszczyk. +

    +All problems prepared by Pablo Heiber, Ian Tullis, and Onufry Wojtaszczyk. +Solutions and other problem preparation and review by Shane Carr, Minh Doan, +Nathan Pinsker, and Yerzhan Utkelbayev. +

    +Analysis authors: +

    +
      +
    • Oops: Ian Tullis
    • +
    • Remarkably Parallel Scenario: Ian Tullis
    • +
    • Rearranging Crates: Pablo Heiber
    • +
    • The Only Winning Move: Ian Tullis
    • +
    diff --git a/distributed_codejam/2016_r1/crates/analysis.html b/distributed_codejam/2016_r1/crates/analysis.html new file mode 100644 index 00000000..07a5f6f6 --- /dev/null +++ b/distributed_codejam/2016_r1/crates/analysis.html @@ -0,0 +1,128 @@ +

    How to solve the problem non-distributedly

    +

    +In this problem we are given an arrangement of crate stacks and we have to move the crates around +to make them evenly distributed. Let us consider these starting stack heights: +4, 3, 1, 3. +

    +There are a total of 11 crates for 4 stacks. By the definition of the statement, we need 3 crates in +each of the leftmost 3 stacks and 2 crates in the rightmost stack. +

    + + + +
    starting height4313
    target height3332
    difference10-21

    +As you can see in the difference row, some stacks will need adjustment. +

    +Notice that the movement of crates is commutative. That is, if we have a set of instructions of the +form "move from crate i to crate j", any order of executing them produces the same result. So, we +can consider the moves in increasing order of the index of the stack from which crates are removed. +

    +It is never optimal to move a crate from stack i to stack j and later a crate +from stack j to stack i, since we can achieve a strictly better solution by avoiding both moves. +Since we can only move crates to adjacent stacks, we should never move more than the 1 excess crate +out of the leftmost stack in the example above. The only place to move the extra crate is the +neighboring stack to the right, so we just do that, getting to the following state: +

    + + + +
    starting height3413
    target height3332
    difference01-21

    +Now we have to process the second stack, and we cannot move any crates back to the first! So, we +are in a similar situation as before, and with a single additional move we get to: +

    + + + +
    starting height3323
    target height3332
    difference00-11

    +Now we get to a stack that has a negative difference. That means we have to bring crates to it. +However, we can just do that greedily, as everything to the left is already balanced. We just bring +crates from our right neighbor. Even if that leaves the right neighbor with too few crates, we will +fix that in the next step. One more move yields: +

    + + + +
    starting height3332
    target height3332
    difference0000

    +Now we are at the last stack, and of course this has to be balanced, as we have no further stack to +the right to move crates to or bring crates from. With 3 moves, balanced the stacks in this test +case. +

    +Generalizing what we did in the previous example, we can always greedily choose to balance each +stack by moving crates to or bringing crates from its right neighbor, as long as we process stacks +from left to right. Moreover, we can even "owe" a stack crates: for instance, if our initial state +was 0 0 3, our first greedy move would be to move one crate from the middle stack to +the left, getting to 1 -1 3. This poses no problem: the -1 represents a debt +that will eventually be paid. In this case, processing the second stack would require two moves +to get 2 crates from its right neighbor and get to 1 1 1. As we observed at the +beginning, the move order is interchangeable, and it is easy to see that there is always an order +that leaves no stack in negative size. Notice that, as before, there is no other way for the +stack on the left to get its missing crate than to eventually get it from the middle stack, so +that decision must be correct. +

    +To formalize the solution: it should take exactly +Moves(i) = |sum of (DesiredHeight(j) - GetStackHeight(j)) over all j ≤ i| moves to balance the +stack in position i. Notice that the summation accounts for whatever changes we did +when processing stacks to its left. +This is pretty straightforward to do in linear time non-distributed code: +

    +sum = 0
    +result = 0
    +for i in range:
    +  sum = sum + (GetStackHeight(i) - DesiredHeight(i))
    +  result = result + |sum|
    +

    +where DesiredHeight is pretty easy to implement in constant time. +

    +

    How to distribute the solution

    +

    +As is usually the case in Distributed Code Jam, linear time is just not fast enough for the +Large dataset. Since the result is just the sum of something for each index i, can +we just distribute that sum in the same way that our +example codes +distribute a summation of the input, by making each node sum operate over a particular interval of +indices? +

    +The answer, unfortunately, is no. And the reason is that the value of sum in the +pseudocode above depends on all previous indices, not just those on a local range, so distributing +it naively will make each node wait on its left neighbor, thus removing any speedup due to +parallelization. To avoid the wait, we could make each node calculate its own starting value for +sum, but that doesn't really work either. +For instance, if some node processes indices i between 101 and 200, we would need +sum to start with the sum of all indices up to 100. For a node that processes +indices between 201 and 300, we would need the sum for all indices up to 200. You can +easily see that the node that processes the right-most end of the interval needs to know the sum of +all other indices, which requires reading the entire input. +

    +However, we can do an earlier pass in which we calculate the starting value of sum for +each node, so that a node only needs to read the part of the input it is assigned to +process. Since sum is just the cumulative function of +GetStackHeight(i) - DesiredHeight(i), we can make each node sum over the +is that it is assigned, send that sum to the master, and make the master calculate the +cumulative sums and send each node the one it needs. Of course, we need to do an +even earlier pass to get DesiredHeight(i) in the first place, since it depends +on the sum of all of the stack heights. Pseudo-code follows: +

    +On every node:
    +  compute sum of heights of all stacks in my_range
    +  send sum to master
    +Only on master:
    +  compute the (at most two) DesiredHeight value(s)
    +  send to all nodes
    +On every node:
    +  sum = 0
    +  result = 0
    +  for i in my_range:
    +    sum = sum + (GetStackHeight(i) - DesiredHeight(i))
    +  send sum to master
    +Only on master:
    +  cumulative_sum = 0
    +  for each node, in increasing order of assigned intervals:
    +    receive sum and store in node_sum
    +    send the node cumulative_sum as its starting value for sum
    +    cumulative_sum += node_sum
    +On every node:
    +  receive cumulative_sum and store in sum
    +  do the single node solution and send result to master
    +Only on master:
    +  receive result from every node and sum them to obtain the final result
    +
    diff --git a/distributed_codejam/2016_r1/crates/statement.html b/distributed_codejam/2016_r1/crates/statement.html new file mode 100644 index 00000000..43391bf8 --- /dev/null +++ b/distributed_codejam/2016_r1/crates/statement.html @@ -0,0 +1,94 @@ +

    Problem

    +

    +You are the manager of the warehouse of the largest wharf in the area. The +warehouse is tall and long, but narrow, so you have created a single row of +stacks of crates. Unfortunately, the ship unloaders are usually sloppy and in a +rush, so the number of crates can vary wildly between stacks, making them look +like an uneven skyline of buildings. +

    +The District Crate Judge (DCJ) is visiting the facilities tomorrow, and you +want to get a perfect score in her assessment. You decided to use an old crane +to rearrange the crates to make them into a neatly organized crate wall that +will impress the DCJ, but this task may take a long time! Your crane can only +handle one crate at a time. Moreover, the crane's wheels are not strong enough +to move if the crane is handling a crate, so the crane can only drop off a +crate on a stack adjacent to the one the crate came from. +

    +The crane starts off at the leftmost stack, without any crates. The crane can +carry out only the following sequence of actions, which we will call a +move: +

    +
      +
    1. Position the crane at any stack with at least one crate. This may be the +stack the crane is already at.
    2. +
    3. Pick up the top crate from the stack the crane is at. (Even if this causes +the stack to have zero crates, it still counts as a stack.)
    4. +
    5. Put that crate on top of a stack directly adjacent to the stack the +crane is at. +
    +

    +Notice that if you want to move a crate two stacks down to the right, for +example, this will take two moves. +

    +You must use some number of moves to transfer crates around so that all stacks +are as even as possible, with all the excess distributed among the leftmost +possible stacks. If there are N stacks and a total of C crates, +you want the C%N leftmost stacks to have ⌈ +C/N ⌉ crates each and the rest of the stacks to have +⌊ C/N ⌋ crates each. For instance, if there are 3 +stacks and a total of 8 crates, you want the stack heights to be 3, 3, 2, from +left to right. +

    +What is the minimum number of moves you need to use to balance the stacks as +specified above? Since the result can be really big, output it modulo +109+7 (1000000007). +

    + +

    Input

    +

    +The input library is called "crates"; see the sample inputs below for examples in your +language. It defines two methods: + + + + + + + + + + + + + +
    Method name and parametersParameter limitsReturnsApproximate time for a single call
    GetNumStacks()the number of stacks0.12 microseconds
    GetStackHeight(i)1 ≤ i ≤ GetNumStacks()the starting number of crates in stack i, counting from left to right +and starting from 10.12 microseconds
    +

    + +

    Output

    +

    +Output a single line with a single integer, the remainder of dividing the +minimum number of moves needed to balance the stacks by 109+7 +(1000000007). +

    + +

    Limits

    +

    +Time limit: 6 seconds.
    +Memory limit per node: 128 MB.
    +Maximum number of messages a single node can send: 5000.
    +Maximum total size of messages a single node can send: 8 MB.
    +1 ≤ GetStackHeight(i) ≤ 109, for all i.
    +

    + +

    Small dataset

    +

    +Number of nodes: 10.
    +1 ≤ GetNumStacks() ≤ 106. +

    + +

    Large dataset

    +

    +Number of nodes: 100.
    +1 ≤ GetNumStacks() ≤ 109. +

    diff --git a/distributed_codejam/2016_r1/oops/analysis.html b/distributed_codejam/2016_r1/oops/analysis.html new file mode 100644 index 00000000..d4e581cb --- /dev/null +++ b/distributed_codejam/2016_r1/oops/analysis.html @@ -0,0 +1,85 @@ +

    Oops: Analysis

    +

    Deducing the original problem

    +

    +Now that we think about it, our sample code might not have been as efficient as +it could have been! Let's look at the C++ version: what is it doing? +

    +
      +
    • There is a mysterious GetNumber function that takes a single +input — a nonnegative number less than N — and returns a +number. So, notionally, there is a list of N numbers that the code reads +from.
    • +
    • On the k-th node, we examine all possible pairs (i, j) from this list, +but then we only process the ones for which j modulo NumberOfNodes +equals k.
    • +
    • When a node processes a pair (i, j), it finds the value i - j. It stores +the largest such value it has seen so far, and whenever it finds a larger one, +it updates its stored value and sends the new value to the master node.
    • +
    • The master node processes all incoming values, one node at a time, and +takes the largest of those values.
    • +
    • Since every pair (i, j) will be handled by one of the nodes, the code, +when run on all nodes, will find the largest difference between two numbers +in the list.
    • +
    +

    +So the original statement must have been something like "Given a list of +N numbers, what is the largest possible difference between them?" Yes, +that sounds about right. How did we forget that? +

    +

    The sample code's approach

    +

    +Now that we know what we are supposed to be doing, we can assess how well the +sample code does it: +

    +

      +
    • Each node iterates through all N2 combinations of +selections from N, even though it only acts on a fraction of them.
    • +
    • Whenever a node finds a difference that is larger than the largest +difference it has previously seen, it just can't wait to tell the master about +it, and immediately sends a message with that difference. This creates a lot of +traffic over the network.
    • +
    • The master must wait for a node to finish sending all of these messages +(followed by a DONE message), and then process all of them, before receiving +messages from the next node.
    • +
    +

    +This approach has an O(N2) searching phase, since all nodes +operate in parallel. Note that this running time of this phase does not +improve if we add more nodes. +

    +There are cases in which each node produces a large number of messages; for +example, in the case in which the i-th element of N is i, each node +will produce N messages. In a case like this, the master has a total of +NumberOfNodes * N messages to handle in the processing phase. It +is a bad sign that the only part of our distributed solution that depends +on the number of nodes gets worse with more nodes! +

    +Since NumberOfNodes << N in general, the searching phase +dominates the processing phase, and the overall running time is +O(N2) overall. This is rather embarrassing. We only vaguely +remember writing this code after a long night at our favorite dance club, but +that is no excuse! Now that we have our wits about us, how can we do better? +

    +

    A better approach

    +

    +

      +
    • Observe that the largest difference between two numbers in our list will be +between the largest and smallest numbers in the list. This reduces the problem +to finding those largest and smallest values.
    • +
    • We can give each node an equal portion of the list. Each node can look +through its portion, find the locally largest and smallest numbers, and send +those to the master.
    • +
    • The master can receive all of the (locally largest, locally smallest) pairs +and find the globally largest and smallest numbers. Then it can compute and +return the difference between those extremes.
    • +
    +

    +In this approach, we make a single parallelized pass over the data, so the +running time of the search phase is O(N / NumberOfNodes). The +processing phase has an O(NumberOfNodes) running time. Again, since +NumberOfNodes << N in general, and the master only has to +process two values from each node, the search phase dominates and the overall +running time is O(N / NumberOfNodes) — much better! +Note that a single-node version of this method is O(N), which is still +better than our sample code. +

    diff --git a/distributed_codejam/2016_r1/oops/statement.html b/distributed_codejam/2016_r1/oops/statement.html new file mode 100644 index 00000000..3da67d56 --- /dev/null +++ b/distributed_codejam/2016_r1/oops/statement.html @@ -0,0 +1,159 @@ +

    Problem

    +

    +Oops. +

    + +

    +The team preparing the Distributed Code Jam made a mess and needs your help. The +statement and solutions for this problem were lost minutes before the contest, +and the only thing that could be recovered was a set of correct but really slow +(and misguided) solutions that were supposed to time out, one per language. +Fortunately, we still have the test data. Can you reconstruct the statement and +solve the problem properly based on the recovered slow solutions? +

    +

    Notice that in this problem 20 nodes are used to run both the Small and the Large +datasets, which is not the usual number for Distributed Code Jam problems. 20 nodes were also used +to run the solutions and produce the answers for the examples.

    +

    + +

    +The C++ solution: +

    +#include <message.h>
    +#include <stdio.h>
    +#include "oops.h"
    +
    +#define MASTER_NODE 7
    +#define DONE -1
    +
    +int main() {
    +  long long N = GetN();
    +  long long nodes = NumberOfNodes();
    +  long long my_id = MyNodeId();
    +  long long best_so_far = 0LL;
    +  for (long long i = 0; i < N; ++i) {
    +    for (long long j = 0; j < N; ++j) {
    +      if (j % nodes == my_id) {
    +        long long candidate = GetNumber(i) - GetNumber(j);
    +        if (candidate > best_so_far) {
    +          best_so_far = candidate;
    +          PutLL(MASTER_NODE, candidate);
    +          Send(MASTER_NODE);
    +        }
    +      }
    +    }
    +  }
    +  PutLL(MASTER_NODE, DONE);
    +  Send(MASTER_NODE);
    +
    +  if (my_id == MASTER_NODE) {
    +    long long global_best_so_far = 0;
    +    for (int node = 0; node < nodes; ++node) {
    +      long long received_candidate = 0;
    +      while (true) {
    +        Receive(node);
    +        received_candidate = GetLL(node);
    +        if (received_candidate == DONE) {
    +          break;
    +        }
    +        if (received_candidate > global_best_so_far) {
    +          global_best_so_far = received_candidate;
    +        }
    +      }
    +    }
    +    printf("%lld\n", global_best_so_far);
    +  }
    +  return 0;
    +}
    +

    +

    +The Java solution: +

    +public class Main {
    +  static int MASTER_NODE = 7;
    +  static int DONE = -1;
    +
    +  public static void main(String[] args) {
    +    long N = oops.GetN();
    +    long nodes = message.NumberOfNodes();
    +    long my_id = message.MyNodeId();
    +    long best_so_far = 0L;
    +    for (long i = 0; i < N; ++i) {
    +      for (long j = 0; j < N; ++j) {
    +        if (j % nodes == my_id) {
    +          long candidate = oops.GetNumber(i) - oops.GetNumber(j);
    +          if (candidate > best_so_far) {
    +            best_so_far = candidate;
    +            message.PutLL(MASTER_NODE, candidate);
    +            message.Send(MASTER_NODE);
    +          }
    +        }
    +      }
    +    }
    +    message.PutLL(MASTER_NODE, DONE);
    +    message.Send(MASTER_NODE);
    +
    +    if (my_id == MASTER_NODE) {
    +      long global_best_so_far = 0;
    +      for (int node = 0; node < nodes; ++node) {
    +        long received_candidate = 0;
    +        while (true) {
    +          message.Receive(node);
    +          received_candidate = message.GetLL(node);
    +          if (received_candidate == DONE) {
    +            break;
    +          }
    +          if (received_candidate > global_best_so_far) {
    +            global_best_so_far = received_candidate;
    +          }
    +        }
    +      }
    +      System.out.println(global_best_so_far);
    +    }
    +  }
    +}
    +
    +

    Input

    +

    +The input library is called "oops"; see the sample inputs below for examples in +your language. It defines two methods: + + + + + + + + + + + + +
    Method name and parametersParameter limitsReturnsApproximate time for a single call
    GetN()a 64-bit number0.05 microseconds
    GetNumber(i)0 ≤ i < GetN()a 64-bit number0.05 microseconds
    +

    + +

    Output

    +

    +Output what either of the solutions above would output, if they ran on 20 nodes without any limits +on memory, time, number of messages or total size of messages. +

    + +

    Limits

    +

    +Time limit: 2 seconds.
    +Memory limit per node: 128 MB.
    +Maximum number of messages a single node can send: 1000.
    +Maximum total size of messages a single node can send: 8 MB.
    +Number of nodes: 20.
    +-1018 ≤ GetNumber(i) ≤ 1018, for all i.
    +

    + +

    Small dataset

    +

    +1 ≤ GetN() ≤ 30,000. +

    + +

    Large dataset

    +

    +1 ≤ GetN() ≤ 109. +

    diff --git a/distributed_codejam/2016_r1/rps/analysis.html b/distributed_codejam/2016_r1/rps/analysis.html new file mode 100644 index 00000000..92832d4c --- /dev/null +++ b/distributed_codejam/2016_r1/rps/analysis.html @@ -0,0 +1,36 @@ +

    Remarkably Parallel Scenario: Analysis

    +

    +The title playfully refers to Rather Perplexing Showdown, +the other "RPS" problem from Saturday's Round 2, but it also alludes to the +convenient parallelizability of the tournament tree structure. There is one +wrinkle, though: it is not so convenient to divide up a single-elimination +tournament tree based on powers of 2 into exactly 100 independent parts! It is +much more natural to divide such a tree into 2K independent +parts, each of which is the subtree that determines one of the players in the +Kth from last round of the tournament. Fortunately, it is easy to use a +number of nodes less than 100, even though your code will run on all of them; +simply have the unwanted nodes exit immediately, and never send any messages to +them or receive any messages from them. +

    +For example, for N = 25, we can evenly divide the starting lineup among +64 nodes, with node 0 handling the first 225 / 64 = 219 +players, node 0 handling the next 219 players, and so on. In this +way, the i-th node operates on the subtree of the tournament that produces the +i-th player (counting starting from 0) in the round with 64 players, and these +subtrees are totally independent of one another. We can have each node send its +winning player's ID back to a master node, and then the master can finish off +the 64-player tournament, which is a much smaller version of the original +problem. +

    +Both the master node and the worker nodes (the master can also be a worker) can +use the same function to determine a tournament winner. One possibility is to +store all of the players for a given node in a vector, go through that vector +comparing adjacent pairs and finding winners, populate another vector of half +the size (or even overwrite the same vector), and so on. The limit on N +is small enough that there is no need to optimize this part. +

    +It is important not to assume that the Large dataset will consist only of +maximal cases. If you hard-code in a choice of 64 nodes, for example, your +solution might fail on a case like N = 3 (which only has 8 players), +depending on your implementation. +

    diff --git a/distributed_codejam/2016_r1/rps/rps_example.png b/distributed_codejam/2016_r1/rps/rps_example.png new file mode 100644 index 0000000000000000000000000000000000000000..0769eda6b670c7806927a321c94143ae5d3aaa1c GIT binary patch literal 8726 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 K-E!?4kNyYa83OJA literal 0 HcmV?d00001 diff --git a/distributed_codejam/2016_r1/rps/statement.html b/distributed_codejam/2016_r1/rps/statement.html new file mode 100644 index 00000000..d540161d --- /dev/null +++ b/distributed_codejam/2016_r1/rps/statement.html @@ -0,0 +1,89 @@ +

    Problem

    +

    +Your Rock-Paper-Scissors tournament yesterday went so well that you've been +asked to organize another one. It will be a single-elimination tournament with +N rounds. 2N players will participate, and they +will have unique ID numbers in the range 0 through 2N-1, +inclusive. +

    +Initially, the players will be lined up from left to right, in increasing +order by ID number. In each round, the first and second players in the lineup +(starting from the left) will play a match against each other, and the third +and fourth players in the lineup (if they exist) will play a match against each +other, and so on; all of these matches will occur simultaneously. The winners +of these matches will remain in the lineup, in the same relative order, and the +losers will leave the lineup and go home. Then a new round will begin. This +will continue until only one player remains in the lineup; that player will be +declared the winner. +

    +In each Rock-Paper-Scissors match, each of the two players secretly chooses one +of Rock, Paper, or Scissors, and then they compare their +choices. Rock beats Scissors, Scissors beats Paper, and Paper beats Rock. If +one player's choice beats the other player's choice, then that player wins and +the match is over. You are tired of worrying about ties, so you have decided +that if the players make the same choice, the player on the left wins the match. +

    +You know that the players this year are not very strategic, and each one has a +preferred move and will only ever play that move. Fortunately, you know every +player's preferred move, so you can figure out: what is the ID number of the +player who will win the tournament? +

    +Here's an example tournament with N = 2: +

    + +

    +In Round 1, player 1 beats player 0 (since Rock always beats Scissors), and +player 2 beats player 3 (since the player with the lower number wins a tie). +In Round 2, player 2 beats player 1 (since Paper always beats Rock). So, +player 2 is the winner. +

    + +

    Input

    +

    +The input library will be called "rps"; see the sample inputs below for +examples in your language. It defines two methods: GetN(), which returns the +number N of rounds in the tournament, and GetFavoriteMove(id), which +returns the favorite move of the player with ID number id, for +0 ≤ id < 2GetN(). This move will always be either +R, P, or S, representing Rock, Paper, or +Scissors, respectively. +

    + + + + + + + + + + + + +
    Method name and parametersParameter limitsReturnsApproximate time for a single call
    GetN()a 64-bit number0.09 microseconds
    GetFavoriteMove(i)0 ≤ i < 2GetN()a character0.09 microseconds
    +

    Output

    +

    +Output one value: the ID number of the winning player. +

    + +

    Limits

    +

    +Time limit: 3 seconds.
    +Memory limit per node: 128 MB.
    +Maximum number of messages a single node can send: 1000.
    +Maximum total size of messages a single node can send: 8 MB.
    +GetFavoriteMove(id) is always one of R, P, or +S for all valid values of id.
    +

    + +

    Small dataset

    +

    +Number of nodes: 10.
    +1 ≤ GetN() ≤ 10.
    +

    + +

    Large dataset

    +

    +Number of nodes: 100.
    +1 ≤ GetN() ≤ 28.
    +

    diff --git a/distributed_codejam/2016_r1/winning_move/analysis.html b/distributed_codejam/2016_r1/winning_move/analysis.html new file mode 100644 index 00000000..f7675208 --- /dev/null +++ b/distributed_codejam/2016_r1/winning_move/analysis.html @@ -0,0 +1,75 @@ +

    The Only Winning Move: Analysis

    +

    How should we handle the input data?

    +

    +Given infinite memory, this problem is not too tough: one approach is to sort +the input data in non-decreasing order and then read it from left to right, +looking for the first unique number. As is usually the case in DCJ, though, +there is too much data to fit on a single node, so we will have to divide the +task up among nodes. It's tough to split up a sort among nodes without +exceeding the memory limit of any of them or running out of time, especially +with edge cases in the mix... and you should assume that we will put in edge +cases! So we need another approach. +

    +What should each node do? We can't just find the smallest locally unique number +on each node; what if all of these numbers turn out to be the same, for +instance, and some other globally unique number is unaccounted for? Nor is it +enough to find and send all locally unique numbers on each node. What if the +smallest of these locally unique numbers also appears multiple times on a +different node, and so is not globally unique? +

    +One approach that works is to have each node produce a list with all of the +locally unique numbers and two copies of all of the locally non-unique numbers. +(It is also possible to produce two lists, one with the locally unique numbers +and one with one copy of each of the locally non-unique numbers.) It is +possible for this step not to reduce the amount of data at all, but in most +cases, this local pruning will help. +

    +

    How should we handle the processed data?

    +

    +If we try to send all of this processed data to a single master node, the +master may not have enough memory to handle it. Consider a case in which every +number is different, for example. We could not have solved that case on a +single node, so it will not work to send that data back to a single node. What +if we spread the load around? +

    +One promising idea is to send all of the 1s to node 1, all of the 2s to node 2, +and so on, with every number being sent to the node with ID equal to that +number modulo the number of nodes. Then, for example, node 1 sees all the 1s in +existence. If it sees two or more 1s, then 1 is not globally unique; if it sees +exactly one, then 1 is globally unique. We only need to find the smallest +such globally unique number from each node. We do not need to worry that +multiple nodes may find the same globally unique number, since any given number +has had all of its copies directed to only one particular node. +

    +Once again, we can imagine a test case that will make this fail: what if all +the numbers are different, but all are a multiple of the number of nodes? Then +they will all go to the same node and overwhelm it. A way around this is to +come up with a hash function that maps each number to a node in a less +predictable way. This will work even if all of the numbers in the test case are +the same, since each node will send at most two copies of a given number. +

    +

    Hey, this is MapReduce!

    +

    +In fact, it is a double +MapReduce: +

    +
      +
    • Map 1: A master evenly divides up the input data among all nodes. (You +don't need to have a master do this explicitly; you can just have each node +read the appropriate range of the data.)
    • +
    • Reduce 1: Each node produces a list of numbers with one copy of each +locally unique number and two copies of each locally non-unique number.
    • +
    • Map 2: Each node sends those numbers to reducers using a hash.
    • +
    • Reduce 2: Each node finds the smallest of its numbers (if any) that is +globally unique.
    • +
    • Final step: Each node sends those smallest globally unique numbers (or 0) +to a master node, which picks the smallest positive number or 0.
    • +
    +

    +We do not need to earmark one set of nodes as mappers and another as reducers. +All nodes can act as reducers for step 1, and then become reducers for step 2. +

    +Since both reducing steps involve sorting, this solution has a time complexity +of O(GetNumPlayers() log GetNumPlayers() / NumberOfNodes()), which is fast +enough in practice under the given limits. +

    diff --git a/distributed_codejam/2016_r1/winning_move/statement.html b/distributed_codejam/2016_r1/winning_move/statement.html new file mode 100644 index 00000000..d613224e --- /dev/null +++ b/distributed_codejam/2016_r1/winning_move/statement.html @@ -0,0 +1,58 @@ +

    Problem

    +

    +Perhaps you have played this game before: Every player submits a positive +integer. The winner, if any, is the player who submitted the smallest positive +integer that was submitted by no other player. +

    +

    +Your friends invited you to play this game, but you decided that game theory is +complex and the only winning move is not to play. Instead, you volunteered to +judge the game. Given the players' choices, can you determine what the winning +number was? +

    +

    Input

    +

    +The input library will be called "winning_move"; see the sample inputs below +for examples in your language. It will define two methods: GetNumPlayers(), +which will return the number of players in the game, and GetSubmission(i), +which will return the number chosen by the player with ID number i, for 0 ≤ +i < GetNumPlayers(). +

    + + + + + + + + + + + + +
    Method name and parametersParameter limitsReturnsApproximate time for a single call
    GetNumPlayers()a 64-bit number0.3 microseconds
    GetSubmission(i)0 ≤ i < GetNumPlayers()a character0.3 microseconds
    +

    Output

    +

    +Output one value: the winning number, or 0 if there was no winner. +

    + +

    Limits

    +

    +Time limit: 6 seconds.
    +Memory limit per node: 128 MB.
    +Maximum number of messages a single node can send: 1000.
    +Maximum total size of messages a single node can send: 1 GB.
    +1 ≤ GetSubmission(i) ≤ 1018. +

    + +

    Small dataset

    +

    +Number of nodes: 10.
    +1 ≤ GetNumPlayers() ≤ 1000.
    +

    + +

    Large dataset

    +

    +Number of nodes: 100.
    +1 ≤ GetNumPlayers() ≤ 25000000.
    +

    diff --git a/distributed_codejam/2016_r2/again/analysis.html b/distributed_codejam/2016_r2/again/analysis.html new file mode 100644 index 00000000..ecc873ac --- /dev/null +++ b/distributed_codejam/2016_r2/again/analysis.html @@ -0,0 +1,63 @@ + +

    ... we did it again: Analysis

    +

    Deducing the original problem

    +

    +As was the case in Oops, from Round 1, it's necessary to look at some source code first. What is it doing? +

    +

      +
    • There are two mysterious functions, GetA and GetB that take a single input — a number between 0 and GetN() - 1 — and return an integer. So, notionally, there are two lists of GetN() numbers that the code reads from.
    • +
    • On the k-th node, we examine all pairs (i, j) 0 ≤ i, j < GetN(), but only process the pairs for which (i + j) modulo NumberOfNodes() equals k.
    • +
    • When a node processes a pair (i, j), it calculates GetA(i) * GetB(j) and sends that value to the master.
    • +
    • The master node adds all incoming values and calculates the summation modulo LARGE_PRIME.
    • +
    • Note that the MASTER_NODE (node 0) only sums the results from other nodes. Pairs with (i + j) modulo NumberOfNodes() equal to 0 are therefore skipped.
    • +
    +Incidentally, the last point shows that changing the number of nodes doesn't just change the running time... it also changes the result! This is why the samples, the small case, and the large case all use a given fixed number of 20 nodes. +

    +So the original statement must have been something like: +"Given two list of GetN() numbers, what is the sum of GetA(i) * GetB(j) for all (i, j) where (i + j) modulo NumberOfNodes() does not equal 0?" +

    + +

    The sample code's approach

    +

    +Now that we know what we are supposed to be doing, we can assess how badly the +sample code actually does it: +

    +

    +

      +
    • Each node iterates through all GetN()2 pairs of (i, j), even though it only acts on a fraction of them.
    • +
    • Each node sends a message for each pair it processes which creates a O(GetN()2) of traffic over the network.
    • +
    • The master node then sums the values obtained from the approximately GetN()2 messages.
    • +
    +

    +This approach has an O(GetN()2) calculation and summation phase. Note that this running time does not improve if we add more nodes. +

    +O(GetN()2) run time is rather embarrassing. We only vaguely +remember writing this code after a long night of caffeine fueled Dungeon & Dragons, but +that's no excuse! Now that we have our wits about us, how can we do better? +

    + +

    A better approach

    +
      +
    • Observe that the sum of the product of multiple pairs can be rearranged to use a single multiplication:
      + + GetA(a0) × GetB(b0) + GetA(a0) × GetB(b1) + + GetA(a1) × GetB(b0) + GetA(a1) × GetB(b1) = +
      + (GetA(a0) + GetA(a1)) × (GetB(b0) + GetB(b1)). + +
    • +
    • The example above generalizes easily to more than 2 elements from each of A and B, in particular, we can aggregate all elements with a given remainder modulo NumberOfNodes(), as all of those impact the result in the same way.
    • +
    • The k-th node (including the master!) can calculate the sum of A and B for each index that has k as remainder when dividing by NumberOfNodes(). Notice that this divides the indices pretty evenly across nodes: +
      +SumGetA[k] = GetA(k) + GetA(k + NumberOfNodes()) + GetA(k + 2 × NumberOfNodes()) + ...
      +SumGetB[k] = GetB(k) + GetB(k + NumberOfNodes() + GetB(k + 2 × NumberOfNodes()) + ...
      +
    • +
    • Note: intermediate values are calculated modulo LARGE_PRIME to avoid overflows.
    • +
    • The master node can calculate the overall result modulo LARGE_PRIME as SumGetA[i] × SumGetB[j] for all pairs (i, j) where (i + j) modulo NumberOfNodes() does not equal 0 (MASTER_NODE).
    • +
    + +

    +In this approach, we make a single parallelized pass over the data, so the running time of this phase is O(GetN() / NumberOfNodes()). The second phase has an O(NumberOfNodes()2) running time. Since NumberOfNodes() << GetN() in general, and the master node only has to process two values from each node, the first phase dominates and the overall running time is O(GeN() / NumberOfNodes()) — much better! +

    +Notice that a single-node version of this method is O(GetN()), which is still better than our sample code. +

    diff --git a/distributed_codejam/2016_r2/again/statement.html b/distributed_codejam/2016_r2/again/statement.html new file mode 100644 index 00000000..3b588726 --- /dev/null +++ b/distributed_codejam/2016_r2/again/statement.html @@ -0,0 +1,153 @@ +

    Problem

    +

    ... we did it again

    +

    +You know the issue: just as in the Oops problem from Distributed Round 1, we +have lost our problem statement and the correct solutions, and we only have +these two correct but slow solutions, one per supported language. Once again, +we still have our test data. Can you still solve the problem? +

    +Notice that in this problem 20 nodes are used to run both the Small and the +Large datasets, which is not the usual number for Distributed Code Jam +problems. 20 nodes were also used to run the solutions and produce the answers +for the examples. +

    +The C++ solution: +

    +
    +#include <message.h>
    +#include <stdio.h>
    +#include "again.h"
    +
    +#define MASTER_NODE 0
    +#define SENDING_DONE -1
    +#define LARGE_PRIME 1000000007
    +
    +int main() {
    +  if (MyNodeId() == MASTER_NODE) {
    +    long long result = 0;
    +    for (int node = 1; node < NumberOfNodes(); ++node) {
    +      while (true) {
    +        Receive(node);
    +        long long value = GetLL(node);
    +        if (value == SENDING_DONE) {
    +          break;
    +        } else {
    +          result = (result + value) % LARGE_PRIME;
    +        }
    +      }
    +    }
    +    printf("%lld\n", result);
    +    return 0;
    +  } else {
    +    for (long long i = 0; i < GetN(); ++i) {
    +      for (long long j = 0; j < GetN(); ++j) {
    +        long long value = GetA(i) * GetB(j);
    +        if ((i + j) % NumberOfNodes() == MyNodeId()) {
    +          PutLL(MASTER_NODE, value);
    +          Send(MASTER_NODE);
    +        }
    +      }
    +    }
    +    PutLL(MASTER_NODE, SENDING_DONE);
    +    Send(MASTER_NODE);
    +  }
    +  return 0;
    +}
    +
    +

    +The Java solution: +

    +
    +public class Main {
    +  static int MASTER_NODE = 0;
    +  static long SENDING_DONE = -1;
    +  static long LARGE_PRIME = 1000000007;
    +
    +  public static void main(String[] args) {
    +    if (message.MyNodeId() == MASTER_NODE) {
    +      long result = 0;
    +      for (int node = 1; node < message.NumberOfNodes(); ++node) {
    +        while (true) {
    +          message.Receive(node);
    +          long value = message.GetLL(node);
    +          if (value == SENDING_DONE) {
    +            break;
    +          } else {
    +            result = (result + value) % LARGE_PRIME;
    +          }
    +        }
    +      }
    +      System.out.println(result);
    +    } else {
    +      for (long i = 0; i < again.GetN(); ++i) {
    +        for (long j = 0; j < again.GetN(); ++j) {
    +          long value = again.GetA(i) * again.GetB(j);
    +          if ((i + j) % message.NumberOfNodes() == message.MyNodeId()) {
    +            message.PutLL(MASTER_NODE, value);
    +            message.Send(MASTER_NODE);
    +          }
    +        }
    +      }
    +      message.PutLL(MASTER_NODE, SENDING_DONE);
    +      message.Send(MASTER_NODE);
    +    }
    +  }
    +}
    +
    + +

    Input

    +

    +The input library is called "again"; see the sample inputs below for examples +in your language. It defines three methods: +

    +
      +
    • GetN(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit number.
      • +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    • GetA(i): +
        +
      • Takes a 64-bit number in the range 0 ≤ i < GetN().
      • +
      • Returns a 64-bit number. +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    • GetB(i): +
        +
      • Takes a 64-bit number in the range 0 ≤ i < GetN().
      • +
      • Returns a 64-bit number. +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    + +

    Output

    +

    +Output what either of the solutions above would output, if they ran on 20 nodes +without any limits on memory, time, number of messages or total size of +messages. +

    + +

    Limits

    +

    +Time limit: 2 seconds.
    +Memory limit per node: 128 MB.
    +Maximum number of messages a single node can send: 1000.
    +Maximum total size of messages a single node can send: 8 MB.
    +Number of nodes: 20.
    +0 ≤ GetA(i) ≤ 109, for all i.
    +0 ≤ GetB(i) ≤ 109, for all i.
    +

    + +

    Small input

    +

    +1 ≤ GetN() ≤ 30,000.
    +

    + +

    Large input

    +

    +1 ≤ GetN() ≤ 108. +

    diff --git a/distributed_codejam/2016_r2/analysis_intro.html b/distributed_codejam/2016_r2/analysis_intro.html new file mode 100644 index 00000000..b6fa94dc --- /dev/null +++ b/distributed_codejam/2016_r2/analysis_intro.html @@ -0,0 +1,49 @@ +

    +The second of our two online distributed rounds consisted of a quartet of +problems. ...we did it again is another code analysis and improvement +problem in the same vein as +Round 1's Oops. +Lisp++ asks contestants to act like a compiler for a language consisting +only of parentheses. Asteroids is about a dangerous journey through a +slowly advancing asteroid field, and Gas Stations is about a rather less +dangerous journey along a very long road. +

    +The first two problems got many solves relatively quickly; the last two +proved tougher. Our first attempts for Asteroids and Gas Stations came in +at 49:54 and 1:13:55, respectively. Eryx was the first to submit every +dataset (by 1:21:31). The scoreboard filled up with many 100 scores, but after +the judging dust settled, there were only 14; the fastest 75 also made it +into the top 15. eatmore took top honors with a perfect score in 2:09:25; our +runner-up in the 2015 Distributed Finals, Marcin.Smulewicz, took second place. +Our 2015 Distributed Code Jam champion, bmerry, also qualified to defend his +title at the 2016 Finals in New York on August 6. +

    +Thank you all for participating! We are excited to continue growing and +improving the Distributed contest, and we appreciate your feedback. +

    +
    +

    +Cast +

    +Problem B (...we did it again): Written by Pablo Heiber and Onufry +Wojtaszczyk. Prepared by Onufry Wojtaszczyk. +

    +Problem C (Lisp++): Written by Pablo Heiber. Prepared by Ian Tullis. +

    +Problem D (Asteroids): Written by Pablo Heiber and Onufry Wojtaszczyk. +Prepared by Pablo Heiber. +

    +Problem E (Gas Stations): Written by Onufry Wojtaszczyk. Prepared by +Pablo Heiber. +

    +Solutions and other problem preparation and review by Minh Doan and +Won-seok Yoo. +

    +Analysis authors: +

    +
      +
    • ...we did it again: Seth Troisi and Won-seok Yoo
    • +
    • Lisp++: Pablo Heiber and Won-seok Yoo
    • +
    • Asteroids: Pablo Heiber
    • +
    • Gas Stations: Pablo Heiber and Won-seok Yoo
    • +
    diff --git a/distributed_codejam/2016_r2/asteroids/analysis.html b/distributed_codejam/2016_r2/asteroids/analysis.html new file mode 100644 index 00000000..4dc4e016 --- /dev/null +++ b/distributed_codejam/2016_r2/asteroids/analysis.html @@ -0,0 +1,16 @@ +

    Asteroids: Analysis

    +

    A non-distributed solution to the problem

    +

    +The quickest way to get going with this problem is to model it as a longest path problem in a graph. As the linked article mentions, this is NP-complete in general, but can be solved in linear time in particular cases like the one we have at hand. If we consider the edges the possible moves after a turn is resolved, the destination row is always above the source row for any edge and thus the graph is acyclic. Moreover, we have exactly 3 incoming edges to each position (i, j): you can arrive by either staying in the same column from(i-1, j), moving right from (i, j-1) or moving left from (i, j+1). In each case you need to check that all involved positions are valid (i.e., not outside the screen) and not asteroids. The cost of the edge is, of course, the sum of the involved cells (only include one of the source cell and destination cell, as the destination of one edge is the source of the next). In this way, we have a graph with at most 3 × GetHeight() × GetWidth() edges and we can calculate the longest path on it in linear time. +

    +One thing to notice, as it is usual for this kind of dynamic programming approach to paths in graphs, is that we don't have to construct the graph explicitly. We just calculate the result from each position as the best possible from each possible previous position. Calculating all positions in row i only requires the result from positions in row i-1, so we can use O(GetWidth()) memory in total (instead of a linear size memory). This makes a single node solution comply with memory limits even for the Large dataset. However, as you might expect, it would be too slow. +

    +

    How to distribute the solution

    +

    +Our single node solution uses the results in row i to calculate results in row i + 1. Unfortunately, this is a deep dependency; we can't assign a subset of rows to each node because only one new row can be calculated at any given time, making the solution be effectively non-distributed, since only one node is doing computation at any given time. +

    +An additional observation is that, since the dependency for a given cell is three contiguous cells, and they overlap with the dependencies of neighboring cells, a slice of columns [j, k) in row i + 1 has a dependency of only a slightly bigger slice of columns [j - 1, k + 1) in row i. This yields a first idea: we can make each node calculate a slice of row i, and then synchronize with its neighbors to send the additional cell that each needs for their next rows. However, in the Large dataset, this implies 30,000 steps of message exchanging, which is too much overhead. With a baseline latency of 5ms, 30,000 message exchanges would result in 150 seconds of waiting messages alone! +

    +We can do better if we delay the exchange of messages: If want to calculate r rows ahead, we need r extra cells on each side of our slice for the dependency (notice that each row adds one cell on each side). Therefore, we can calculate about GetWidth() / NumberOfNodes() steps (watch out the rounding) locally, and then exchange GetWidth() / NumberOfNodes() extra cells with each neighbor. This maintains more or less the same amount of total size of messages, but reduces the number of messages by a factor of NumberOfNodes(), which is significant because network latency, which is more important than network speed in cases with many messages, is multiplied by the number of synchronizing steps. +This trick is finally enough to pass the Large dataset. +

    diff --git a/distributed_codejam/2016_r2/asteroids/statement.html b/distributed_codejam/2016_r2/asteroids/statement.html new file mode 100644 index 00000000..064bc587 --- /dev/null +++ b/distributed_codejam/2016_r2/asteroids/statement.html @@ -0,0 +1,166 @@ +

    Problem

    +

    Asteroids

    +

    +You are playing a game of DCJAsteroids in your old arcade machine. The game is played on a screen +that displays a matrix of square cells. The matrix has a fixed width and infinite height. +Each cell may contain your spaceship, an asteroid, or some amount of helium. +The goal of the game is to collect helium without colliding with any asteroids. +Your spaceship always stays in the bottom row of the screen, and it can only move horizontally. +

    +The game is turn-based. On a turn, you can remain where you are, move to the cell on your left +(unless you are in the left-most column), or move to the cell on your right (unless you are in the +right-most column). After you move (or not) your spaceship, every other item in the screen +moves to the cell directly below it. The cells in the bottom row fall off the bottom of the screen +and disappear. +

    +For instance, suppose this is a current arrangement, with X representing your +spaceship, # representing a cell occupied by an asteroid, and . +representing a cell not occupied by an asteroid (to keep it simple, we do not depict the amount of +helium in this example). +

    +#.##.
    +#..#.
    +..X..
    +

    +If you decide to stay at your current position, then the resulting state after the turn is resolved +will be: +

    +#.##.
    +#.X#.
    +

    +If you decide to move left, then the result after the turn is resolved will be: +

    +#.##.
    +#X.#.
    +
    +If you ever move the spaceship into a cell occupied by an asteroid or an asteroid moves into the +cell occupied by your spaceship, the spaceship is destroyed. In the example above, +if you decided to move right, after the turn is resolved, your spaceship and an asteroid would +occupy the same cell, so the spaceship would be destroyed. If you decided to move left two turns +in a row, the second move would make your spaceship move into an asteroid, so it would be +destroyed as well. +

    +Each cell not occupied by an asteroid may have a different amount of helium. +We assign a digit 0 through 9 to each non-asteroid cell (instead of +.); the number in a cell represents the number of points you earn from the helium in +that cell. Each time your spaceship moves into a cell containing helium or a cell containing helium +moves into your spaceship's position, you collect the helium there and are awarded the points. +You can only collect the helium in each cell once; after that, your spaceship can still be in +that cell, but you collect no additional helium and receive no additional points for it. +

    +You will be given a matrix of characters representing the asteroids and helium that are approaching. +You may assume that all rows below and above the input matrix are full of worthless helium (i.e., +they are rows of 0s). +You may choose the position you are in when the bottom row in the input matrix falls into your +current row. +

    +If there is no way to navigate through the entire input matrix without the spaceship being +destroyed, output -1. Otherwise, output the maximum number of points you can +accumulate while navigating through the entire input matrix. You are considered done navigating +only when every row in the input matrix has already disappeared from the screen. +

    +Let us add point values to the empty cells in the example above (assume your current position +has 0 value). +If you start by going left and then stay for two turns, you would go through +these states and emerge without being destroyed: +

    +#1##9    00000    00000     00000
    +#23#9 -> #1##9 -> 00000  -> 00000
    +66X15    #X3#9    #X##9     0X000
    +

    +In this case you collect 6+2 points in the first turn, 1 in the second turn and 0 in the third, +yielding a total of 9 points. If instead you stay for the first turn, then move left, and then +stay again, this would happen: +

    +#1##9    00000    00000     00000
    +#23#9 -> #1##9 -> 00000  -> 00000
    +66X15    #2X#9    #X##9     0X000
    +

    +In this case you collect 3 points in the first turn, 2+1 points in the second and 0 in the third, +for a total of 6 points. Any other sequence of moves gets the spaceship destroyed. +

    + +

    Input

    +

    +The input library is called "asteroids"; see the sample inputs below for examples in your +language. It defines three methods: +

      +
    • GetHeight(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the height of the input matrix.
      • +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    • GetWidth(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the width of both the screen and the input matrix.
      • +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    • GetPosition(i, j): +
        +
      • Takes two 64-bit integers in the ranges 0 ≤ i < GetHeight(), + 0 ≤ j < GetWidth().
      • +
      • Returns a character: the contents of the cell at row i and column j of the input matrix. + The character is # if the cell contains an asteroid and a digit if not, + representing the point value of the helium you can collect from that cell, as explained above. + Rows are numbered from bottom to top (so your spaceship will enter the rows in increasing + order). Columns are numbered from left to right, as explained above. +
      • +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    +

    + +

    Output

    +

    +Output a single line with a single integer: the maximum number of points you can accumulate in +your journey through the asteroids without being destroyed, or -1 if it is impossible to avoid +destruction. +

    + +

    Limits

    +

    +Time limit: 6 seconds.
    +Memory limit per node: 128 MB.
    +Maximum number of messages a single node can send: 1000.
    +Maximum total size of messages a single node can send: 8 MB.
    +GetPosition(i, j) is a digit or #, for all i and j.
    +

    + +

    Small dataset

    +

    +Number of nodes: 10.
    +1 ≤ GetHeight() ≤ 1000.
    +1 ≤ GetWidth() ≤ 1000. +

    + +

    Large dataset

    +

    +Number of nodes: 100.
    +1 ≤ GetHeight() ≤ 30,000.
    +1 ≤ GetWidth() ≤ 30,000. +

    + +

    +For ease of reading, these are the input matrices in the samples: +

    +8##123
    +#999#1
    +21##11
    +52#11#
    +
    +1#78
    +0011
    +#2#9
    +0136
    +0#8#
    +21#9
    +
    +0##
    +000
    +##0
    +
    diff --git a/distributed_codejam/2016_r2/gas_stations/analysis.html b/distributed_codejam/2016_r2/gas_stations/analysis.html new file mode 100644 index 00000000..ad270755 --- /dev/null +++ b/distributed_codejam/2016_r2/gas_stations/analysis.html @@ -0,0 +1,49 @@ + +

    gas_stations: Analysis

    +

    Road-centric idea

    +

    +Consider a 1-km segment between two stations. Assume that, whenever the car uses gas, it uses the oldest gas in the tank (this doesn't change the results, of course). Careful inspection shows the liter of gas used to drive a segment between kms i and i+1 can only come from stations at kms in the interval [max(0, i - GetTankSize() + 1), i]. If we use gas that comes from a station with index j < i - GetTankSize() + 1, then all j-i 1-km segments in the interval [j, i+1] have to use gas from a station with index less than or equal to j, which means all the gas for the interval was on the car when we departed the station at j, which contradicts the maximum tank size. At the same time, if we choose for every segment a station in range, and at every station we get exactly what was assigned for segments ahead, it is easy to verify that we get a valid gas-plan for the trip. +

    +Having a fixed interval for each segment, it's clear that we want to get the gas at the station in the interval with minimum price. So, the solution is the sum for each i in [0, GetNumKms()-1] of the minimum of the set {GetGasPrice(j) : j in [max(0, i - GetTankSize() + 1), i]}. +

    +From this, we can see a trivial O(GetNumKms() × GetTankSize()) algorithm that solves the problem, but this is too slow, even for the Small dataset. +

    +

    An O(n log n) algorithm

    +

    +We can use a sliding window to maintain the set of current stations. From one segment to the next we need to add at most one new price and remove at most one price that should no longer be considered, and standard sorted structures like C++'s set or Java's TreeSet provide insertion, deletion and finding the minimum in efficient logarithmic time. This yields a clear and concise O(n log n) algorithm. +

    +To distribute this algorithm and get a NumberOfNodes()x speedup, as we usually want, each node can first calculate minimum price for its entire assigned interval and broadcast it to all other nodes. Each node can exploit this information to make the size of the sliding window bounded by GetNumKms()/NumberOfNodes() (10^7). The idea is: if the tank size is big, the range of indices assigned to some other nodes are going to be included in every interval considered in a given node, so instead of doing the sliding window through those, we just skip them and use their overall minimum as an additional possible price for every internval. Unfortunately, memory limit can be a problem without careful planning, as sorted tree structures tends to incur in a large constant for their linear memory requirements. Dividing segments further (for example, use 1000 intervals instead of 100) and reusing memory can be one solution, but this may make implementation complicated. +

    +

    Two-way scanning in O(n)

    +

    +Instead of maintaining a sorted structure, we can use two-way scanning. Let's think of some arbitrary 2 × GetTankSize() km interval in the middle of the road and get the price for each segment in there. Then, the pseudocode for calculating answer for this interval is something like this: +

    +for i in [interval_start, interval_start + 2 × GetTankSize())
    +  result = result + min({GetGasPrice(j) : j in [max(0, i - GetTankSize() + 1), i]})
    +
    +

    +The main idea is slicing calcuation in two parts and calculate separately. The code can be rewritten: +

    +middle = interval_start + GetTankSize()
    +for i in [interval_start, interval_start + 2 × GetTankSize())
    +  result = result + min(
    +    min({GetGasPrice(j) : j in [max(0, i -GetTankSize() + 1), middle]}) ,
    +    min({GetGasPrice(j) : j in [middle + 1, i]}) )
    +

    +With this new version, it's easy to see that we can precalculate all the minimums over sets in linear time to avoid the nested loops, like this: +

    +middle = interval_start + GetTankSize()
    +min_backwards[0] = GetGasPrice(middle - 1)
    +min_forward[0] = GetGasPrice(middle)
    +for i in [1, GetTankSize())
    +  min_backwards[i] = min(min_backwards[i - 1], GetGasPrice(middle - 1 - i))
    +  min_forward[i] = min(min_forward[i - 1], GetGasPrice(middle + i))
    +result = result + min_backwards[GetTankSize() - 1] +  min_forward[GetTankSize() - 1]
    +for i in [interval_start + 1, interval_start + 2 × GetTankSize() - 1)
    +  result = result + min( min_backwards[middle - i - 1], min_forward[i - interval_start] )
    +
    +To run this on a single node, we need to do it for interval_start that is a multiple of 2 × GetTankSize(), and be careful with the last interval that might be smaller, to get every minimum we need exactly once. Since we need to process O(GetNumKms() / (2 × GetTankSize()) intervals, and each take time O(2 × GetTankSize()), the total running time is O(GetNumKms()). +

    +To distribute this linear algorithm, we just use a similar idea than for the less efficient sliding window algorithm distribution: the min_backwards/min_forward arrays simply replace the sorted tree structure as a more efficient way to calculate the sliding window part, and the speedup by skipping large contiguous part that are present in every interval is the same. +Notice that, in case the tank is small enough to not skip anything, but large enough to be significant, we need the multiple intervals trick to achieve true linear time, even in the distributed case. +

    diff --git a/distributed_codejam/2016_r2/gas_stations/statement.html b/distributed_codejam/2016_r2/gas_stations/statement.html new file mode 100644 index 00000000..0ddeeb1d --- /dev/null +++ b/distributed_codejam/2016_r2/gas_stations/statement.html @@ -0,0 +1,79 @@ +

    Problem

    +

    Gas Stations

    +

    +You are about to embark on the road trip of a lifetime, covering millions or even billions +of kilometers! Of course, gas for such a long trip can get really expensive, and you are +on a budget, so you should plan ahead. Fortunately, the road you are taking has a lot of +gas stations: there is one at your starting point, one every kilometer thereafter. +There is no gas station at your ending point (not that it would do you any good). +

    +Different gas stations may charge different prices per liter of gas. +Your car is really old, so it can run for exactly 1 kilometer with 1 liter of gas. +You can never have more liters of gas than the tank can hold, but you can add gas to the tank at +any station; you do not need to wait for the tank to be empty. +Each time you advance a kilometer, your tank's contents are reduced by 1 liter. It is OK if the +tank becomes empty exactly at the end of your trip or exactly as you reach a gas station (in which +case you can add gas there). +

    +Given the length of your route, the size of your tank and the price per liter of each station, what +is the minimum amount of money you need to complete your trip? Your starting point is exactly at the +station at the 0 kilometer mark, and your tank starts empty. +

    + +

    Input

    +

    +The input library is called "gas_stations"; see the sample inputs below for examples in your +language. It defines three methods: +

      +
    • GetNumKms(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the total length in kilometers of your trip.
      • +
      • Expect each call to take 0.15 microseconds.
      • +
      +
    • +
    • GetTankSize(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the maximum amount of liters of gas you can have in your tank.
      • +
      • Expect each call to take 0.15 microseconds.
      • +
      +
    • +
    • GetGasPrice(i): +
        +
      • Takes a 64-bit integer in the range 0 ≤ i < GetNumKms().
      • +
      • Returns a 64-bit integer: the price of each liter of gas in the station exactly i kilometers + from your starting point.
      • +
      • Expect each call to take 0.15 microseconds.
      • +
      +
    • +
    +

    + +

    Output

    +

    +Output a single line with a single integer: the minimum amount of money you need to pay for all the +gas necessary for your trip. +

    + +

    Limits

    +

    +Time limit: 5 seconds.
    +Memory limit per node: 128 MB.
    +Maximum number of messages a single node can send: 1000.
    +Maximum total size of messages a single node can send: 8 MB.
    +1 ≤ GetTankSize() ≤ GetNumKms().
    +1 ≤ GetGasPrice(i) ≤ 109, for all i.
    +

    + +

    Small dataset

    +

    +Number of nodes: 10.
    +1 ≤ GetNumKms() ≤ 106. +

    + +

    Large dataset

    +

    +Number of nodes: 100.
    +1 ≤ GetNumKms() ≤ 5 × 108. +

    diff --git a/distributed_codejam/2016_r2/lisp_plus_plus/analysis.html b/distributed_codejam/2016_r2/lisp_plus_plus/analysis.html new file mode 100644 index 00000000..c9f9d99e --- /dev/null +++ b/distributed_codejam/2016_r2/lisp_plus_plus/analysis.html @@ -0,0 +1,35 @@ + +

    Lisp++: Analysis

    +

    How can we recognize a Lisp++ valid program?

    +

    +It is not hard to show that a Lisp++ program is valid if and only if each prefix of the program has at least as many opening parentheses as closing parentheses, and the overall program has the same amount of both in total. Let us prove it: +

    +Let o(X) and c(X) be the number of opening and closing parentheses of a string X, respectively. Assume there is a string of parentheses P that has the aforementioned properties that is not a valid program. Then o(P) = c(P) and for each prefix X of P o(X) ≥ c(X). If there is at least one proper prefix A such that o(A) = c(A), decompose P into AB (choose any such A if multiple exist). Since o(A) = c(A) and each prefix of A is also a prefix of P, A fulfills the hypotheses, and it's shorter than P, so A is a valid program. Now, each prefix D of length k of B is a prefix C of length |A| + k of P minus A. Then, o(D) = o(C) - o(A) ≥ c(C) - o(A) = c(C) - c(A) = c(D). Similarly, o(B) = c(B). B is shorter than P and fulfills the hypothesis, so B is also a valid program, and therefore AB = P is a valid program by definition, which contradicts the assumption. +

    +In the other direction, assume P is a valid program of minimum length that does not have the mentioned properties. The program () does have both properties, so P ≠ (). If P = (A) with A a valid program, since A has the properties, P also has them because each proper prefix of P has one more opening parentheses and no more closing parentheses than a prefix of A, and the totals are both one more than the totals for A. Finally, if P = AB with A and B valid programs, the totals of each parenthesis type in P are equal because they are in both A and B, and each proper prefix of P is either a proper prefix of A, all of A (both of which trivially have at least as many opening parentheses as closing parentheses) or a proper prefix of B plus A. Since A has the counts balanced, and the proper prefix of B has more opening parentheses, the resulting proper prefix of P will also have more opening parentheses. +

    +Furthermore, it is easy to see that if P is a string of parentheses that has more opening parentheses than closing parentheses on every prefix, it can be extended to a valid program simply by appending enough closing parentheses. Therefore, we have to find the first position in the input program such that the number of opening parentheses up to that position is less than the number of closing parenteheses. This is easy to do in a single node with the following pseudocode: +

    +x = 0
    +for each character c in position i in the input:
    +  if c == '(':
    +    x = x + 1
    +  else
    +    x = x - 1
    +  if x < 0: return i
    +if x == 0:
    +  return the length of the input
    +else:
    +  return -1
    +
    +

    Distributing the solution

    +Let us assign each node 1 / NumberOfNodes() of the input string, awaiting information from a previous node. The information consists of: +
      +
    • The difference in number of parentheses of the previous part of the string.
    • +
    • Whether a result was already found.
    • +
    +Time complexity is still O(GetLength()) as single node version, as each node is idle until all previous nodes finish their work. However, we can do something similar to the trick from +Rearranging Crates in Round 1. We calculate the difference in parentheses from each segment independently, send them all to the master, let the master calculate the initial imbalance from each node, and then have each node report a result back to the master. Of course, the master should only consider the result from the node reporting an error on the shortest prefix of the string, and the node considering the right end of the string needs to report whether it finished with an x equal to 0 or greater than 0 if there was no error. +

    +Another way to implement the same idea is to send the master the totals and the deepest imbalance found within each node. That way, the master can use that information directly to know which part of the string the first error is in, and then simply process that part, without using the other nodes again. This saves some message passing, and possibly simplifies the distribution, at the expense of making the code more redundant. The difference in running time between both versions is negligible. +

    diff --git a/distributed_codejam/2016_r2/lisp_plus_plus/statement.html b/distributed_codejam/2016_r2/lisp_plus_plus/statement.html new file mode 100644 index 00000000..29b2cbea --- /dev/null +++ b/distributed_codejam/2016_r2/lisp_plus_plus/statement.html @@ -0,0 +1,98 @@ +

    Problem

    +

    Lisp++

    +

    +Alyssa is a huge fan of the Lisp programming language, but she wants it to have +even more parentheses, so she is designing a new language, Lisp++. A valid +Lisp++ program consists of one of the following. (In this specification, +P stands for some valid program -- not necessarily the same program each time.) +

      +
    • () Literally, just an opening parenthesis and a closing + parenthesis.
    • +
    • (P) A program within a pair of enclosing + parentheses.
    • +
    • PP Two programs (not necessarily the same), back to + back.
    • +
    +

    +Alyssa is working on a compiler for Lisp++. The compiler must be able to +evaluate a string consisting of ( characters and/or ) +characters to determine whether it is a valid Lisp++ program, and provide the +user with some helpful information if it is not. If the program is valid, the +compiler should print -1. Otherwise, it should print the length of the longest +prefix that could be extended into a valid program by adding zero or more +additional characters to the end. If that prefix is the empty prefix, the +compiler should print 0. In particular, if the input string is not a valid program, but can be +extended to a valid program, the compiler should print the length of the input string. +

    +For example: +

      +
    • ()(()()) is a valid program, so the compiler should print +-1.
    • +
    • (())) is not a valid program. The prefix (()) is +the longest prefix that is or could be extended into a valid program (in this +case, it already is one), so the compiler should print 4. The only longer +prefix is (())) (i.e., the entire string), but there is no way to +add (any number of) characters to the end of that string to make it into a +valid program.
    • +
    • ) is not a valid program. The prefix ) cannot be +extended into a valid program. The empty prefix is not a valid program, but it +can easily be extended into one (by adding (), for example). So +the compiler should print 0.
    • +
    +

    +Given a string, what should Alyssa's compiler print? +

    + +

    Input

    +

    +The input library is called "lisp_plus_plus"; see the sample inputs below for +examples in your language. It defines two methods: +

    +
      +
    • GetLength(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the length of the string.
      • +
      • Expect each call to take 0.07 microseconds.
      • +
      +
    • +
    • GetCharacter(i): +
        +
      • Takes a 64-bit integer in the range 0 ≤ i < GetLength().
      • +
      • Returns a character: either ( or ), the + character at position i.
      • +
      • Expect each call to take 0.07 microseconds.
      • +
      +
    • +
    + +

    Output

    +

    +Output a single line with a single integer: what the compiler should print, as described above. +

    + +

    Limits

    +

    +Time limit: 3 seconds.
    +Memory limit per node: 128 MB.
    +Maximum number of messages a single node can send: 1000.
    +Maximum total size of messages a single node can send: 8 MB.
    +GetCharacter(i) is always ( or ).
    +

    + +

    Small input

    +

    +Number of nodes: 10.
    +1 ≤ GetLength() ≤ 106. +

    + +

    Large input

    +

    +Number of nodes: 100.
    +1 ≤ GetLength() ≤ 109. +

    + +

    +The sample input files are the same examples from the problem statement, in +the same order. +

    diff --git a/distributed_codejam/2017_finals/analysis_intro.html b/distributed_codejam/2017_finals/analysis_intro.html new file mode 100644 index 00000000..5f224a64 --- /dev/null +++ b/distributed_codejam/2017_finals/analysis_intro.html @@ -0,0 +1,56 @@ +

    + Our third-ever DCJ World Finals started off with Baby Blocks and + Lemming, both of which would have been relatively straightforward as + non-distributed problems, but which were somewhat difficult to distribute + efficiently and correctly. Median was yet another 2017 DCJ problem + in which nodes did not all behave the same way; contestants had to figure out + how different nodes were storing their data before they could even begin to + use all of those nodes to find the median. Finally, Lisp+++ took a + previous DCJ problem + and added what looked like a minor twist... but that small change actually + made the problem much, much more challenging to solve! +

    + In general, contestants found problems B and C approachable, but D and E were + much more difficult. bmerry held the scoreboard lead for much of the + contest; he was unable to solve E-small, but he was the first to submit + everything else, reaching a total of 67 points. Other contestants were stuck + at 38 (B + C) or 48 (B + C + D-small), but in the last 20 minutes, three E-small + solutions appeared from ecnerwala, eatmore, and + Gennady.Korotkevich. E-large was so hard that it did not even get a wild + last-minute attempt! The contest was so tough that no contestant submitted more + than six out of the eight possible datasets. +

    + As usual for a Distributed Code Jam World Finals round, though, a lot changed + in the judging phase. Many contestants got both B-large and C-large right, but + nobody got D-large... so it all came down to D-small and E-small! ecnerwala + was the only contestant to solve both of those plus B and C, and he became our new + Distributed Code Jam World Champion, with 59 points. eatmore took second + with 49; krijgertje had the fastest of the 48-point scores, so he took third. +

    + That brings our 2017 Distributed contest to a close. We continue to be + impressed with how well our contestants have handled harder and harder + distributed problems over the years! We hope you will join us again next + year; remember that 100 nodes means 100 times (or, perhaps more realistically, + 10 times) the fun! +

    +
    +

    + Cast +

    + Problem A (Testrun): Written and prepared by George Spelvin. +

    + Problem B (Baby Blocks): Written by Pablo Heiber. Prepared by + Won-seok Yoo. +

    + Problem C (Lemming): Written by Pablo Heiber. Prepared by Yerzhan + Utkelbayev. +

    + Problem D (Median): Written and prepared by Pablo Heiber. +

    + Problem E (Lisp+++): Written and prepared by Pablo Heiber. +

    + Solutions and other problem preparation and review by Shane Carr, Mariano + Crosetti, Md Mahbubul Hasan, Ian Tullis, and Onufry Wojtaszczyk. +

    + All analyses by Pablo Heiber. +

    diff --git a/distributed_codejam/2017_finals/baby_blocks/analysis.html b/distributed_codejam/2017_finals/baby_blocks/analysis.html new file mode 100644 index 00000000..8b1db884 --- /dev/null +++ b/distributed_codejam/2017_finals/baby_blocks/analysis.html @@ -0,0 +1,45 @@ +

    Baby Blocks: Analysis

    +

    Small dataset: a single-node solution

    +

    +A single-node solution for this problem can be described, and implemented, +in a few lines. Let Si be the sum of the weights of all the blocks between indices +0 and i, inclusive, and Tj be the sum of the weights of all the blocks between indices j and N-1, +inclusive, where N is the total number of blocks. Since all blocks' weights are positive, +if Si ≤ Tj, for all indices j' with j' < j, +Si ≤ Tj < Tj'. Symmetrically, if +Si ≥ Tj, for all indices i' with i' > i, +Si' > Tj. So, we can do the following: Start with the pair +i = 0, j = N - 1. Based on the comparison, we can increase i, decrease j, or both, +and that will cover all possible pairs we need to count. Of course, when +Si = Tj, we add 1 to a running total. Moreover, since the difference +between i and j decreases with every step, they will meet and the process will stop +in at most N - 1 steps. Since we examine increasingly long prefixes and suffixes, we +can also adjust S and T in constant time, and therefore, the algorithm runs in linear +time overall. This solution is fast enough for the Small dataset, but too slow for the +Large dataset, and we need to do something distributed. +

    +

    Large dataset

    +

    +One idea to distribute the solution is simply to assign ranges of both indices to each node, +and use the single-node solution, adding up all partial results in a master for final output. +This has two problems. The first one (and the easiest one to solve) is that we need to initialize +Si and Tj as the sum of possibly many values. This can be solved +with the most common technique in Distributed Code Jam: a first stage in which each node +sums up its own ranges and distributes it to other nodes, which can sum a prefix as the sum of +many of those partial sums. The second problem is more difficult to solve: ranges of values for +i correspond to ranges of values for j in a way that depends on the value at all indices +before the first value for i and all indices after the last value for j. Fortunately, there is +also a less standard but known technique we can use to solve the synchronization. This technique +also appeared in the more involved problem +Air Show, from +the 2016 finals. +

    +The idea is to use the first stage to get partial sums of fixed-sized intervals. Then, we can do +a version of the single-node algorithm using those partial sums as individual weights. Every time +we move one index, we get a new synchronized pair of indices, together with initial values for +Si and Tj, for a worker to process. There is a bit more detail, but it is +described in +the analysis for +Air Show. In that problem, two arrays of integers are synchronized for processing. Here, we are +synchronizing a single array and its reverse, in a way. +

    diff --git a/distributed_codejam/2017_finals/baby_blocks/statement.html b/distributed_codejam/2017_finals/baby_blocks/statement.html new file mode 100644 index 00000000..7c9a1210 --- /dev/null +++ b/distributed_codejam/2017_finals/baby_blocks/statement.html @@ -0,0 +1,75 @@ +

    Problem

    + +

    Baby Blocks

    +

    + Your two babies Alicia and Bobby love to play with toy blocks. Their blocks + come lined up in a single line in a box; each block has a certain weight. + When it is time to play, Alicia takes the leftmost i blocks for some number + i ≥ 1, and Bobby takes the j rightmost blocks for some number j ≥ 1. + i and j are not necessarily the same, and the babies always choose values + such that i + j does not exceed the total number of blocks. There may be + some blocks left over in the box after the babies have finished taking + blocks. +

    + Like many babies, Alicia and Bobby are very concerned about unfairness. + After the babies have taken their blocks, but before the babies have started + to play with them, Alicia will put all of her blocks on one side of a scale, + and Bobby will put all of his blocks on the other side of the scale. If the + two total weights are equal, the babies will play happily. Otherwise, they + will start to cry and throw the blocks around. You would prefer to avoid this. +

    + How many possible ways are there for the babies to take blocks so that they + will be happy? Two ways are different if and only if their (i, j) pairs are + different. +

    + +

    Input

    +

    + The input library is called "baby_blocks"; see the sample inputs below for + examples in your language. It defines two methods: +

    +
      +
    • GetNumberOfBlocks(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of blocks in the box.
      • +
      • Expect each call to take 0.1 microseconds.
      • +
      +
    • +
    • GetBlockWeight(i): +
        +
      • Takes a 64-bit integer in the range 0 ≤ i < + GetNumberOfBlocks().
      • +
      • Returns a 64-bit integer: the weight of the i-th block in the box, + where i = 0 corresponds to the leftmost block.
      • +
      • Expect each call to take 0.1 microseconds.
      • +
      +
    • +
    + +

    Output

    +

    + Output a single integer: the number of different ways for the babies to take + blocks that will make them happy. +

    + +

    Limits

    +

    + Time limit: 3 seconds.
    + Memory limit per node: 128 MB.
    + Maximum number of messages a single node can send: 1000.
    + Maximum total size of messages a single node can send: 8 MB.
    + 1 ≤ GetBlockWeight(i) ≤ 109.
    +

    + +

    Small dataset

    +

    + Number of nodes: 10.
    + 2 ≤ GetNumberOfBlocks() ≤ 106.
    +

    + +

    Large dataset

    +

    + Number of nodes: 100.
    + 2 ≤ GetNumberOfBlocks() ≤ 109.
    +

    diff --git a/distributed_codejam/2017_finals/lemming/analysis.html b/distributed_codejam/2017_finals/lemming/analysis.html new file mode 100644 index 00000000..02c797c4 --- /dev/null +++ b/distributed_codejam/2017_finals/lemming/analysis.html @@ -0,0 +1,82 @@ +

    Lemming

    +

    +In the problem, we are given what is effectively a +functional graph +on a matrix: Each cell of the matrix is assigned a "next" cell, or it points to the outside. +If we define the graph's set of nodes as the cells of the matrix plus one node to +represent "outside", that points to itself, the definition fits precisely. +As you can see in the article linked above, each connected component of a functional +graph is a cycle with trees rooted at nodes in the cycle and the edges pointing in +the direction of the root. +

    +The defined graph contains a single one-node cycle, which is the outside node, +as no other node points to itself. +Each other cycle is a potential trap that may leave the lemming roaming +forever unless we add a pickup point. On the other hand, setting a pickup point on any +node in a cycle is enough to "break it" and ensure that the lemming will be safe if +he starts at any point in that cycle. Moreover, this single fix also saves the lemming +when starting on a node of the connected component that is not in the cycle. Edges +point towards the root there, so following them will eventually lead the +lemming into the cycle, which we have already shown is safe after the fix. +

    +However, the component with the special single-node cycle remains, which can't +be fixed in the same way because the "outside" node is not a cell, and so it +can't be turned into a pickup point. There is also a greedy strategy that works here: +for each node that points directly to the outside node, it is clear that we +need to make it a pickup point; otherwise, the lemming starting there would fall off +the matrix. Using our functional graph model of the problem, we can see that it is +also enough to fix just those cells: any other node in the matrix is part of a +tree whose edges lead to the root (the "outside"). That path has a next-to-last +node before the root, and that node is necessarilly fixed by the rule above. +

    +To sum up, we need to count the number of cycles, and the number of cells that +point to the outside, and sum those two values. The result is a minimal number +of pickup points that prevent the lemming from falling off the matrix or looping around forever. +

    +We can implement this count in linear time. Counting the cells that point +directly outside is simple and quick. To count cycles, we can just follow +edges starting at a cell, and keeping track of the visited nodes, both in the current +path and in the previous paths (for instance, keep an integer set at -1 for non-visited +nodes, and a path id for visited nodes, which increases for every new path). +If we encounter a node visited in the current path, we have detected +a cycle. If we go outside the matrix or encouter a node visited before, we stop +and discard the current path. Note that the first time each component is +visited, its central cycle will be discovered. Every subsequent time +we visit that component, edges on the trees will be traversed, and the search will +stop when it reaches the root, which is on the already-discovered central cycle. +

    +Unfortunately, linear time is, as usual, not fast enough in Distributed Code +Jam. It's easy to distribute the task of counting the cells that point to the +outside; we can even do this in a single node. But it is more difficult to +distribute the task of counting cycles, so, from now on, we will focus on that. +

    +

    Small dataset

    +

    +In the Small dataset, upward moves are forbidden. This means that the only possible +cycles are of length two, with two cells that are horizontally adjacent. These can be +counted by assigning a number of rows of the matrix to each worker. As long as +each worker has an entire row, it can detect cycles within itself and the +combination of results is just the sum of all partial results. +

    +

    Large dataset

    +

    +For the large dataset, we need to find a way to distribute the full algorithm +to search for paths. Fortunately, the idea behind the distribution is not too +complex to describe. Unfortunately, the implementation can be a bit tricky. +

    +We start by dividing the matrix evenly across machines. The easiest way is to give +each node a rectangular strip of a few rows or columns. Then, each machine can +follow the algorithm laid out at the beginning of this analysis, except that there is +a third "stop case" when following a path: moving into some other machine's +territory. When that happens, we need to record the exit point in all cells of the +path. If we reach a cell that we have already visited, it may be from an already +fixed component, in which case we do nothing futher, or it may be a cell leading +to another machine, in which case we record that in the current path as well. +In this way, we end up with some partial paths that are not in fixed components, +but rather go from one cell in the outer edge of the machine's assigned area to +another. Luckily, the size of these edges is small enough that we can send the +connectivity information over the network to a master. Using all of this information, +master can build a new graph to see how many cycles form when those partial paths +are joined, and add that count to the sum of the partial results received +from workers from components that were entirely within one worker. +

    diff --git a/distributed_codejam/2017_finals/lemming/statement.html b/distributed_codejam/2017_finals/lemming/statement.html new file mode 100644 index 00000000..eb63a2fa --- /dev/null +++ b/distributed_codejam/2017_finals/lemming/statement.html @@ -0,0 +1,102 @@ +

    Problem

    + +

    Lemming

    +

    +Our Distributed Code Jam team has a pet lemming, Larry, who likes to follow instructions. To give +Larry some exercise, we place him on a table with grid of cells, each of which contains an arrow +pointing either up, down, left, or right. Larry will always move one unit in the direction indicated +by his current cell. If this takes him off the edge of the table, he falls harmlessly onto some +padding. Otherwise, he follows the direction indicated by his new cell, and so on, possibly +continuing in an infinite loop. +

    +

    +We don't want Larry to fall off the table or exercise forever, so we want to turn one or more of +the existing cells on the table into blank pickup points. +If Larry starts on or reaches a pickup point, we pick him up and the exercise period is over. +

    +

    +What is the minimum number of cells that we need to change into pickup points to ensure that no +matter where Larry is initially placed on the grid, he will eventually be picked up, instead of +falling off the table or exercising forever? +

    + +

    Input

    +

    +The input library is called "lemming"; see the sample inputs below for examples in your +language. It defines three methods: +

      +
    • GetRows(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of rows in the input grid.
      • +
      • Expect each call to take 0.06 microseconds.
      • +
      +
    • +
    • GetColumns(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of columns in the input grid.
      • +
      • Expect each call to take 0.06 microseconds.
      • +
      +
    • +
    • GetDirection(r, c): +
        +
      • Takes two 64-bit integers in the ranges 0 ≤ r < GetRows(), + 0 ≤ c < GetColumns().
      • +
      • Returns a character: the contents of the cell at row r and column c of the input grid. + The character is one of ^ (ASCII code 94), lowercase v, + <, > which represents up, down, left and right respectively. + Rows are numbered from top to bottom. Columns are numbered from left to right, + as explained above. +
      • +
      • Expect each call to take 0.06 microseconds.
      • +
      +
    • +
    +

    + +

    Output

    +

    +Output one line with a single integer: the minimum number of pickup points that you need to create. +

    + +

    Limits

    +

    +Number of nodes: 100 (for both the Small and Large datasets).
    +Time limit: 15 seconds.
    +Memory limit per node: 1 GB.
    +Maximum number of messages a single node can send: 1000.
    +Maximum total size of messages a single node can send: 8 MB.
    +1 ≤ GetRows() ≤ 30,000.
    +1 ≤ GetColumns() ≤ 30,000.
    +

    + +

    Small dataset

    +

    +GetDirection(r, c) is one of the characters v, <, +>, for all r and c.
    +

    + +

    Large dataset

    +

    +GetDirection(r, c) is one of the characters ^, v, <, +>, for all r and c.
    +

    + +

    +For ease of reading, these are the input matrices in the samples: +

    +<v><
    +<<v>
    +>><>
    +
    +><><><><><><><>
    +
    +<v<
    +>>^
    +v>>
    +^^^
    +
    +

    +Note that the last sample case would not appear in the Small dataset. +

    diff --git a/distributed_codejam/2017_finals/lispp3/analysis.html b/distributed_codejam/2017_finals/lispp3/analysis.html new file mode 100644 index 00000000..e9d19623 --- /dev/null +++ b/distributed_codejam/2017_finals/lispp3/analysis.html @@ -0,0 +1,164 @@ +

    Lisp+++: Analysis

    + +

    +This Finals problem is certainly harder than Round 2 2016's +Lisp++. +However, it helps to consider that problem's +solution as a +first step. Both problems involve parsing a simple +context-free grammar by +matching opening and closing parentheses. What makes Lisp+++ a lot harder is that we also need to +match each pair of parentheses with a particular + most of the time. +This seemingly small change means that we can't just rely on +the count of unmatched opening parentheses as we did in Lisp++. Consider the following example: +

    + ((()+()+())) +

    +Notice that if we remove all instances of (), we get ((++)), where +opening parentheses can be matched with closing ones, and either can be matched with a plus, +in the proper order. So just keeping counts of how 'deep' we are for each character won't tell +us whether the program has proper nesting. +

    +

    A single-node solution

    +

    +Even though a single-node solution will not pass either dataset for this problem, it is helpful to +consider one. As the example shows, just counting the frequencies of the different characters is +not enough. We can see this problem as a parentheses-matching problem with two types of opening +parentheses and closing parentheses. Openings are ( and (+, and closings +are +) and ). Of course, each opening type matches exactly one closing +type, and vice versa. An important caveat is that (+ and +) do not appear +consecutively, but need to have a valid program in between. However, the hard matching part involves +finding a suitable 3-match by first pairing each + with a ( and ), +and then matching the types described above. +

    +On a single machine, we can make do with a stack of openings, onto which we push any ( or ++ that we see. Whenever we see a ), we try to pop either a single ( +or a pair (+. To avoid stuff like ++, (+, +) and +(()), we can just remember whether the top of the stack is the last element we've seen, and +act accordingly before pushing a + or popping. If we are about to push a +, the +top needs to be a ( and not the last character seen from the input. If we are about to pop a +pair (+, the top should not be the last character seen from the input. If we are about +to pop a single (, on the other hand, it should be the last character seen, since +() is valid but (P) for a program P is not. As soon as +we detect an error, we finish. Notice that as long as the stack is valid, there is a way to complete the +string to empty the stack without errors: just add the appropriate closing for each ( or +(+ in the stack, inserting extra () as needed. If no error is detected, +the solution can decide between outputting -1 and the length of the string by checking if the +stack is empty at the end. +

    +This solution takes time linear on the input size, but that's too slow for the input bounds of +this problem. Moreover, the stack can get really large, so the memory limit is also an issue. We +need a solution that reduces both the running time and the memory requirements by distributing +the work. +

    +

    Small dataset

    +

    +There are two similar solutions to the Small dataset. Both depend on dividing the work evenly +between nodes and combining the results in a master, +taking advantage of the extra limit on the number of + characters in the input. +When distributing the strategy mentioned above, the algorithm gets only a bit more complicated +than the single-node algorithm. +Each node gets a substring S from the input. For any node except the one getting the +leftmost piece, there could be pluses and closing parentheses on it that need to be matched with +pluses and/or opening parentheses that are outside of S. To represent them, we introduce a +second stack that we call the left stack. The original stack in the single-node solution is, +accordingly, the right stack. We work as in the single-node algorithm, except that if the +right stack is empty, and we see a + or a ), we push it onto the left +stack. This means that is pending matching with characters to the left of S. Opening parentheses +are always pushed to the right stack. If there is something in the right stack, any match must use +it before something can be pending on the left stack. Once a node is finished parsing its substring, +it will have left and right stacks (either could be empty), and it may have found an error. +

    +We can then take advantage of the extra Small limit on +es to "compress" these stacks +to send them all to a single master. Since stacks contain a single type of parenthesis and up to +100 pluses, we can represent them as a list of up to 101 integers by counting the size of each +block of consecutive parentheses together. Then, the master can simulate matching large consecutive +pieces, instead of matching individual parentheses. +

    +A more insightful solution with a simpler implementation is to observe the effect of the + +limit on the single-node solution. Any popping from the stack that is not immediately after a push +requires a + to not be an error. This implies that at most the top 101 opening parentheses +can be successfully matched and popped. A stack can also contain up to 100 pluses. This makes only +the top 201 characters relevant, but it's also important to distinguish a stack with 201 characters +from one with 202 or more, due to the need to recognize the empty stack at the end. This means +only the top 202 elements in the stack are really impactful on the final answer and we can +discard the rest. +

    +In the distributed solution we started above, +instead of compressing, we can just ship the topmost 202 elements of the right stacks (or all if +less than that) to the master. Similarly, at most the bottom-most 202 elements of a left stack can +produce a successful match and pop when combining, so we can ship only those. This requires the +master to receive and process up to 404 × 99 characters, which it can do using the regular +algorithm instead of trying to speed through compressed stacks. Notice that for left stacks, you +need to either reconstruct the stack or ship input indices instead of characters to be able to know the +error place once the error is detected. Right stacks, on the other hand, can be sent as characters, +as all errors when processing the right stacks are checked in the workers, not in the master. +

    +After combining the received left and right stacks, the master outputs the earliest error among +the internal errors reported by workers and its own discoveries while combining. +

    +The first stage of this algorithm takes time O(N / M), where N is the length of the input and M is +the number of machines, as building the stacks requires a single pass over the assigned substring, +with each character requiring only constant time to process (it only depends on the tops of the +stacks). The combine stage is linear on the size of the things received by the master, which makes +it negligible, so the overall time complexity is O(N / M). +

    +

    Large dataset

    +

    +For the Large dataset we use the same initial approach of building a left and right stack on each +node for a substring of the input. Unfortunately, we can't afford to send them all to a master, +since matching elements can be arbitrarily far apart, spanning multiple nodes. Moreover, the stacks are +not highly compressible — quite the opposite! Since each parentheses type ( or +(+ on a right stack matches a particular type +) or ) on a left +stack, and they can be arbitrarily interleaved, stacks have an information density higher than 1 bit +per 2 characters. We are not allowed enough communication size to send that amount, even with great +compression. +

    +Before continuing, please be advised that we will skip even more details than in the solutions +presented before. There are many details to take into account, mostly due to +translating the "is the top of the stack the last element pushed" of the single-node algorithm +into a distributed solution. As an illustration, the first official solution we wrote for this +problem had 35 ifs and 8 fors, which makes for 43 comparisons, all of which have to be just right. +

    +We will let Li and Ri represent the left and right stacks from node i, +assuming that nodes with smaller indices are assigned substrings closer to the beginning of the +string. Then, the stacks are logically arranged in the following way: +

    +L0 R0 L1 R1 L2 R2 ... +

    +From this, we can see that L0 should be empty, or otherwise it will necessarily +cause an error. Also, the right part of Ri must match the left part of Li+1. +Moreover, at least one of them (the one with the fewest parentheses) is matched completely in that +pairing. This hints at a possible solution: a master gathers data about the number of +parentheses on each stack, and processes them in the order shown above. The master builds a +stack of pending right stacks. Each right stack is added to it. Each left stack is matched to the +top right stack. That empties at least one of them. If the right stack empties, it is removed from +the stack of right stacks. If the left stack is not exhausted, it is matched with the new top, +and so on, until either the left stack is exhausted or the stack of right stacks is empty. If there +is left stack after emptying the right stacks, there is an error in there and we need to match +no further. Otherwise, we continue with the right stacks that remain. +

    +During this algorithm, the master needs to keep track of sub-stacks as stacks are sometimes +partially matched after a pairing, and also to explain the pairing to worker nodes. +Each sub-stack is an interval of parentheses within a stack, with all the associated pluses. We +can represent stacks by an interval of positions. Reprocessing those positions with the original +algorithm will yield the same stack. We can represent a sub-stack as an interval of remaining +parentheses within a stack. Parentheses that are fully matched within a worker node are counted, as +they do not appear in the stacks the master considers. Since each pairing exhausts at least one stack, +and there are 2M stacks, there are at most 2M pairings. This means we can assign each worker at most 2 +pairings and they will all be processed. Processing a pairing requires reading at most 2 N / M +characters, for a total of 4 N / M, because a stack contains at most N / M characters. Depending on +the strategy to assign pairs to workers, it's possible to get the number of characters a worker needs +to process in the worst case down to 2 N / M overall. +

    +Then, each worker reports up to 3 errors to the master: +one initial internal error from the first stage, and up to two errors from the pairings. The master +gathers all those errors and reports the earliest, or uses the same strategy of checking whether +the stack of right stacks is empty to decide between -1 and the length of the input. +

    +As a quick overview of where all those technicalities arise, notice that stacks or sub-stacks may +eventually consist of a single +, which can't really be matched directly and must wait to +be attached in between another pair. When that happens, we need to carry the index to avoid an extra +full reconstruction just for that character. Additionally, one needs to keep track of whether the +top of the right stack or sub-stack is the last character in the input while matching a pair. +

    diff --git a/distributed_codejam/2017_finals/lispp3/statement.html b/distributed_codejam/2017_finals/lispp3/statement.html new file mode 100644 index 00000000..1e71fd91 --- /dev/null +++ b/distributed_codejam/2017_finals/lispp3/statement.html @@ -0,0 +1,111 @@ +

    Problem

    + +

    Lisp+++

    +

    + After a year of parenthetical bliss using + Lisp++, + Alyssa thought it was finally time to go beyond the parentheses. She decided + to create an extension of the language that adds the + operator to simplify + the semantics. She fittingly named the new language Lisp+++. +

    + A valid Lisp+++ program consists of one of the following. (In this + specification, P stands for some valid program -- not necessarily the + same program each time.) +

      +
    • () Literally, just an opening parenthesis and a closing + parenthesis.
    • +
    • PP Two programs (not necessarily the same), back to + back.
    • +
    • (P+P) A pair of + parentheses enclosing two programs (not necessarily the same), separated + by a + character.
    • +

    + Alyssa's new Lisp+++ requires a compiler that must be able to evaluate a + string consisting of (, ), and/or + + characters and determine whether it is a valid Lisp+++ program, and provide + the user with some helpful information if it is not. If the program is valid, + the compiler should print -1. Otherwise, it should print the length of the + longest prefix that could be extended into a valid program by adding one or + more additional characters to the end. If that prefix is the empty prefix, + the compiler should print 0. In particular, if the input string is not a + valid program, but can be extended to a valid program, the compiler should + print the length of the input string. +

    +For example: +

      +
    • (()+())() is a valid program, so the compiler should print + -1.
    • +
    • (()()+) is not a valid program. The prefix + (()()+ is the longest prefix that is or could be extended into + a valid program; in this case, one possible valid extension is + (()()+()). So, the compiler should print 6. The only longer + prefix is (()()+) (i.e., the entire string), but there is + no way to add (any number of) characters to the end of that string to make + it into a valid program.
    • +
    • ) is not a valid program. The prefix ) cannot + be extended into a valid program. The empty prefix is not a valid program, + but it can easily be extended into one (by adding (), for + example). So the compiler should print 0.
    • +
    +

    + Given a string, what should Alyssa's Lisp+++ compiler print? +

    + +

    Input

    +

    + The input library is called "lispp3"; see the sample inputs below for + examples in your language. It defines two methods: +

    +
      +
    • GetLength(): +
        +
      • Takes no argument.
      • +
      • Returns a 32-bit integer: the length of the string.
      • +
      • Expect each call to take 0.08 microseconds.
      • +
      +
    • +
    • GetCharacter(i): +
        +
      • Takes a 64-bit integer in the range 0 ≤ i < GetLength().
      • +
      • Returns a character (, ), or + +: the character at position i, counting from left to + right.
      • +
      • Expect each call to take 0.08 microseconds.
      • +
      +
    • +
    + +

    Output

    +

    + Output a single line with a single integer: what the compiler should print, + as described above. +

    + +

    Limits

    +

    + Number of nodes: 100 (for both the Small and Large datasets).
    + Time limit: 5 seconds.
    + Memory limit per node: 128 MB.
    + Maximum number of messages a single node can send: 1000.
    + Maximum total size of messages a single node can send: 128 KB. + (Notice this is less than usual.)
    + 1 ≤ GetLength() ≤ 109. + GetCharacter(i) is always (, ), or + +.
    +

    + +

    Small input

    +

    + number of i such that GetCharacter(i) = + ≤ 100. + (There are at most 100 +s in the input.) +

    + +

    Large input

    +

    + No additional limits. (There may be more than 100 +s in the input.) +

    + +

    + The sample input files are the same examples from the problem statement, in + the same order. +

    diff --git a/distributed_codejam/2017_finals/median/analysis.html b/distributed_codejam/2017_finals/median/analysis.html new file mode 100644 index 00000000..6da2050b --- /dev/null +++ b/distributed_codejam/2017_finals/median/analysis.html @@ -0,0 +1,145 @@ +

    Median: Analysis

    + +

    +The Small dataset of this problem is "regular", in the sense that there's a fixed input +in the form of a list of N integers, and all machines have equal access to all of +it. For the Large dataset, the situation is quite different. In both, however, we are +asked to find the median of that list, but we don't have enough time or memory to read the +entire list in a single machine, so the "regular" algorithms don't work. It is noteworthy +that we can't have the entire input on memory at the same time, even if we sum up the memory +for all 100 nodes! +

    + +

    Small dataset

    + +

    +There exist algorithms to sort a list in a distributed way, and linear +algorithms to find the median are possible to distribute in a similar fashion. However, most of +those algorithms assume that we can perform linear size communication between the machines, and +that the entire input can be in memory at the same time. +

    +One helpful observation is that we can easily solve the inverse problem: given an integer X, is it +below, above, or equal to the median? That solution requires a single pass through the input and +little memory. Moreover, it's trivial to distribute that solution to cut the total time +by a factor of M = NumberOfNodes(). +

    +We can use that observation to do a binary search to get +the median. This needs log R steps, where R is the range of input values, for an overall time +complexity of O(N log R / M), which looks pretty good. Unfortunately, log R is about 30 for this +problem, and 30 passes over 1 / M of the input take 30 × N × 0.1 microseconds / M, +which is 30 seconds in the worst case. In practice, it might take a bit less time than that, +since the 0.1 microseconds per API call is an upper bound, but it would be hard to make this +solution fast enough to pass. +

    +Fortunately, we can do better by doing a K-ary search. Since accessing the input is the slowest +part of the algorithm, we should try to get more information out of every call. The binary search +uses each pass to decide between three ranges [L, X - 1], [X, X], and [X + 1, U], where [L, U] is +the current range being investigated and X is usually their average. In a K-ary search, we +partition the range into O(K) intervals. And since we are doing that, it is much easier (and more +efficient in the worst case) to make these intervals of sizes that are as similar as possible. +This makes each step resemble a bucket sort: +we define K buckets and have each node count the number of integers in its range that fall into +each bucket. Since the buckets are of equal size, we can do this in constant time per integer. +Then, we send the list of buckets to a master, which can calculate the overall total per bucket +and make one pass through the buckets to decide which one the median is in. +

    +Then, we partition that interval further into buckets until each bucket has size 1. A good choice +for this problem is 1000 buckets, as they require exactly 3 passes, and the math is really +simple. To go down to 2 passes we would need 104.5 buckets, and that would also make +the messages larger, but it might be possible to make that strategy work. +You can see the solution as a K-ary search, or as a distributed +pseudo-Radix sort on the base-1000 +expression of the integers, where we only keep sorting the range that contains the only +element that we are interested in (the median). +The overall time complexity is also O(N log R / M), but since we are using log1000 +instead of log2, we cut the time by a factor of about 10, which makes it under the +time limit. +

    + +

    Large dataset

    + +

    +In the Large dataset, we face some additional challenges. We don't know the exact bounds of the +input list. Moreover, each node gets a different rotation of that list, so we can't partition the +input by assigning ranges of indices unless we know how rotated the nodes are relative to each +other. Our solution focuses on finding N and the rotation differences (the dis), and +then applying the solution explained for the Small dataset. +

    +One hint to the solution comes from the strange guarantee that the input is randomly shuffled. +This means that a medium-sized consecutive part of the data (for instance, a sublist of length +100) is guaranteed to be unique unless some integer in the list has enormous frequency. +We define sublist considering a circular list of N integers, as in the problem statement, so +a sublist may wrap around from the last to the first integer, possibly several times if +N < 100. +Luckily, we can detect such a case by random sampling. Also, if a given integer +X occurs in the data with a frequency 1/2 or higher, the median of the data is necessarily X. +So we can random sample the data by querying random indices i in the +range [0, 1018]. Since the range is so much larger than N, we can be sure that i % N +is close to uniformly distributed in [0, N-1], even though we do not know N. +Then, if we measure the frequency +of any given integer X to be larger than, say, 6 / 10, we output X. Otherwise, we continue. +Notice that even with a small number of samples, like 105, the variance is small and we +can afford a huge margin of error; for example, if the frequency is between 1/2 and 6/10, +that is fine, since the next step of the algorithm should detect that X is the median. +Since we can afford to sample a relatively small number of positions, this step doesn't really +require distribution and takes negligible running time. +

    +So, we have ensured that the N sublists of length 100 starting at N consecutive positions (they +cycle with period N, of course) are unique with high probability. This means +we can use a +sliding window hash to quickly +identify a repeat. At this point, a solution similar to the one for +shhh +seems possible: we effectively randomize the deltas by choosing a random start index for each +node. Then, each node hashes its first 100 integers, sends the hash to all other nodes, and then +receives the hashes found by the other nodes. Then it scans forward, computing a rolling hash as +it goes, until it sees a known hash; once it does, it tells the master which node's hash it has +found, and where. Finally, a master uses that information to deduce all deltas and the value of N. +The master can do that by detecting the order in which the starting points of each node appear +in the cycle and then adding up the distances between +itself and any other node. The sum of all NumberOfNodes() distances is the value of +N. However, this approach has some issues: +

      +
    • If N is small, the probability of two deltas being equal can be large, and even 1 if + N < NumberOfNodes(). That is not unworkable, but it adds case-work code + to handle the case of the initial hash being equal for two nodes.
    • +
    • As mentioned in the + + analysis for shhh, this solution + can be slow. If the deltas for a given test case are not randomized, then in the worst case, + a node may have to scan O(N) values before recognizing a hash. Even if the deltas are + randomized, the expected largest gap before finding a hash is not reduced by a factor linear + in the number of machines, but rather by a square-root factor, so traversing that gap can + still be slow. This can be made to work with a careful implementation, but it's potentially + risky. +

    +Luckily, there is a better way: have a single canonical node hash the sublist starting at positions +that are multiples of K, and then have all nodes look for those and find a delta relative +to the canonical node. This requires no randomization and is guaranteed to have each search be at +most K steps long. With a choice like K = 107 for the gap between hashes, we get +100 different hashes to search through (the same as in the previous option), but we have a +guarantee to finish in at most K = N / 100 steps, which would be the highly unlikely best-case +scenario in the shhh-like solution. Additionally, having the +canonical node perform the same search starting from position 1 will give its delta relative to +itself with a value greater than 0, or in other words, the value of N. Another benefit is that the +delta is given directly, instead of requiring a master pass to calculate them. +We got even faster overall results with K = 106, and you can use testrun to experiment +with your code to choose parameters like this one. +

    +Notice that is possible that, instead of the "true" value of N, we will find a multiple of N +instead. This is fine, though, since the median of multiple of copies of the data is the same as +the median of the original data. (Watch out for non-odd N, though, and define median accordingly!). +We are guaranteed to find an N that is less than 109 + K, which is not significantly +larger than the actual upper bound. Once we have N and the deltas, we can essentially transform a +Large test case into a Small one, and apply the Small solution to finish the problem. +

    +As a final note, notice that, if we don't perform an advance check for numbers of unusually +high frequency, sublists starting at different places of the real data may coincide, which +would break our algorithm to find N and the deltas. +However, this would only make us get the wrong N and/or wrong deltas, which may make +our Small solution run on possibly overlapping arbitrary slices of the data for each node. +Thanks to the random shuffling, though, the median of any sufficiently long slice of data will be +the high-frequency value, and the same will be true of the union of 100 of those slices. Therefore, +we can skip the advance check and still get the correct value at the end, even if some +intermediate values like N and the deltas that we find might be wrong. +

    diff --git a/distributed_codejam/2017_finals/median/statement.html b/distributed_codejam/2017_finals/median/statement.html new file mode 100644 index 00000000..55e01491 --- /dev/null +++ b/distributed_codejam/2017_finals/median/statement.html @@ -0,0 +1,145 @@ +

    Problem

    +

    Median

    +

    + In this year's online rounds, we ran into trouble with queries of death and + broken memory, so to make it up to you, we decided to put a very easy + median-finding problem in the Finals. We were determined to avoid any + more issues with peach smoothie spills, so we asked our DCJ (Data Curating + Janitor) to set up two special arrays of 100 nodes for this problem, one for + each of the Small and Large datasets. +

    + Our DCJ prepared each array according to our instruction: +

    +
      +
    1. Choose N pieces of data to store. Since the problem is about + finding a median, each of the N pieces of data is a non-negative integer. + (The same number might appear more than once in the data.) N is also + guaranteed to be odd, because who likes finding the median of an even + number of integers?
    2. +
    3. A median-finding problem would be far too easy if the data were given + in order, so, choose one of the N! permutations of the data uniformly at + (pseudo-)random.
    4. +
    5. Write these N pieces of data, in the order of the chosen permutation, + clockwise around the edge of a circular disk, starting from a + fixed point on the very bottom.
    6. +
    7. Make one exact copy of that disk for each node to use.
    8. +
    9. A contestant can call GetData(i) on a node. Each disk has a data-reading + needle that starts at the same fixed point. That node will move its + data-reading needle to read and return the data i places away (clockwise) + from the fixed point, and then the data-reading needle will go back to + the fixed point. Since the disk is a circle, it is allowed for i to be + greater than or equal to N; the node will return the (i % N)th piece of + data. For example, for N = 5, calls to GetData(0) and GetData(5) would + return the same piece of data.
    10. +
    11. A contestant can also call GetN() to learn the value of N.
    12. +
    +

    + Our DCJ (Data Curating Janitor) completed these tasks the night before the + Finals, and, to celebrate, we threw a party and brought in a DCJ (Disc + Controlling Jockey) to play some music. Unfortunately, this morning, we + learned that the Disc Controlling Jockey mistook our array of nodes for the + Large dataset for a very elaborate turntable, and might have spun any or all + of the disks! This means that, for the Large dataset only, even though + all the nodes' disks have the same data in the same clockwise order, + their fixed points might no longer be the same. For example, a piece + of data that one node thinks is the 0th might be the 3rd on another node! +

    + Moreover, we forgot to ban flavors of smoothie other than peach, and our + Disc Controlling Jockey also spilled a strawberry smoothie all over the array + of nodes for the Large dataset. Because of this, + for the Large dataset only, the GetN() function no longer returns any + useful data. +

    + The Finals have already started, and we do not have time to fix the system. + Can you find the median of the data anyway? +

    + +

    Input

    +

    + The input library is called "median"; see the sample inputs below for + examples in your language. It defines two methods: +

    +
      +
    • GetN(): +
        +
      • Takes no argument.
      • +
      • Returns a 32-bit integer.
      • +
      • Expect each call to take 0.1 microseconds.
      • +
      +
    • +
    • GetData(i): +
        +
      • Takes exactly one 64-bit integer argument: an index i, 0 ≤ i ≤ + 1018.
      • +
      • Returns a 32-bit integer: the number that is i places away clockwise + from the node's fixed point, as described in the problem statement.
      • +
      • Expect each call to take 0.1 microseconds.
      • +
      +
    • +
    +

    + The data is generated as follows: values for N, a data set of N integers + X0, X1, ..., XN-1, and + 100 "delta" integers d0, d1, ..., d99 are + chosen by the test setter. Then, a permutation P of the integers + 0 through N-1 is chosen uniformly at (pseudo)-random*; all nodes use the same + values of N, Xs, ds and P.
    + GetN() returns N in the Small dataset and -1 in the Large dataset, on all nodes.
    + GetValue(i) on node j returns XP[(i + dj) % N].
    +

    +*: for technical reasons, the randomness used is weaker than something like +std::random_shuffle in C++ or java.util.Collections.shuffle in Java. +For transparency, this is the exact procedure used to retrieve the i-th (0-based) element out of +N of a random permutation (you do not necesarilly need to fully understand this to solve the +problem): +

    + int index = i;
    + do {
    +   int MASK = (power == 32) ? ~0 : ((1 << power) - 1);
    +   index += add;
    +   index *= m1;
    +   index &= MASK;
    +   index ^= index >> ((power / 2) + 1);
    +   index += add2;
    +   index *= m2;
    +   index &= MASK;
    +   index ^= index >> (2 * power / 3);
    + } while (index >= N);
    + return index; +

    +where power is ceil(log2(N)), and add, add2, +m1 and m2 are distinct constants between 0 and N - 1, inclusive, +such that m1 and m2 are odd. +

    + +

    Output

    +

    + Output one line with a single integer: the median of the data. +

    + +

    Limits

    +

    + Number of nodes: 100 (for both the Small and Large datasets).
    + Time limit: 7 seconds.
    + Memory limit per node: 32 MB. (Notice this is less than usual.)
    + Maximum number of messages a single node can send: 5000.
    + Maximum total size of messages a single node can send: 8 MB.
    + 1 ≤ Xi ≤ 109, for all i.
    + N % 2 = 1. (N is odd.)
    +

    + +

    Small dataset

    +

    + di = 0, for all i. (The nodes all indexed relative to the same + original fixed point.)
    +

    + +

    Large dataset

    +

    + 0 ≤ di < N, for all i.
    +

    + +The sample input has access to GetN() for all cases, but contains values of di +other than 0. To test the Small dataset, you can edit the files to have all di +equal to 0 (the answer is of course the same). To test the Large, you can just not use +the GetN() function. diff --git a/distributed_codejam/2017_r1/analysis_intro.html b/distributed_codejam/2017_r1/analysis_intro.html new file mode 100644 index 00000000..52161abf --- /dev/null +++ b/distributed_codejam/2017_r1/analysis_intro.html @@ -0,0 +1,50 @@ +

    + Our third Distributed Code Jam tournament is underway! Our first round started + with Pancakes — our third pancake-themed problem of 2017 — + in which the distribution method was relatively straightforward. + Weird Editor required a greedy aha and some more thought on how to + parallelize that insight. Todd and Steven brought part of the + classic "how would you sort a million/billion integers?" problem to DCJ. + Finally, Query of Death added a new wrinkle to DCJ: some of the nodes + would "stop working" as your code ran, and you had to work around that + dynamically! +

    + We had almost 1000 contestants on the scoreboard this time, and there were + an impressive 67 perfect scores! Top honors went to mk.al13n, who reached + 100 at 1:21:13, with a penalty time ten minutes ahead of semiexp., our + second-place finisher. To meet the top-500 threshold for advancement, you + needed to earn at least 37 points (which corresponded to A + B + C-small) + sufficiently quickly. We apologize for the high latency on judging Smalls + toward the end of the contest. +

    + The top 500 from this round will compete in Distributed Round 2 in three + weeks. There are twenty chances to head to the World Finals in Dublin! Which + familiar faces will return, and which new challengers will appear? +

    +
    +

    + Cast +

    + Problem A (Testrun): Written and prepared by Alan Smithee. +

    + Problem B (Pancakes): Written by Ian Tullis. Prepared by Mohammed + Hossein Bateni. +

    + Problem C (Weird Editor): Written by Pablo Heiber. Prepared by + Won-seok Yoo. +

    + Problem D (Todd and Steven): Written by Onufry Wojtaszczyk. Prepared + by Ian Tullis. +

    + Problem E (Query of Death): Written and prepared by Ian Tullis. +

    + Solutions and other problem preparation and review by Tomi Belan, Md Mahbubul Hasan, and Andi Purice. +

    +Analysis authors: +

    +
      +
    • Pancakes: Pablo Heiber
    • +
    • Weird Editor: Pablo Heiber
    • +
    • Todd and Steven: Ian Tullis
    • +
    • Query of Death: Ian Tullis
    • +
    diff --git a/distributed_codejam/2017_r1/pancakes/analysis.html b/distributed_codejam/2017_r1/pancakes/analysis.html new file mode 100644 index 00000000..936a8479 --- /dev/null +++ b/distributed_codejam/2017_r1/pancakes/analysis.html @@ -0,0 +1,32 @@ +

    Pancakes: Analysis

    +

    +The Infinite House of Pancakes made the jump from +Google +Code +Jam +into Distributed Code Jam! In this case, we want to serve them from a huge stack. +

    +As a first approach, we can consider simulating the described process: go around the table checking +the top of the stack and serving as appropriate, counting the number of times we complete a full +revolution. We can see that, for some of the largest cases, this is not very efficient. It is not +hard (give it a try!) to devise a case where we have to skip almost all of the diners in order to +serve the top pancake. Luckily, we can achieve the needed speed up with a simple idea: instead of +trying every possible diner, we can jump directly to the one that will get the next pancake in the +stack. When jumping, we have to check whether we complete a revolution. This is easy to verify as +we know we complete a revolution every time we serve diner j right after diner i, +with j < i. +

    +This means we can rephrase the problem in more straightforward way for a program: count the number +of consecutive pairs of integers a, b in the input sequence such that +b < a. This is trivial to do in a single node, and not so hard to distribute +either: just split the input into evenly sized parts, give one part to each worker and have it +count only the decreasing consecutive pairs in it. We have to be a careful now because we don't +need to split the input itself, but the comparisons that need to be checked. They involve two +indices each, so we need the last element of a piece to be also considered by the node which +counts the following piece. As an example, if worker 0 reads the input between indices 0 and 9999, +for instance, it will be counting 9998 inversions, and worker 1 will need to read starting at 9999, +not 10000, to be able to compare the numbers at positions 9999 and 10000. Since the number of +duplicate readings is so small, just one extra call per node, it won't heavily impact our running +time. After each node made its share of comparisons, it can just ship the total number of +revolutions it found to a single master, which adds them all up and prints the final result. +

    diff --git a/distributed_codejam/2017_r1/pancakes/statement.html b/distributed_codejam/2017_r1/pancakes/statement.html new file mode 100644 index 00000000..f8a7d34d --- /dev/null +++ b/distributed_codejam/2017_r1/pancakes/statement.html @@ -0,0 +1,111 @@ +

    Problem

    + +

    Pancakes

    + +

    + At the Infinite House of Pancakes, D hungry diners are seated + around a circular table. They are numbered from 0 at the top + of the table and clockwise around to D-1. Each diner likes a + particular type of pancake: the type with the same number as the + diner. +

    +

    + You are a pancake server with a stack of pancakes. You are currently + at the top of the table (just about to serve diner 0), and + you will walk around the table clockwise, perhaps multiple + times. Every time you pass by a diner, if the pancake at the top of + the stack is the diner's preferred kind, you will serve that + pancake. You will keep serving that diner until the top of the stack + no longer matches the diner's preference; then you will move on to + the next diner, and so on. +

    +

    + Each time you complete a revolution, after passing by + diner D-1 and before reaching diner 0 again, you + check to see if the stack is empty. If so, you are done. + Otherwise, you continue clockwise around the table for another + revolution, serving the diners. +

    +

    + For example, suppose there are 4 diners and 4 pancakes in the + stack 3, 1, 2, 0 in order from top to bottom. You start your first + revolution by skipping + diners 0, 1 and 2 to serve diner 3 the top pancake, getting the + stack down to 1, 2, 0. After that, you go back to diner 0 + (starting a second revolution), skipping it + to get to diner 1 and giving her pancake 1, getting the stack down + to 2, 0. Since she doesn't like pancake 2, you go to diner 2, which + gets it and you have only a single pancake 0 left. To serve it, you + go through diner 3 and back to 0 (starting a third revolution), + who gets the final pancake, and your job is done. You required 3 + revolutions in total for this particular arrangement, even though + you didn't need to finish the last one completely. +

    +

    + Note that there may be some diners who do not receive any pancakes. +

    +

    + Given the initial stack of pancakes, how many revolutions will you + make around the table? +

    + + +

    Input

    +

    +The input library is called "pancakes"; see the sample inputs below for +examples in your language. It defines three methods: +

      +
    • GetStackSize(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the initial number of pancakes in the stack.
      • +
      • Expect each call to take 0.8 microseconds.
      • +
      +
    • +
    • GetNumDiners(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number D of diners seated around the table.
      • +
      • Expect each call to take 0.8 microseconds.
      • +
      +
    • +
    • GetStackItem(i): +
        +
      • Takes a 64-bit number in the range 0 ≤ i < GetStackSize().
      • +
      • Returns a 64-bit integer: the i-th pancake type in the stack, where + i = 0 corresponds to the first one we plan to serve.
      • +
      • Expect each call to take 0.8 microseconds.
      • +
      +
    • +
    + +

    Output

    +

    + Output a single integer: the number of revolutions we make around + the table. +

    + +

    Limits

    +

    +Time limit: 3 seconds.
    +Memory limit per node: 128 MB.
    +Maximum number of messages a single node can send: 1000.
    +Maximum total size of messages a single node can send: 8 MB.
    +0 ≤ GetStackItem(i) < GetNumDiners().
    +

    + +

    Small dataset

    +

    +Number of nodes: 10.
    +1 ≤ GetStackSize() ≤ 105.
    +3 ≤ GetNumDiners() ≤ 106.
    +

    + +

    Large dataset

    +

    +Number of nodes: 100.
    +1 ≤ GetStackSize() ≤ 108.
    +3 ≤ GetNumDiners() ≤ 109.
    +

    + +The first sample case is the one explained in the statement. diff --git a/distributed_codejam/2017_r1/query_of_death/analysis.html b/distributed_codejam/2017_r1/query_of_death/analysis.html new file mode 100644 index 00000000..1fdf9cc0 --- /dev/null +++ b/distributed_codejam/2017_r1/query_of_death/analysis.html @@ -0,0 +1,84 @@ +

    Query of Death: Analysis

    +

    Small dataset

    +

    + It is often possible in DCJ to write sufficiently fast single-node solutions + for Small datasets. This problem is a notable exception: it is impossible to + write any single-node solution! As soon as the query of death (QoD) is + made, there is no way to get the values from any other positions... and of + course there is no way to ensure that the QoD is made last. +

    + However, it is possible to write a Small-only solution that uses only two + nodes. The first node starts at position 0 and reads each position many times. + If the returned value is always the same at a given position, then it is safe + to move on to the next position, but if the returned value is inconsistent at + a given position, then that position must be the QoD. At that point, the + first node sends the index of the QoD and the cumulative total so far + (including the correct value at the QoD position) to the second node, and + the second node starts one position after the QoD and finishes totaling up + the remaining values. If you check 100 times at each position, your odds of + getting fooled (by a broken node that appears consistent) are only about 1 in + 1032. Even with up to 10000 positions and multiple test cases, the + odds of failure are negligible; you might be better off worrying about cosmic + rays interfering with the hardware running the program! +

    +

    Large dataset

    +

    + The Large cases have many more values, and it is too slow to step through + them cautiously, and too risky to decrease the number of checks at each + position too much. It is possible to strike the right balance between caution + and speed, and to otherwise refine this strategy until it can pass the Large + (with a little luck), but this is not necessary. There is a solution that is + much less error-prone. +

    + Our solution uses a pool of worker nodes and a master node; the master + keeps track of how many and which workers are broken. It also knows the range + of positions in which the QoD could be; initially, this "range of interest" + is [0, GetLength()). The algorithm repeats this cycle: +

    +
      +
    1. The master splits the range of interest up as evenly as possible across + all workers that are not broken, and tells each worker which subrange to + look at. It keeps track of which subrange it assigned to which worker.
    2. +
    3. Each worker reads the data in its subrange and sums up the values. Then, + it checks, e.g., the last position in the subrange 100 times to see whether + the QoD has caused it to break. If so, when it sends back its total, it + also sends back an indication that it is broken. (If a worker is assigned + an empty range, it just returns a "total" of 0.)
    4. +
    5. The master totals up the values from all non-broken workers, and updates + the range of interest to match the subrange assigned to the worker that + just broke. It also removes that worker from the pool. Then the master + assigns tasks to workers again, and so on.
    6. +
    7. Eventually, a worker will report that it has broken even after being + assigned a range of only 1 position; at that point, the master will know + the position of the QoD. It can add the value from that node and the other + values from the other nodes to its total, and then return the answer.
    8. +
    +

    + Let w be the number of worker nodes, which is + NumberOfNodes() or NumberOfNodes()-1, depending on + whether you use the master as a worker. Then the size of the range shrinks by + a factor of w with the first cycle, w-1 with the second cycle, + and so on, with exactly one worker breaking in each cycle; however, the range + contracts fast enough that there is no danger of running out of workers, even + in the Small dataset. The running time is dominated by the first cycle; most + of the values are read only once, and only 1/w of them are read twice, + and only 1/(w)(w-1) are read three times, and so on. Letting + n = GetLength(), the overall running time of that first + cycle is O(n / w), and both our Small and Large datasets + use enough nodes to allow us to safely ignore the contribution from the other + cycles. There are logw(n) cycles, and the + master-worker communication in these introduces another additive factor, but + again, especially in our Large datasets, this will be drowned out by the time + taken just to read all the data. +

    + Note that this method is substantially faster than the cautious strategy + mentioned above, which has a worst-case running time of O(n * number + of checks at each position / NumberOfNodes()). +

    + A single pass of the Large idea already narrows the range of where the QoD + might be by a factor of 100, so we can use the Small solution to finish + instead of the multiple-pass algorithm explained above. You should probably + choose the option that fits your coding style best, as multiple passes over + the same code mean shorter overall code, but also slightly more complicated + (and potentially error-prone) logic. +

    diff --git a/distributed_codejam/2017_r1/query_of_death/statement.html b/distributed_codejam/2017_r1/query_of_death/statement.html new file mode 100644 index 00000000..e7570648 --- /dev/null +++ b/distributed_codejam/2017_r1/query_of_death/statement.html @@ -0,0 +1,110 @@ +

    Problem

    + +

    Query of Death

    + +

    + We planned a nice simple warm-up DCJ problem for you this year: find the sum + of many values. You can call a GetLength() function to get the + number of values and a GetValue(i) function to get the i-th of + those values; to make it even easier, each of those values is either 0 or 1. + Simple, right? Unfortunately, we have been having a technical difficulty, and + now the contest is starting and it is too late to fix it. +

    + The issue is that there is exactly one value of i — we are not sure + what that value is, but we will call it iqod — that is a + "query of death" (a term occasionally used at Google for a query with severe + adverse effects) that causes the following malfunction. The first time that + GetValue(iqod) is called on a node, the function will return the + correct iqod-th value. However, this will cause the GetValue + function to "break" on that node. After that, every future call to + GetValue(i) on that node will return 0 or 1 purely at (pseudo)random, + independently of the value of i or of any previous calls. Other nodes are + not affected when a node breaks in this way, but the malfunction can still + happen in the future: any other node on which you call + GetValue(iqod) will also break. +

    + The iqod value that causes the breakage is the same for every node + within a test case; it may vary across test cases, though. Nodes do not + remain broken across different test cases. +

    + As an example, suppose that we have two unbroken nodes A and B, and two + values iok and iqod. Then the following sequence of + calls would produce the following results: +

    +
      +
    1. GetValue(iok) on node A: the correct value is returned.
    2. +
    3. GetValue(iqod) on node A: the correct value is returned, but + node A breaks.
    4. +
    5. GetValue(iok) on node B: the correct value is returned.
    6. +
    7. GetValue(iok) on node A: a random value is returned.
    8. +
    9. GetValue(iqod) on node A: a random value is returned.
    10. +
    11. GetValue(iqod) on node B: the correct value is returned, but + node B breaks.
    12. +
    13. GetValue(iqod) on node B: a random value is returned.
    14. +
    15. GetValue(iok) on node B: a random value is returned.
    16. +
    17. GetValue(iqod) on node A: a random value is returned.
    18. +
    19. GetValue(iok) on node A: a random value is returned.
    20. +
    +

    + We apologize for the inconvenience, but can you find the sum anyway? +

    + +

    Input

    +

    + The input library is called "query_of_death"; see the sample inputs below for + examples in your language. It defines two methods: +

      +
    • GetLength(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the total number of values to be summed up. + (This function still works correctly even on a broken node.)
      • +
      • Expect each call to take 0.2 microseconds.
      • +
      +
    • +
    • GetValue(i): +
        +
      • Takes a 64-bit number in the range 0 ≤ i < + GetLength().
      • +
      • Returns a 32-bit number (which is always either 0 or 1): the + i-th value if the node is not broken, or 0 or 1 at + (pseudo)random if the node is broken.
      • +
      • Expect each call to take 0.2 microseconds.
      • +
      +
    • +
    +

    + +

    Output

    +

    +Output a single line with one integer: the sum of all of the values. + +

    Limits

    +

    +Time limit: 2 seconds.
    +Memory limit per node: 128 MB.
    +Maximum number of messages a single node can send: 1000.
    +Maximum total size of messages a single node can send: 8 MB.
    +There is exactly one iqod value, which is the same for each node, +and it is within the allowed range for GetLength().
    +0 ≤ GetValue(i) ≤ 1, for all i.
    +

    + +

    Small dataset

    +

    +Number of nodes: 10.
    +1 ≤ GetLength() ≤ 104.
    +

    + +

    Large dataset

    +

    +Number of nodes: 100.
    +1 ≤ GetLength() ≤ 108.
    +

    + +

    + The code for the samples simulates the node-breaking behavior described in + the statement; the actual test cases have the specified behavior, but the + implementation (e.g., of randomness on a broken node) is not necessarily the + same. +

    diff --git a/distributed_codejam/2017_r1/todd_and_steven/analysis.html b/distributed_codejam/2017_r1/todd_and_steven/analysis.html new file mode 100644 index 00000000..45d92639 --- /dev/null +++ b/distributed_codejam/2017_r1/todd_and_steven/analysis.html @@ -0,0 +1,94 @@ +

    Todd and Steven: Analysis

    +

    + This problem is about the final merge step of a large + mergesort. + It turns out not to be helpful that Todd's sequence has only odd values and + Steven's sequence has only even values, apart from the implication that no + value in either sequence appears again in either sequence. Even under the + odd/even constraints, the two sequences could still interleave in any way. + However, since we designed the data to have that property for our + convenience, it was only fair to tell you about it! +

    +

    Small dataset

    +

    + It is possible to solve the Small dataset on a single node by carrying out + the entire merge step on that node. We can start with a TOTAL = 0, and + indexes T = 0 and S = 0 into Todd's and Steven's sequences. Then, we can + repeatedly do the following: check whether Todd's element at T or Steven's + element at S is smaller. Let the smaller value be N; then add N XOR (T + S) + to our total, and increment T if N came from Todd's sequence or S otherwise. + As in any mergesort merge step, we must take care not to try to check an + element of a sequence after all of the elements of that sequence have been + processed. When we finish, TOTAL modulo 109 + 7 is our answer. As + usual, we need to avoid overflows by taking modulo after each operation + instead of just at the end. +

    +

    Large dataset

    +

    + For the Large dataset, we do not have enough time to merge the sequences on a + single node. We will have to distribute the data, but we cannot simply have + node 0 handle the first hundredth of Todd's sequence (and the parts of + Steven's sequence that merge with that), node 1 handle the second hundredth, + and so on. For example, it is possible that all of the values in Steven's + sequence are smaller than all of the values in Todd's sequence. In such a + case, some nodes would process well over 1/100 of the total input, and would + spend too much time doing so. +

    + What we really want is a way for the first node to handle the first hundredth + of the final merged sequence, the second node to handle the second + hundredth, and so on. The tricky part is figuring out just which ranges of + indices from each sequence are part of the first, second, etc. hundredths of + that merged sequence! We need a way to know this for a given range in the + merged sequence without actually calculating the entire merged sequence up to + that point. +

    + Intuitively, some sort of binary search should help, but it is challenging to + figure out what to binary search on. There are various possible solutions; we + will describe one that involves three binary searches but is relatively easy + to understand. +

    + Our "outer" binary search will search the range [1, 5 × 109] + to find the value of the K-th element of the merged sequence, where K will be + a value like 1/100 × L, where L is the length of the merged sequence. + When our search considers a candidate value X, we must determine whether the + value is too low, too high, or just right to be the K-th element. To do so, + we need to know the number T' of elements in Todd's sequence are less than or + equal to X, and the number S' of elements in Steven's sequence that are less + than or equal to X. If T' + S' = K, we have found our K-th element; otherwise, + we can try a higher or lower value of X, in the usual way for a binary search. +

    + Now all we need is a way to actually find the values of T' and S' above. Since + the sequences are sorted, we can use an "inner" binary search on each + sequence to look for the number of values in that sequence less than or equal + to X. +

    + All that remains is the distribution. The first of the 100 nodes can be + responsible for the range of indexes [0, L/100) into the merged sequence; the + second can handle [L/100, 2L/100), and so on. Each node uses our multiple + binary search strategy once to find each end of its range. Once a node knows + its range, it can perform the standard merge step algorithm described in the + Small dataset, and keep track of its part of the hash sum. Then, the nodes + can send their partial sums to the master, which adds them to get a final + answer. +

    + Because each step of the outer binary search involves two more inner binary + searches, the time complexity of that part of the method is O((log M)(log L)), + where M is the global maximum value among the two sequences and L = + GetToddLength() + GetStevenLength() is the length + of the merged sequence. Note that this term is not influenced by + NumberOfNodes(), since each node simultaneously binary searches + on both of those ranges. The merge step is where that savings comes in; it + takes O(L / NumberOfNodes()) time. The latter term dominates the former term, + and this distributed method is fast enough to solve the Large dataset. +

    + Other solutions are possible. It is possible to use fewer binary searches. + For example, you can establish "delimiters" in both sequences (say, 0, 1/40, + 2/40... of the way through Todd's, and 0, 1/40, 2/40... of the way through + Steven's), sort those delimiters together, take pairs of neighboring + delimiters, and then treat each pair (x, y) as a "find and sort all values + [x, y)" task. These tasks can then be distributed to workers. This method + is similar to the one described in our + analysis + for the + Air Show problem from the 2016 DCJ Finals. +

    diff --git a/distributed_codejam/2017_r1/todd_and_steven/statement.html b/distributed_codejam/2017_r1/todd_and_steven/statement.html new file mode 100644 index 00000000..473eeece --- /dev/null +++ b/distributed_codejam/2017_r1/todd_and_steven/statement.html @@ -0,0 +1,112 @@ +

    Problem

    + +

    Todd and Steven

    +

    + By now, it is a programming interview cliché: How do you sort a very + large sequence of unique integers in increasing order? Finally, we are ready + to reveal the correct answer: +

    +
      +
    1. Give all of the odd integers from the sequence to one assistant named + Todd, and ask Todd to sort those integers in increasing order.
    2. +
    3. Give all of the even integers from the sequence to another assistant + named Steven, and ask Steven to sort those integers in increasing + order.
    4. +
    5. Merge Todd's sequence with Steven's sequence to produce the final + sorted sequence.
    6. +
    +

    + Todd and Steven have already performed steps 1 and 2 on a certain sequence, + and now we would like you to perform step 3. Since the resulting sorted + sequence can be very long, we only ask you to output a hash of it as proof + that you found it. Let Xj denote the j-th element (counting + starting from 0) of the merged sequence. Please find the sum, over all j, of + (Xj) XOR j. (Here XOR refers to the + bitwise operation + between the two integers, represented in both C++ and Java by the operator + ^.) Since the output can be a really big number, we only ask you + to output the remainder of dividing the result by the prime 109+7 + (1000000007). +

    + +

    Input

    +

    + The input library is called "todd_and_steven"; see the sample inputs below + for examples in your language. It defines two methods: +

    +
      +
    • GetToddLength(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of values in Todd's sorted + sequence.
      • +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    • GetToddValue(i): +
        +
      • Takes a 64-bit integer argument in the range 0 ≤ i < + GetToddLength().
      • +
      • Returns a 64-bit integer: the i-th value of Todd's sorted + sequence.
      • +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    • GetStevenLength(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of values in Steven's sorted + sequence.
      • +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    • GetStevenValue(i): +
        +
      • Takes a 64-bit integer argument in the range 0 ≤ i < + GetStevenLength().
      • +
      • Returns a 64-bit integer: the i-th value of Steven's sorted + sequence.
      • +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    + +

    Output

    +

    + Output one line with a single 64-bit integer: the sum described in the + problem statement, modulo the prime 109+7 (1000000007). +

    + +

    Limits

    +

    + Time limit: 4 seconds.
    + Memory limit per node: 128 MB.
    + Maximum number of messages a single node can send: 1000.
    + Maximum total size of messages a single node can send: 8 MB.
    + 1 ≤ GetToddValue(i) ≤ 5 × 109, for all + i.
    + GetToddValue(i) < GetToddValue(i + 1), for all + i. (Todd's sequence is sorted in increasing order.)
    + GetToddValue(i) % 2 = 1, for all i. (All elements in Todd's + sequence are odd.)
    + 1 ≤ GetStevenValue(i) ≤ 5 × 109, for all + i.
    + GetStevenValue(i) < GetStevenValue(i + 1) for + all i. (Steven's sequence is sorted in increasing order.)
    + GetStevenValue(i) % 2 = 0, for all i. (All elements in Steven's + sequence are even.)
    +

    + +

    Small dataset

    +

    + Number of nodes: 10.
    + 1 ≤ GetToddLength() ≤ 106.
    + 1 ≤ GetStevenLength() ≤ 106.
    +

    + +

    Large dataset

    +

    + Number of nodes: 100.
    + 1 ≤ GetToddLength() ≤ 109.
    + 1 ≤ GetStevenLength() ≤ 109.
    +

    diff --git a/distributed_codejam/2017_r1/weird_editor/analysis.html b/distributed_codejam/2017_r1/weird_editor/analysis.html new file mode 100644 index 00000000..f36255e6 --- /dev/null +++ b/distributed_codejam/2017_r1/weird_editor/analysis.html @@ -0,0 +1,77 @@ +

    Weird Editor: Analysis

    +

    Small dataset

    +

    +This problem has a deceptively simple single-node solution if tackled +from the right angle. First, notice that the order in which we apply the operations +doesn't matter, so we only need to pick which digits to remove and we can append +the appropriate number of zeroes at the end. +Naturally, we want to prioritize maximizing the most significant digit, then give +second priority to maximizing the second most significant digit, and so on. +We can then see that if the first occurrence of the maximum digit in our number +is at index i, we should remove all digits up to and including index i - 1. +Moreover, if there are any other occurrences of that maximum, we want to remove +any other digits between those occurrences, to ensure that our result starts +with as many copies of the maximum as possible. +

    +Combining the above insights, we can see that we should remove any non-maximum digit +that appears before a maximum digit. What about the digits that follow the last +occurrence of the maximum digit? These represent a smaller instance of the same problem +(with a new maximum digit), so we could solve it in the same way. +This approach can be tricky to implement and can take O(N2) time, +where N = GetNumberLength(), in the worst case; a variant that makes one +forward pass through the data for each digit is O(10N) and is fast enough to pass the +Small. (With 10 nodes available for the Small, this solution can even be distributed.) +But we can do even better by approaching the problem in reverse, from the least +significant digit backwards to the most. +

    +Let N = GetNumberLength() and di = GetDigit(i). +Consider the last digit dN-1 in the input. It is always optimal to to not +remove it; if it is a zero, removing it does nothing, and otherwise, removing it +would be strictly worse. +Now consider the next to last digit dN-2. For any fixed string of digits +s that we choose to keep from among the first N-2, our options for the final value +are either sdN-2dN-10000... or sdN-1000.... +For all s, the former is larger if and only if dN-2 ≥ dN-1. +This same argument generalizes to all digits: we should keep a digit +di if there is no larger digit to its right. Notice that we have arrived +at the same conclusion as before. +

    +We can adapt this idea into a single-node solution as follows: from the least significant +digit to the most significant, keep track of the maximum digit M seen so far, and use the +current digit di if di ≥ M. To accumulate the actual result, we +should also keep a running total T and the number of digits used so far D. Then, +to represent appeding the current digit di to the left of our new number, +we add di × 10D to T. +At the end, we need to append N-D zeroes to the right, which we can do by multiplying +by 10N-D. All operations need to be done modulo 109 + 7 to avoid +overflows. Exponentiating can be done in +logarithmic time on the +exponent, which makes this solution require only O(N log N) time and a single pass over the +input. +

    +

    Large dataset

    +

    +As usual in Distributed Code Jam, even a linear solution is too slow +for the Large dataset, and we need to improve further. If we take the +usual route of dividing the input into NumberOfNodes() +evenly sized chunks so that each worker can handle one of them, we'll +quickly notice that, other than the worker handling the rightmost +piece, each worker is missing the a key piece of information. When +starting from digit dj and going backwards, we need to know +the largest digit occurring between indices j+1 and N-1 — that is, +max(dj+1, dj+2, ..., dN-1) — +to initialize the "maximum so far" value M from our single-node algorithm. +To calculate those NumberOfNodes() additional values, we can +do a preliminary pass and have each worker send the largest value +from within its range max(di, di+1, ..., dj) +for an inclusive range [i, j]. Then, a +single master can do a linear pass and calculate the value needed +for worker k as the maximum of the maxima reported by workers +k+1 through NumberOfNodes()-1. This +preliminary pass technique is similar to the one needed for the + +Crates problem from Round 1 of Distributed Code Jam 2016. +

    +Other more complicated single-node solutions can also be distributed effectively, +but with significantly more effort than the right-to-left solution presented here. +

    diff --git a/distributed_codejam/2017_r1/weird_editor/statement.html b/distributed_codejam/2017_r1/weird_editor/statement.html new file mode 100644 index 00000000..e4523708 --- /dev/null +++ b/distributed_codejam/2017_r1/weird_editor/statement.html @@ -0,0 +1,83 @@ +

    Problem

    + +

    Weird Editor

    + +

    + You just installed a text editor in your computer to edit a text file containing + only a positive integer (in base 10). Unfortunately, the editor you installed was + not as versatile as you would have hoped. +

    +

    + The editor supports only one operation: choosing and removing any digit, and + concatenating one 0 (zero) at the right end of the sequence. + In this way, the length of the digit sequence is always preserved. +

    +

    + For instance, suppose your initial digit sequence is 3001. If you applied the + operation to the third digit from the left, you would obtain 3010. If you then + applied the operation on 3010 to the second digit form the left, you would obtain + 3100. In this case, 3100 is the largest result that can be obtained + using the allowed operation zero or more times. +

    +

    + What is the maximum number that can be the result of applying the allowed operation + to the given input number zero or more times? Since the output can be a really big number, + we only ask you to output the remainder of dividing the result by the prime + 109+7 (1000000007). +

    + + +

    Input

    +

    +The input library is called "weird_editor"; see the sample inputs below for +examples in your language. It defines two methods: +

      +
    • GetNumberLength(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of digits in the number you're + given.
      • +
      • Expect each call to take 0.11 microseconds.
      • +
      +
    • +
    • GetDigit(i): +
        +
      • Takes a 64-bit integer in the range 0 ≤ i < + GetNumberLength().
      • +
      • Returns a 64-bit integer: The i-th digit in the given number. Digits + are numbered from left (most significant) to right (least significant). + That is, GetDigit(0) is the most significant digit and + GetDigit(GetNumberLength() - 1) is the least significant digit.
      • +
      • Expect each call to take 0.11 microseconds.
      • +
      +
    • +
    + +

    Output

    +

    + Output a single integer: the maximum number that can be obtained by applying the allowed + operation to the input number zero or more times. Output that number modulo the prime + 109+7 (1000000007). +

    + +

    Limits

    +

    +Time limit: 3 seconds.
    +Memory limit per node: 128 MB.
    +Maximum number of messages a single node can send: 1000.
    +Maximum total size of messages a single node can send: 8 MB.
    +0 ≤ GetDigit(i) ≤ 9, for all i.
    +GetDigit(0) ≠ 0.
    +

    + +

    Small dataset

    +

    +Number of nodes: 10.
    +2 ≤ GetNumberLength() ≤ 106.
    +

    + +

    Large dataset

    +

    +Number of nodes: 100.
    +2 ≤ GetNumberLength() ≤ 109.
    +

    diff --git a/distributed_codejam/2017_r2/analysis_intro.html b/distributed_codejam/2017_r2/analysis_intro.html new file mode 100644 index 00000000..20c1d494 --- /dev/null +++ b/distributed_codejam/2017_r2/analysis_intro.html @@ -0,0 +1,55 @@ +

    + In the second of our two online DCJ rounds, contestants contended with four + more distributed challenges. Flagpoles was by far the most accessible + of the four, with many possible solutions. Number Bases required a + combination of mathematical reasoning and careful distribution of an addition + with carries. Broken Memory presented a situation superficially + similar to the one in the first online round's + Query of Death, + in which nodes could behave unreliably... but the solution method was rather + different this time. Finally, the tough Nanobots was approachable + using a distribution technique similar to one that worked for one of our 2016 + Finals problems. +

    + By the end of the first hour of the contest, at least one Large solution had + been submitted for each problem, and complete submissions started coming in + around the two-hour mark. Many solutions to E-large poured in in the last few + minutes of the contest, and before judging, we had many more 100s on the + board then there are slots for advancement. But once judging was complete, + only 14 perfect scores and 31 E-large solutions remained. To advance, it was + necessary but not sufficient to solve E-large; the cutoff was a sufficiently + fast 75 points. fagu emerged the winner with a penalty time of 2:30:37, our + defending champion bmerry was second, and krijgertje was third (and had the + first perfect score with no wrong tries or Large resubmissions). +

    + Our 21 advancers have one more challenge left: the Distributed World Finals! + These will take place in Dublin, one day before the regular Code Jam World + Finals. As a reminder, the results for both rounds are provisional until + official advancement emails are sent, but it currently appears that five + contestants will compete in both Finals; could one of them win both? That + could be tough, since the competitions do not test exactly the same + skillsets; this year, there is no overlap among the top 5 qualifiers for both + Finals. But anything can happen in DCJ; join us in August to find out who + will take the title! +

    +
    +

    + Cast +

    + Problem A (Testrun): Written and prepared by Nicolas Bourbaki. +

    + Problem B (Flagpoles): Written by Pablo Heiber. Prepared by Yerzhan + Utkelbayev. +

    + Problem C (Number Bases): Written by Pablo Heiber. Prepared by + Won-seok Yoo. +

    + Problem D (Broken Array): Written and prepared by Pablo Heiber. +

    + Problem E (Nanobots): Written and prepared by Pablo Heiber. +

    + Solutions and other problem preparation and review by Mohammed Hossein + Bateni, Md Mahbubul Hasan, Andi Purice, Ian Tullis, and Yerzhan Utkelbayev. +

    + All analyses by Pablo Heiber. +

    diff --git a/distributed_codejam/2017_r2/broken_memory/analysis.html b/distributed_codejam/2017_r2/broken_memory/analysis.html new file mode 100644 index 00000000..763e78b8 --- /dev/null +++ b/distributed_codejam/2017_r2/broken_memory/analysis.html @@ -0,0 +1,99 @@ +

    Broken Memory: Analysis

    + +

    Small dataset

    + +

    +In this problem, for the first time +(arguably) +in DCJ history, the input data is different on each node. Since +the output asks for the location of the points of difference for all nodes, even the most efficient +solution must use every node in some way. There is no sneaking through this problem with a purely +single-node solution! +

    +The Small dataset is an exercise in understanding an unusual statement and knowing your DCJ +primitives. The input size is small enough that you can have every node read the entire input, ship +everything to a master node, and then have the master node compare all of the data to see where the +points of difference are for each node. +

    + +

    Large dataset

    + +

    +As usual, the Large dataset is where the real challenge lies. There is too much data for a node to +send, so we need to send a "summary" instead. This summary must be compact and must allow us to +check for differences; a natural solution is +hashing. Choosing a hash function for +this problem is not trivial; we discuss it in a separate section below. We will proceed assuming +that we have a workable hash function. +

    +It would be useless to have each node send a single hash of the complete data, and then compare +those hashes; they would all be different, because all the nodes have different data. What if we +instead send a hash of some initial region of the data? If two nodes have equal hashes for that +region, then their points of difference must not be in that region; if they have unequal hashes, +at least one of the nodes has a point of difference in that region. +

    +We can generalize this insight into an approach resembling binary search. We start by splitting the +data into two intervals. We compare hashes from every node for each interval, and keep only the +intervals where hashes differ. Then we divide each of those intervals into two intervals, and so +on. Since there are exactly NumberOfNodes() = 100 broken positions overall, the number +of surviving intervals at the end of each step will never exceed 100, although we may examine as +many as 20000 (200 intervals, each calculated on each node) in a step. However, if we try to use +all nodes at once, this method will be too slow. Notice that when we compare the 100 hashes for a +given interval, we know that at least 99 of them will be the same. That sounds like wasted effort! +

    +A better solution is to pair up the nodes and run the same hash-comparison algorithm for each pair +in parallel. Now, the number of surviving intervals per stage is at most 2 instead of 100, and +since all the pairs are being compared in parallel, the process should be about 50 times faster. +The only issue is that for each pair, we will find the locations of the two points of difference, +but for each one, we won't know which node in the pair has the correct value and which has the +different value. We can easily take care of that in a final step by running the Small solution +discussed above on the 100 points of difference instead of the entire input. Alternatively, we can +simply compare the data from the two points of difference retrieved from each pair of nodes with +the corresponding data from any other node, which will be guaranteed to have the correct data at +both of those points. +

    +Halving the intervals in each step of our algorithm results in log_2 N stages. Since each step +requires sending hashes, our running time includes a term of log_2 N times this communication +latency. If we divide the intervals into K parts instead, we will get log_K N stages; this reduces +communication latency but makes the computational cost of each step larger. The Testrun "problem" +can be used to find a good compromise value of K; you can generate your own worst-case dataset to +test on easily by tweaking the code in the provided samples. In our implementation, we got good +results with K = 40. However, the solution is so fast that the differences are small. +

    + +

    A word on hashing

    + +

    +The described solution relies heavily on hashing without going into much detail about it. The first +thing to notice is that our use of hashing is really specific: we need to be able to accurately +compare two integer sequences of the same length that differ at up to two positions at the most. +That's quite different from standard applications like a hash table. In a hash table, we need +millions or more hashes to not collide with each other, so the probability of collision has to be +very small. In our case, we are comparing a much smaller number of hashes, and only two at a time, +so we can get away with somewhat larger collision probabilities. On the other hand, we are hashing +lots of long sequences that are mostly the same, and it would be better to be able to do that +without always going through the entire sequence, as simple hashing mechanisms generally do. +

    +A common trick to define a function that depends on two indices f(i, j) and +represents something about the subsequence between i and j is to define it as +f(i, j) = g(j) - g(i-1), where g is something that we can precalculate +efficiently. Since the domain of g is only linear on the size of the input, this is plausible in +our case. The canonical example of use of this technique is the sum, where +f(i, j) = sum of the subsequence between the indices i and j and +g(k) = sum of the subsequence between indices 0 and k. Of course, the sum is a really +bad hash in our case; for example, the sequences 1 3 3 7 and 1 7 3 3 will hash to the same value +despite differing at only 2 positions. (Notice that the sum would be a perfect hashing if we were +guaranteed to have a difference of at most 1 position between the sequences.) Choosing any +commutative operation, like the product, won't help, as any commutative operation will be equal +when computed over two sequences that have the same multiset of values, as in the example above. +

    +A simple solution to this problem is to define f as the sum over the indices k between +i and j of something other than GetValue(k). In particular, we +can include the value of k itself and sum, for instance, +k × GetValue(k). This breaks commutativity. Unfortunately, for +this particular option, collisions are pretty easy to produce by engineering the input sequences +carefully. However, we can use hash1(k) × hash2(GetValue(k)) where +hash1 and hash2 are fast hash functions for integers. In this case, to produce collisions by +engineering the sequences, the DCJ staff would need to break the hash functions, which is assumed +to be an almost impossible task. +

    diff --git a/distributed_codejam/2017_r2/broken_memory/statement.html b/distributed_codejam/2017_r2/broken_memory/statement.html new file mode 100644 index 00000000..a30b27c0 --- /dev/null +++ b/distributed_codejam/2017_r2/broken_memory/statement.html @@ -0,0 +1,92 @@ +

    Problem

    + +

    Broken Memory

    + +

    +As you may remember from +last +year, +we have a tendency to screw things up at the worst possible moment. For 2017, we promised ourselves +that we wouldn't misplace any test cases, though, and we delivered. Unfortunately, last night we +spilled a peach smoothie over some of our Google Cloud servers. We immediately called the +Dedicated Cloud Janitor (DCJ), but he was fed up with our continuous messes and refused to help. +So, we are turning to our most powerful allies, our contestants, to once again rescue us from +ourselves. +

    +Fortunately, the servers were already loaded with the data for the problems, so before the damage, +the memory was exactly the same in all of them. +We conducted a preliminary investigation that revealed that every node's memory was damaged in +a different place. +

    +We have narrowed the search to a relatively small part of the memory, and we have encoded that as a +list of integers for your convenience. Each node has the same list of integers, except at exactly +one damaged position; the value there will be different from the corresponding value on all +other nodes. +

    +For example, suppose the original data is represented by the following list of integers: 1 5 9 3 1. +Suppose the broken index for node 0 is 2. That means that when requesting the values for indices +0, 1, 3 and 4, the returned value will be correct (1, 5, 3 and 1, respectively). However, +when requesting the value for index 2 on node 0, the returned value could be 1 or 5 or 352462352, +for example, but definitely not the correct one (9). If, on the other hand, node 1's broken index +is 0, then, when requesting the value of indices 1, 2, 3 and 4, the correct values (5, 9, 3 and 1, +respectively) will be returned, but when requesting index 0, the returned value could be 9 or 5 or +379009, but definitely not the correct one (1). Notice that for node 3, neither index 2 nor index 0 +can be the broken index, because each node is broken at a different index from all other nodes. +

    +Can you find the broken index on each node for us? +

    + +

    Input

    +

    +The input library is called "broken_memory"; see the sample inputs below for +examples in your language. It defines two methods: +

    +
      +
    • GetLength(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of values in the part of the memory where all the damage + happened.
      • +
      • Expect each call to take 0.02 microseconds.
      • +
      +
    • +
    • GetValue(i): +
        +
      • Takes a 64-bit number in the range 0 ≤ i < GetLength()
      • +
      • Returns a 64-bit integer: the i-th value in the memory.
      • +
      • Expect each call to take 0.02 microseconds.
      • +
      +
    • +
    + +

    Output

    +

    +Output a single line with NumberOfNodes() integers: the broken index for each node, +in ascending order of node ID.

    + +

    Limits

    +

    +Time limit: 2 seconds.
    +Memory limit per node: 256 MB.
    +Maximum number of messages a single node can send: 1000.
    +Maximum total size of messages a single node can send: 128 KB. (Notice that this is less than + usual.)
    +1 ≤ GetValue(i) ≤ 1018, for all i.
    +

    + +

    Small dataset

    +

    +Number of nodes: 10.
    +10 ≤ GetLength() ≤ 1000.
    +

    + +

    Large dataset

    +

    +Number of nodes: 100.
    +100 ≤ GetLength() ≤ 107.
    +

    + +

    +In this problem, the sample inputs are valid for running on 10 nodes, and the sample outputs + are computed using 10 nodes. +

    diff --git a/distributed_codejam/2017_r2/flagpoles/analysis.html b/distributed_codejam/2017_r2/flagpoles/analysis.html new file mode 100644 index 00000000..61c2c4e7 --- /dev/null +++ b/distributed_codejam/2017_r2/flagpoles/analysis.html @@ -0,0 +1,76 @@ +

    Flagpoles: Analysis

    + +

    +As we can see from the definition in the problem statement, a consecutive set of flagpoles having +collinear tips is equivalent to the corresponding input values forming an +arithmetic progression. +This means the problem translates into finding the longest contiguous subsequence forming an +arithmetic progression. Since the difference between any two consecutive elements in the +progression is the same, we can simplify this further by considering an altered input: the list of +differences between consecutive elements. A contiguous subsequence of size k which is an +arithmetic progression in the input corresponds to a contiguous subsequence of size k-1 in +the altered input. After having a special case for an input with a single element, this poses no +additional problem as there is always an arithmetic progression of length at least 2. +

    +From the paragraph above, we now have to solve a much easier problem: given a list of integers, +find the length of the longest consecutive subsequence of elements that are all equal. +From this point, we will focus on solving the altered problem. The result we look for can then +be found simply by adding 1 to the result of the altered problem. +

    + +

    A single-node solution

    + +

    +The problem is almost trivial on a single node in quadratic time, and a simple loop can also solve +it in linear time, as the following pseudocode illustrates: +

    +previous = first element of the altered input
    +current = best = 1
    +for each x in the altered input except the first:
    +  if x == previous:
    +    current = current + 1
    +    best = max(best, current)
    +  else:
    +    previous = x
    +    current = 1
    +
    + +

    A distributed solution

    + +

    +While the single node solution is good enough to pass the Small dataset, we need to go sublinear to +get a shot at the Large set, as there isn't enough time to even read the entire input on a +single node. +

    +The first thing to try, as it's common practice in Distributed Code Jam, is to partition the input +into 100 evenly-sized pieces and get each node to process one piece. If we do that, we can +immediately notice there are two distinct cases: +

      +
    1. The longest contiguous subsequence of equal numbers is all contained in one piece.
    2. +
    3. It spans multiple pieces.
    4. +

    +Case 1 is easy to resolve with our division strategy: each node will report to a master it's +internal result and the master will just take the maximum. However, this will overlook case 2, +so we need to do something extra. Examining the result for this case more closely, we can see that +the longest contiguous subsequence starts inside some node i, then covers nodes +i+1, i+2, ...,j-2, j-1 completely, and then finishes somewhere within +node j, for some pair of indices i < j (if j = i + 1, then no +node is covered completely). That means that, for each node, only a prefix or suffix will be +important (in nodes where the all the elements in the split are equal, the coverage is both a +prefix and a suffix). Putting it all together, each node needs to calculate and report to a master: +

      +
    • An internal result (to cover case 1).
    • +
    • The leftmost and rightmost elements in its range.
    • +
    • The length of the largest prefix of elements equal to its leftmost element.
    • +
    • The length of the largest suffix of elements equal to its rightmost element.
    • +

    +With that information, the master can calculate the best option for case 2 by trying every possible +pair of nodes (since there are only 100): for each i < j, we can consider a +solution if the rightmost element of i is equal to the leftmost element of j, and for +each k, i < k < j, the leftmost and rightmost element of k are +equal and equal to the other two, and the size of k's longest suffix and prefix coincides +with its range length (that is, all elements inside node k are equal). In that case, a +possible result is the longest suffix from i + the longest prefix from j + the length +of the range for each each k. This part can be optimized to be linear in the number of +nodes, but there is no need for it, since the number of nodes is so small. +

    diff --git a/distributed_codejam/2017_r2/flagpoles/statement.html b/distributed_codejam/2017_r2/flagpoles/statement.html new file mode 100644 index 00000000..882d2780 --- /dev/null +++ b/distributed_codejam/2017_r2/flagpoles/statement.html @@ -0,0 +1,82 @@ +

    Problem

    + +

    Flagpoles

    +

    + Cody-Jamal, the famous conceptual artist, was called to design the new United Nations +headquarters. The entrance displays a single row of flagpoles with the flags of different countries. +Each flagpole is exactly 1 meter away from its neighbor(s). Since different nations have different +rules about how high their flags must be flown, the tips of the flagpoles may have different +heights. +

    + The scientists from the famous Detecting Collinearity Journal have become interested in the +flagpoles. In particular, they want to know the maximum number of consecutive flagpoles with tips +that are collinear. A set of contiguous flagpoles has collinear tips if there is a constant d +such that, for every pair of adjacent flagpoles in the set, the height of the right flagpole's tip +minus the height of the left flagpole's tip is equal to d. Notice that the condition is always +true for a set of up to 2 flagpoles. +

    +For example, if the flagpoles' heights are 5, 7, 5, 3, 1, 2, 3, in left-to-right order, the +leftmost 2 flagpoles and the rightmost 3 flagpoles are examples of consecutive sets of flagpoles +with collinear tips. The flagpoles with heights 7 and 1, together with those in between them, +are another example. The leftmost 3 flagpoles, however, do not have collinear tips, so they do not +form such a set. +

    + Given the height in meters of each flagpole tip, in the left-to-right order in which they appear, +can you help the DCJ calculate the maximum size of a set of consecutive flagpoles with collinear +tips? +

    + +

    Input

    +

    +The input library is called "flagpoles"; see the sample inputs below for examples in your +language. It defines two methods: +

    +
      +
    • GetNumFlagpoles(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of flagpoles in the row.
      • +
      • Expect each call to take 0.17 microseconds.
      • +
      +
    • +
    • GetHeight(i): +
        +
      • Takes exactly one 64-bit integer argument: a position i, 0 ≤ i < + GetNumFlagpoles().
      • +
      • Returns a 64-bit integer: the height, in meters, of the flagpole at the ith position from left + to right. The ith flagpole is always i meters to the right of the 0th flagpole.
      • +
      • Expect each call to take 0.17 microseconds.
      • +
      +
    • +
    + +

    Output

    +

    +Output one line with a single integer: the maximum number of consecutive flagpoles with collinear +top ends. +

    + +

    Limits

    +

    +Time limit: 3 seconds.
    +Memory limit per node: 512 MB.
    +Maximum number of messages a single node can send: 1000.
    +Maximum total size of messages a single node can send: 8 MB.
    +1 ≤ GetHeight(i) ≤ 1018.
    +

    + +

    Small dataset

    +

    +Number of nodes: 10.
    +1 ≤ GetNumFlagpoles() ≤ 106.
    +

    + +

    Large dataset

    +

    +Number of nodes: 100.
    +1 ≤ GetNumFlagpoles() ≤ 109.
    +

    + +

    +Sample input 1 is the example given in the problem statement. +

    diff --git a/distributed_codejam/2017_r2/nanobots/analysis.html b/distributed_codejam/2017_r2/nanobots/analysis.html new file mode 100644 index 00000000..558fd836 --- /dev/null +++ b/distributed_codejam/2017_r2/nanobots/analysis.html @@ -0,0 +1,79 @@ +

    Nanobots: Analysis

    +

    Small dataset: a single-node solution

    +

    +Let N = GetRange() and M = GetNumNanobots(). +A straightforward O(N2) solution is to simply query each +possible combination of speed and size and count the Ts. +This, however, is obviously slow. We can refine it by noticing that +the valid datasets are not just any combinations of Ts +and Es. If a bacterium with speed c and size d is trapped, +so is any bacterium with speed <c and size d, or speed c and size +<d. We can display the data as an N-by-N matrix in which +columns represent increasing values (from left to right) for speed +and rows represent increasing values (from top to bottom) for size. +The key observation is that every row or column will have zero or more +Ts and then only Es. We will get a region +of Ts in a shape like the following. (In this example, the +team consists of one nanobot with speed 5 and size 3, one with speed +4 and size 4, one with speed 3 and size 5, and one with speed 2 and +size 6.) +

    + +TTTTEEE
    +TTTTEEE
    +TTTEEEE
    +TTEEEEE
    +TEEEEEE
    +EEEEEEE
    +EEEEEEE
    +

    +We can use this to our advantage. If we follow along the border between +the two types of letters, we can calculate the size of the region of Ts +(which is the answer to the problem) in +O(N) time, because the border has a length of at most 2N. +We can start at a corner of the matrix with minimum speed and maximum size, and reduce the size +until we find a bacterium that can be trapped at size x. +The rest of the row also consists of +Ts, so we can count x towards our answer. Then we can increase the speed until a +bacterium is no longer trapped (continuing to add x to the result for +each row that we pass), then reduce the size again for the new row, and so on. +

    +We can further refine that solution by noticing that each step through +a segment can be done using binary search instead of linear search, +because each row or column has at most a single place at which we change +from T to E. This makes the solution +require only O(M) binary searches (and in the worst case, M is a lot +smaller than N), for an overall complexity of O(M log N). This is +sufficient to solve the Small dataset on a single node. +

    +

    Large dataset: a distributed solution

    +

    +For the Large, a single node solution is too slow. If we partition +the matrix into strips of rows, it's possible that a single partition +contains all the corners, so the slowest node still needs to +process O(M) segments by running O(M) binary searches. +We need to use a trick similar to those required for for the +Gold +problem from 2016. One option is to partition the input +into a lot more than NumberOfNodes() pieces and assign +each node a random subset of the pieces, such that the expected number +of corners for the one that gets the most is only +M / sqrt(NumberOfNodes()). Notice that this increases the total +number of required binary searches by the number of pieces, so there is a tradeoff +there, but as long as the number of pieces is significantly smaller than M, it should +be fine. Notice that all pieces have the steplike shape that makes the solution possible. +

    + Another option, more complicated +to code but that does not rely on randomness, is to start our work by +dividing evenly, but only +let workers examine up to N / NumberOfNodes() segments. +After this is done, a few workers may have not have finished, but they +can report their work to a master node, that in turn reassigns the +unfinished portions for a second pass. As it is shown in the +analysis +for Gold, +only a logarithmic number of +passes are needed in the worst case, and it works pretty well in +practice. Please see the analysis for that problem for more details on the math +behind this distribution technique. +

    diff --git a/distributed_codejam/2017_r2/nanobots/statement.html b/distributed_codejam/2017_r2/nanobots/statement.html new file mode 100644 index 00000000..c66a537c --- /dev/null +++ b/distributed_codejam/2017_r2/nanobots/statement.html @@ -0,0 +1,109 @@ +

    Problem

    + +

    Nanobots

    +

    + A group of medical researchers is working on a new treatment to fight bacteria. In the + treatment, special nanobots are transferred into the patient's body, where they + locate and trap any harmful bacteria. This allows the patient to get better, and the + researchers can later retrieve the trapped bacteria for further study. +

    + Not any nanobot can trap any bacterium, though. A nanobot can be characterized by two traits: + size and speed. A nanobot can only trap bacteria that are smaller than it (otherwise, the + bacteria will not fit in the nanobot's cage) and also slower than it (otherwise, the bacteria can + escape the nanobot). Formally, a nanobot with speed A and size B can trap a bacterium with speed + C and size D if and only if A > C and B > D. The speeds and sizes of both nanobots and + bacteria are in the inclusive range [1, GetRange()]. +

    +

    + You have a group of nanobots and you want to know how effective they are at trapping bacteria. + Your goal is to find how many of the GetRange()2 possible bacteria get + trapped by the team of nanobots. Unfortunately, you cannot directly examine the speed and size + of your nanobots. You can only experiment by introducing a bacterium with a specific size and + speed, and watching whether this bacterium gets trapped by the team of nanobots or not. + A team of nanobots will trap a bacterium with speed C and size D if and only if there is at + least one nanobot in the team that has both speed strictly greater than C and size strictly + greater than D. +

    + You may carry out as many experiments of this sort as you want... within the allowed running time + for the problem, of course! You can choose the speed and size of the bacteria in each experiment, + and for each one, you receive one piece of data: whether or not the bacteria was trapped. + Each experiment uses the full team of nanobots, and the same nanobot can catch bacteria in + different experiments. Based on + that information, you need to determine how many of the GetRange()2 + possible bacteria would be trapped by the team of nanobots. (Because the speed can take any + integer value in [1, GetRange()], and the same is true for the size, there are + GetRange()2 possible bacteria.) Since the output can be a really big + number, we only ask you to output the remainder of dividing the result by the prime + 109+7 (1000000007). +

    + Distributed Code Jam is not a licensed physician. Nothing in this problem statement should be + construed as an attempt to offer medical advice. Distributed Code Jam is also not a licensed + scientist. Nothing in this problem statement should be construed as an attempt to offer + scientific advice. Using nanobots to fight harmful bacteria definitely sounds cool, though. +

    + +

    Input

    +

    +The input library is called "nanobots"; see the sample inputs below for examples in your +language. It defines three methods: +

    +
      +
    • GetNumNanobots(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of nanobots on the team.
      • +
      • Expect each call to take 0.2 microseconds.
      • +
      +
    • +
    • GetRange(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the maximum valid value for speeds and sizes of + both bacteria and nanobots.
      • +
      • Expect each call to take 0.2 microseconds.
      • +
      +
    • +
    • Experiment(c, d): +
        +
      • Takes exactly two 64-bit integer arguments: a size c and a speed d, + 1 ≤ c,d ≤ GetRange().
      • +
      • Returns a char: T if a bacteria with size c and speed d is trapped by + the nanobots, or E if it escapes.
      • +
      • Expect each call to take 0.2 microseconds.
      • +
      +
    • +
    + +

    Output

    +

    + Output one line with a single integer: how many of the different bacteria with both size and speed + in the inclusive range [1, GetRange()] would be trapped by the nanobots, + modulo the prime 109+7 (1000000007). Two bacteria are considered different if and only + if they have different speed and/or different size. +

    + +

    Limits

    +

    +Time limit: 18 seconds.
    +Memory limit per node: 256 MB.
    +Maximum number of messages a single node can send: 1000.
    +Maximum total size of messages a single node can send: 8 MB.
    +2 ≤ GetRange() ≤ 1012.
    +Experiment(c, d) is either uppercase T or uppercase E, for + all c, d.
    +The results of Experiment are consistent with the same team of +GetNumNanobots() nanobots across all nodes.
    +It is possible that multiple nanobots might have the same size and speed.
    +

    + +

    Small dataset

    +

    + Number of nodes: 10.
    + 1 ≤ GetNumNanobots() ≤ 105.
    +

    + +

    Large dataset

    +

    + Number of nodes: 100.
    + 1 ≤ GetNumNanobots() ≤ 107.
    +

    diff --git a/distributed_codejam/2017_r2/number_bases/analysis.html b/distributed_codejam/2017_r2/number_bases/analysis.html new file mode 100644 index 00000000..5fca2037 --- /dev/null +++ b/distributed_codejam/2017_r2/number_bases/analysis.html @@ -0,0 +1,94 @@ +

    Number Bases: Analysis

    +

    A non-distributed solution

    +

    +Even for the Small limits, checking a single base for correctness is time consuming, so trying +many bases is a non-starter proposition. Luckily, we have our math training. You may have +learned about doing addition by carry +in elementary school, and possibly learned about how that +generalizes to +arbitrary bases some time later. Either way, we can try to simulate the carry algorithm, +deferring the decision of what base we are using for as long as we can. +

    +To simplify, we'll let X[i] denote the i-th digit of X (that is, GetX(i), with +increasing i meaning more significant digits), and we'll define Y[i] and Z[i] similarly. +We start by adding X[0] + Y[0], remembering that we need (X[0] + Y[0]) % B = Z[0]. +

      +
    • If X[0] + Y[0] = Z[0], the requirement always holds for valid bases and there is no carry +in any of those.
    • +
    • If X[0] + Y[0] < Z[0], the case is IMPOSSIBLE, since we already know that B > Z[0] +and this implies (X[0] + Y[0]) % B = X[0] + Y[0].
    • +
    • If X[0] + Y[0] > Z[0], then B = X[0] + Y[0] - Z[0], so Z[0] is the result for the +current column and there is 1 to carry. (We must continue to investigate these and other +digits, though, to make sure that X + Y = Z really is valid under base B. In particular, +we should check that X[0], Y[0] and Z[0] are all greater than B, either at this point or +later.)
    • +

    +Notice that, if we have more than one candidate base after the first column +(i.e., X[0] + Y[0] = Z[0]), there was no carry, so we can repeat the check above for the second +column. If there is one base candidate, we only need to simulate the addition in that base and +check that it works. All in all, we can decide with a single linear pass over the input whether +we have a single base, multiple bases, or no base. It's important to check that the found base +is larger than all digits (including the ones seen before we found a single candidate base), +either by remembering a running maximum digit seen, or by doing a second pass to check. +

    +There is a two-pass strategy that greatly simplifies the algorithm. On a first pass, see if +there is any column i such that X[i] + Y[i] ≠ Z[i]. If there aren't any, the answer is +NON-UNIQUE. If there is, use the least significant of those (i.e., the minimum such i) one +to determine if there is a base candidate B (or if there is none and so the result is +IMPOSSIBLE). It won't work to use columns other than the least significant one with a +difference, because they may depend on some carry that changes the calculations. If the +least significant column yields a candidate base B, just run a second pass to check that +all digits are less than B, and whether the addition holds. If the check is good, B is the +answer; otherwise, it is IMPOSSIBLE. +

    +The described non-distributed algorithm is enough to solve the Small dataset on a single node. +

    +

    A distributed solution

    +

    +As usual, even a linear algorithm is not fast enough for the Large dataset unless we +distribute it. The two-pass version of the algorithm above is the easiest to distribute. +First, assign each node a consecutive portion of each input (digit i of all of X, Y and Z +should be assigned to the same machine) such that each node gets about 1/100th of the total. +Then, the first pass is easy to distribute: each node can report back to the master the +minimum i such that X[i] + Y[i] ≠ Z[i] in its range, if there is one, and the master +can choose among those, if there is one. After that, the master can decide on a result or +a candidate B, that it can report back to the nodes to do the check in their assigned +portion. Checking for all digits to be less than B is simple, but checking the addition is +a little more nuanced. Nodes treating ranges that are not the least significant one need +to know whether there is a carry. Fortunately, if a node is treating a range [i, j] of +positions, checking position i-1 is enough to know if there is carry at position i. If +X[i-1] + Y[i-1] > Z[i-1], then there is a carry. Otherwise, there is definitely not a +carry, because B is at least 2 and this precludes the possibility of an additional 1 due to +carry from position i-2. Thus, each node can check its own range to see if base B is indeed +possible and then report back to the master. If all nodes report yes, the master can output +B. Otherwise, the master can output IMPOSSIBLE. +

    +Even without using the trick to check only the column i-1 to decide whether or not the +range [i, j] starts with some carry, there is the possibility of having each node calculate +two answers: one that assumes that there is an initial carry, and one that assumes there is +not. As part of each answer, each node also reports whether it's sending any carry out of its +last column. Then, the master can process them from least to most significant, picking the +appropriate answer from each pair. +

    +

    A "polynomial" solution

    +

    +A more theory-intensive but possibly simpler way to solve the problem is to +see the sequences as polynomials in a single variable B. Then, the equation is true if and only +if the polynomial P = X + Y - Z evaluates to 0 for a given value of B. +If X[i] + Y[i] - Z[i] = 0 for all i, then P = 0, and thus any sufficiently large integer is a valid +base, so the answer is NON-UNIQUE. Otherwise, using the +rational root theorem, we can +limit the number of roots of P to positive divisors of +|X[i] + Y[i] - Z[i]| for the smallest i such that |X[i] + Y[i] - Z[i]| is not zero. +The largest such divisor is |X[i] + Y[i] - Z[i]|, and the second largest is at most +|X[i] + Y[i] - Z[i]|/2, which is necessarilly not greater than one of X[i], Y[i] or Z[i], and +hence, not a valid base. Therefore, B = |X[i] + Y[i] - Z[i]| is the only candidate base (we stated +this before, with a different proof). To check that candidate base B, we can evaluate P(B) modulo Q, +for a few large prime values of Q. If P(B) mod Q is non-zero for any Q > 1, then P(B) is certainly +non-zero. Otherwise, it is zero with probability ~1/Q. After three big prime values for Q, the +probability of getting a false positive is extremely low. This algorithm is fairly easy to +implement in linear time, and also to distribute. We also need to check that B is larger +than all the given digits, which we do in the same way as in the other presented solution. +This approach saves us the hassle of dealing with carries manually, which is the most error-prone +part of the algorithm, and also a small annoyance while distributing it. +

    diff --git a/distributed_codejam/2017_r2/number_bases/statement.html b/distributed_codejam/2017_r2/number_bases/statement.html new file mode 100644 index 00000000..2625a8a1 --- /dev/null +++ b/distributed_codejam/2017_r2/number_bases/statement.html @@ -0,0 +1,118 @@ +

    Problem

    + +

    Number Bases

    + +

    + You are given three sequences X, Y, and Z of equal length. The sequences + consist of digits. In this problem we deal with non-decimal bases, + so digits are arbitrary non-negative integers, not necessarily restricted to + the usual 0 through 9 range. +

    + Your task is to investigate the possible bases for which the equation + X + Y = Z is both valid and holds true. You must determine whether + there is no such base, more than one such base, or only one such base. If + there is only one such base, you must find it. +

    + For X + Y = Z to be valid in base B, all digits in all sequences have + to be strictly less than B. For X + Y = Z to be true in base B, the sum of + the integer denoted by X in base B and the integer denoted by Y in base B has + to be equal to the integer denoted by Z in base B. +

    + More formally: let S[i] be the i-th digit from the right of a sequence of + digits S, with i counted starting from 0. Then, for a given S and an integer + base B, we will define the integer denoted by S in base B as f(S, B) = the + sum of all S[i] × Bi. Then the equation X + Y = Z is true + for a base B if and only if f(X, B) + f(Y, B) = f(Z, B). +

    + For example, consider the sequences X = {1, 2, 3}, Y = {4, 5, 6} and Z = {5, + 8, 0}, written with the most significant digits on the left, as usual. That + is, X[0] = 3, X[1] = 2 and X[2] = 1. + B = 8 is an invalid base, because it is not strictly greater than the + middle digit of Z. B = 10 is a valid base, but the expression is not true in + that case because 123 + 456 ≠ 580. B = 9 is a valid base that also makes + the expression true, because {1, 2, 3} in base 9 is 102, {4, 5, 6} in base 9 + is 375 and {5, 8, 0} in base 9 is 477, and 102 + 375 = 477. For this case, + B = 9 is the only possible choice of a valid base B that makes the expression + true. +

    + On the other hand, the one-digit sequences X = {10}, Y = {20} and + Z = {30} have multiple bases for which the equation X + Y = Z is both valid + and true. Any value of B greater than 30 would suffice. +

    + +

    Input

    +

    + The input library is called "number_bases"; see the sample inputs below for + examples in your language. It defines four methods: +

    +
    • GetLength(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of digits in each digit sequence + X, Y, and Z. +
      • +
      • Expect each call to take 0.34 microseconds.
      • +
      +
    • +
    • GetDigitX(i): +
        +
      • Takes a 64-bit integer in the range 0 ≤ i < GetLength().
      • +
      • Returns a 64-bit integer: the i-th digit in the digit sequence X, + numbered from right (least significant) to left (most significant). + That is, GetDigitX(0) is the least significant digit of X and + GetDigitX(GetLength() - 1) is the most significant digit of X.
      • +
      • Expect each call to take 0.34 microseconds.
      • +
      +
    • +
    • GetDigitY(i): +
        +
      • Takes a 64-bit integer in the range 0 ≤ i < GetLength().
      • +
      • Returns a 64-bit integer: the i-th digit in the digit sequence Y, + numbered from right (least significant) to left (most significant).
      • +
      • Expect each call to take 0.34 microseconds.
      • +
      +
    • +
    • GetDigitZ(i): +
        +
      • Takes a 64-bit integer in the range 0 ≤ i < GetLength().
      • +
      • Returns a 64-bit integer: the i-th digit in the digit sequence Z, + numbered from right (least significant) to left (most significant).
      • +
      • Expect each call to take 0.34 microseconds.
      • +
      +
    • +
    + +

    Output

    +

    + Output a single line with a single token x. If there is a single + base B that makes the expression valid and true, x must be the + base 10 representation of B. If there are multiple values of B that make the + expression valid and true, x must be NON-UNIQUE. If + there is no such value of B, x must be IMPOSSIBLE. +

    + +

    Limits

    +

    + Time limit: 3 seconds.
    + Memory limit per node: 128 MB.
    + Maximum number of messages a single node can send: 1000.
    + Maximum total size of messages a single node can send: 8 MB.
    + 0 ≤ GetDigitX(i) ≤ 106, for all i.
    + 0 ≤ GetDigitY(i) ≤ 106, for all i.
    + 0 ≤ GetDigitZ(i) ≤ 106, for all i.
    + (Contrary to typical notation, it is possible for any of the digit sequences + in the input to have a zero at its most significant position. Valid bases are + not restricted to be less than 106 or any other upper bound.)
    +

    + +

    Small dataset

    +

    + Number of nodes: 10.
    + 1 ≤ GetLength() ≤ 106.
    +

    + +

    Large dataset

    +

    + Number of nodes: 100.
    + 1 ≤ GetLength() ≤ 108.
    +

    diff --git a/distributed_codejam/2018_finals/analysis_intro.html b/distributed_codejam/2018_finals/analysis_intro.html new file mode 100644 index 00000000..12fa3f14 --- /dev/null +++ b/distributed_codejam/2018_finals/analysis_intro.html @@ -0,0 +1,48 @@ +

    + Our fourth Distributed Code Jam contest continued to push the boundaries of + the format. Klingon Quiz challenged contestants to answer + multiple-choice questions that they could not actually see. Dodge + presented a somewhat chaotic situation, with waves of monsters swarming + through a grid, but it could be solved via distributed dynamic programming. + Virus represented a new extreme in problems about the DCJ system being + broken — nodes could become infected in a way that rendered them + useless to contestants, and even the act of investigating the problem was + bound to spread the infection! Finally, Khazad-dûm was DCJ's + first floating-point problem; it entailed helping dwarves build a road, + subject to some difficult constraints. +

    + It takes a while just to read and think through problems of this level, and + the scoreboard was quiet for the first 45 minutes or so. Most contestants + started with the Small dataset of Virus. By the 2 hour and 30 minute mark, + all four problems had Large submissions, so the race was on! For the + remainder of the round, contestants struggled to break the 61 point + barrier (Dodge + Virus-Small + Khazaddum), with several reaching that total, + and a very late submission from EgorKulikov brought him to 80 + points... but would those scores hold up? +

    + When the judgments had finished, it was Radewoosh on top with 61, + followed by kevinsogo with a literally last-minute Khazaddum + submission that also vaulted him to 61. tczajka was third with 53. + In a first for the DCJ finals, every problem was completely solved by at + least one contestant, although the more unusual problems got fewer submissions. + Kudos to fagu and Errichto.rekt for being the only solvers of + Klingon and Virus, respectively. You can check out individual contestants' + screencasts + here. +

    + Congratulations to our champion, the finalists, and the 1000 Code Jammers + who were eligible for Distributed Code Jam this year! +

    +
    +

    + Cast +

    + All problems written and prepared by Onufry Wojtaszczyk. +

    + Reviews and other contributions from Mohammed Hossein Bateni, Shane Carr, + Max Chang, Chun-Sung Ferng, Md Mahbubul Hasan, Pablo Heiber, Samuel Huang, + Trung Thanh Nguyen, Etienne Pierre-doray, Andi Purice, Ian Tullis, and + Won-seok Yoo. +

    + Analyses will be coming soon. +

    diff --git a/distributed_codejam/2018_finals/dodge/analysis.html b/distributed_codejam/2018_finals/dodge/analysis.html new file mode 100644 index 00000000..78012e9f --- /dev/null +++ b/distributed_codejam/2018_finals/dodge/analysis.html @@ -0,0 +1,80 @@ +

    Dodge!: Analysis

    +

    Small dataset

    +

    + The following approach solves the Small dataset on a single node. Before we + start to navigate this sea of monsters, we should figure out how far the + monsters from each generator advance before (possibly) colliding with other + monsters. The problem might seem daunting because each generator produces an + infinite stream of monsters. However, notice that the pattern of collisions + (if any) repeats. That is, if one monster from generator X collides with + another monster from generator Y, then the next monster generated by X will + also collide in the same cell with the next monster generated by Y, and so + on. +

    + We make one + dynamic programming + (DP) pass through the traversable area of the game (hereafter the "grid"), + starting from the target cell in the top right, and going row by row, from + right to left in each row. For each generator, we keep track of where in its + row/column its monsters have a collision, if any. In each cell (r, c), if + the generators for row r and column c are in positions such that their + monsters could collide, and neither generator has recorded a collision yet, + we update both generators with the collision position. Our traversal order + ensures that we always check for possible earlier collisions above and/or to + the right of our current cell before we declare a collision in that cell. +

    + Then, we make a second DP pass, this time representing the player's movement + through the grid. In this case, the state is the total number of monster + encounters experienced so far, which we want to minimize. As in similar DP + problems about moving from the lower left corner of a grid to the upper right + corner, the best value for a cell is determined by the best values of the two + cells (if any) to the left of and below that cell. Notice that being in cell + (r, c) implies that we made r + c moves to get there, regardless of the exact + path we took. +

    + We run the DP row by row, from left to right in each row, starting from the + player's starting cell (0, 0). If we want to save memory, we can only keep + track of our current row and the previous one, but this is not required. In + each cell, we check for encounters with the monsters from that row and + column's two generators; first we use our per-generator data from the first + DP pass to see whether each monster could make it to that cell at all, and + then, for each monster that could, compare the number of moves we have made + so far with the distance of that monster's generator from the grid. +

    + This approach makes two passes through the grid, and uses + O(N2) time and O(N) memory. The potentially tricky + parts are visualizing and implementing the rules of the game correctly (for + example, not failing to include encounters in the target cell at the end of + the game) and avoiding off-by-one errors. +

    +

    Large dataset

    +

    + To extend the above approach to the Large dataset, we need to distribute both + DP steps, but in a way that ensures that each calculation has all the + information it needs from previous calculations. We will describe the + adaptation of the first DP pass from our Small solution, but we can use + essentially the same strategy for the second DP pass. +

    + We divide the rows evenly among the 100 nodes; each node will consistently be + responsible for its subset of rows. We evenly partition the columns into 100 + subsets in the same way. This divides the grid into 10000 blocks; let block + (x, y) be the block in the x-th subset of rows from the top (the ones handled + by node x) and the y-th subset of columns from the right. Then we proceed as + follows: +

    + Step 1: Node 0 handles block (0, 0) as in the single-node solution above. + Blocks (0, 1) and (1, 0) need the results (i.e. the per-generator collision + data) to proceed, so node 0 sends them to itself and to node 1. +

    + Step 2: Simultaneously, node 0 handles block (0, 1) and node 1 handles block + (1, 0). They send results to Nodes 0, 1, and 2. +

    + Step 3: Simultaneously, nodes 0, 1, and 2 handles blocks (0, 2), (1, 1), and + (2, 0), respectively, and send results to Nodes 0 through 3. +

    + ...and so on. This takes a total of 2 * 100 + 1 = 201 steps, whereas + our single-node solution handling one block per step would have needed 10000 + steps. This is enough of a distribution factor to solve the Large dataset. As + usual, what remains is to ensure that the distribution strategy passes the + right information, correctly handles grids with N < 100, and so on. +

    diff --git a/distributed_codejam/2018_finals/dodge/statement.html b/distributed_codejam/2018_finals/dodge/statement.html new file mode 100644 index 00000000..94a7ee31 --- /dev/null +++ b/distributed_codejam/2018_finals/dodge/statement.html @@ -0,0 +1,141 @@ +

    Problem

    + +

    Dodge!

    +

    + You are playing the arcade game "Dodge!", and you are really + motivated to get onto the scoreboard. Unfortunately, the machine you are + playing on is old and partially broken, and not all of the buttons work, so + you will have to work even harder. +

    + In Dodge!, you control a character who is trying to navigate from one + starting cell of an infinite grid — (0, 0) — to another + target cell — (N-1, N-1). The down and left + buttons are broken, so you can move your character only up and/or to the + right. Moreover, you have to watch out for monsters! Normally, you would be + able to fight the monsters, but the attack button is also broken, so you can + only try to avoid them, and take damage when you do not. +

    + Monsters in the game are generated ("spawned") by monster generators; a + spawned monster appears in the same cell as the generator. There is one + generator in each column between your starting column and the column + containing the target cell, inclusive; these are all located somewhere above + the row containing the target cell. Specifically, each can be located + in a row between N and N + F - 1, inclusive, where + F is a positive integer. (These generators are not necessarily all + in the same row.) Monsters spawned by these generators always move + downward. (We will say more about F and monster movement in a moment). +

    + Similarly, there is one generator in each row between your starting row and + the row containing the target cell, inclusive; these are all located + somewhere to the right of the column containing the target cell. Specifically, + each can be located in a column between N and N + F - 1. + (These generators are not necessarily all in the same column.) Monsters + spawned by these generators always move to the left. +

    + The game proceeds in discrete units of time called turns, numbered with + integers. In each turn, the following things happen, in order: +

    +
      +
    1. If it is turn 0, you appear in the starting cell.
    2. +
    3. You must move your character one grid cell up or to the right. You cannot + remain in your current cell.
    4. +
    5. Every monster moves either down or to the left, depending on which + generator spawned it, as described above.
    6. +
    7. You take one point of damage for every monster in the cell you are + currently in.
    8. +
    9. If there are two monsters in a single cell, they fight and annihilate + each other, and both disappear from the grid forever.
    10. +
    11. If the turn number is divisible by F, each of the generators spawns a + new monster. The newly spawned monster is in the same cell as the + generator.
    12. +
    13. If you are in the target cell, the game ends.
    14. +
    15. Otherwise, the next turn (numbered one larger than the last turn) starts.
    16. +
    +

    + Note that moving into a cell with a monster in step 2 does not cause you to + take damage; the monster is preparing to move out shortly in step 3, and so + you can dodge! +

    + Moreover, to make things more challenging, before the player appears in the + starting cell, the game has already been running since at least turn + -10FN, so there are already monsters on the grid when you enter; note that + some of those monsters might have already encountered and annihilated each + other. +

    + If you move optimally, what is the minimum amount of damage you will take + before the game ends? +

    + +

    Input

    +

    + The input library is called "dodge"; see the sample inputs below for + examples in your language. It defines four methods: +

    +
      +
    • GetN(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number N, as described + above.
      • +
      • Expect each call to take 0.1 microseconds.
      • +
      +
    • +
    • GetSpawningFrequency(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number F, as described + above.
      • +
      • Expect each call to take 0.1 microseconds.
      • +
      +
    • +
    • RowGeneratorPosition(row): +
        +
      • Takes one 64-bit integer, row - the index of the row, in the range 0 + ≤ row < GetN().
      • +
      • Returns a 64-bit integer: the position of the generator in that row. + This means there is a generator spawning monsters that move to the left + in cell (RowGeneratorPosition(row), row).
      • +
      • Expect each call to take 0.1 microseconds.
      • +
      +
    • +
    • ColumnGeneratorPosition(column): +
        +
      • Takes one 64-bit integer, column - the index of the column, in the + range 0 ≤ column < GetN().
      • +
      • Returns a 64-bit integer: the position of the generator in that column. + This means there is a generator spawning monsters that move down in + cell (column, ColumnGeneratorPosition(column)).
      • +
      • Expect each call to take 0.1 microseconds.
      • +
      +
    • +
    + +

    Output

    +

    + Output a single integer: the minimum amount of damage, as described above. +

    + +

    Limits

    +

    + Time limit: 4 seconds.
    + Memory limit per node: 256 MB.
    + Maximum number of messages a single node can send: 1000.
    + Maximum total size of messages a single node can send: 8 MB.
    + 1 ≤ GetSpawningFrequency() ≤ 106
    + GetN() ≤ RowGeneratorPosition(row) < GetN() + GetSpawningFrequency() for + any valid row.
    + GetN() ≤ ColumnGeneratorPosition(column) < GetN() + + GetSpawningFrequency() for any valid column.
    +

    + +

    Small dataset

    +

    + Number of nodes: 10
    + 2 ≤ GetN() ≤ 100.
    +

    + +

    Large dataset

    +

    + Number of nodes: 100
    + 2 ≤ GetN() ≤ 30000.
    +

    diff --git a/distributed_codejam/2018_finals/khazaddum/kd_example2.png b/distributed_codejam/2018_finals/khazaddum/kd_example2.png new file mode 100644 index 0000000000000000000000000000000000000000..01f32b5e0927637354ef9c0eb2db1f47393f6b1d GIT binary patch literal 5310 zcmb`Ldt8!t`^UjUn#Yw>XDNcr+J<4XgPEEKR7@+i6e5LnxV0W#mef$u@|2pE3igd z?-zNYn~0`Hi;NHmgz5S~zs=xpG6I1V8_okKZgly21mffD^?p9#Ni=mYl@lSeY8guy zQ8R}y*oAq4_AeLqqQ9^_V(5{8F`S=|{_L}%tYKxA_G8RK&od!0;Z>e`8J`@{^HeT$ z_-M0n_2L9|7@^wy)0B@|?}+&ay3T)d^7*biZa2Q&)#3Kj;Vt9a28YKU9jKGerid%t z7{d>B9xi)6zoQeraN$C%JrohK{mNHsLOi_)W6^%5K$@1cJ50pU*xj$QYqf&n*qP9R5=yl+LQip@#C{O$G=r)8;2g!Mr5Tv+KX^~ zeEfZM@@43u32IhTyj@F<=~3y}ULf0D>82|aA28Ku<+?ow%-_q_gb=ik_L<(Mv04*o!6O5TBd zQrEB+zPpYmWZKZtny0rAqfI~i99Df^BD=-XWjj-K)*R$yO+DQ81+Nri;P6!=;>A@1 zJ7q3v^E-jbspRycbt<%c^n@kJnPBE8c>JYo^aQ!-a$jRuwAMxGt^BzLxv2sWysbOl zedg+L9%=JSnqmrWs-ocxIM-ZvN)v1#?kjD5_u0VADZY{JC{O$G9I%9-Z;`xVzti@P zk0pBv=O84!QLQ49wt^NA;#%r7Y&2&M?JDBgQ|5{wlxG=QERIjhu}$yk6dZ=K=bB-a zD`Z9dKa0wj3xzuHZ*yb&+HBwPYn>o%3t(vqi9{A5G6^yD1bzgnsi%;BP2QOk_czQxL>?X zumg$)HayvpqBvVh7(mUHo79Vlq;P=hinmB^ur23M?~zVH0^~o3vfSs;`y${Yqv!Ia zxMUvBSwIGD|HhMr)xb9ju28Hh zNTI+DGP#N&#^5->O}a;@VN4`>9lt4v}PbmAwjiS~oNnXhCko)$VqNllVwOC5n*f%&B@Vr;3sa63R3DDxCgbDh8}lJ%0S8Iu@eu>L5YN)GifoC#1O zYv~;N1kBDO(}TWOt|47}{L2|IC5@rRPpye;FJF!9In)p+U8Q87Pn&dXX+QBJegsDW z`>8qF|Bm<{I{z+N>w@xTaHun|FDPlivWqSC5{3eINGMnF^baT% z%uyFM6Wy|>%y~cv32F_|3T%o;`f#XZik%I{WR7<4#HT^0!1bI%aV8Wn5W2Z=m)eM6 z1*XuGHM~{Q%F+ku1lqkzupG(&s3%u=pVOA8ZHxq=?XU_IPO$UgV*%rb}^4fBmhjg(z;v z4&=;g*9OC;q)j6Z?mk4YN@s#GC_B4raMSCTLp3Qy$&*y@%|nKu1Vr+0WFARPM~?e8$NCeSx^A%|;oBl=+MT=CxNO3~l#I1@f|TFE zj1{||+zs){P8;c&UutmYYOlowS51i}H7$~PdK4&MdH5`(A~T}g)m=Bm9(ZA;9h&-b zD&>|{X5!VKC9j8Se%P*7h<;A|_%gBM@#vMMn_6a3srR0<=hZ3SERBk~FmIjo<)(Uj zE9T(7=E->@jlUG$yWQPzZ1U~&;Pd+a%Ri)zGdEm?m#@%$G5(0=<@D+fqcMKth@SGKru6NH-%jZAw>_Q2PLIaJw>xHD>u!&~lajth>Dq_b zDpkDAeG|VOzI`1}(@UC+emtg4>-@E;)H4MUf4O~f_p=z8i3WlW2>pzi^%kq%G{8(H) za_U9aL{kFH244VI0aC>4TbLU)itzKbDRI-PK1^3H&=`9NXEZb8|8yBa#f%NxhgkIP03OhbO zl0hT(D!})5zK-q*z_K@gO>u0J650`4jeas4S{Q!G`=$KiVm}yyboI}O2q+X{l znihZ8Qc|B}G*ef`Bah)g+OKBv4jC^A@>5q6Vg$4{EH>8K&gH;wn!mE9RUv2oVRG;g$s`lf|bHa|Vg@yA}7S=-Wmfg^qpHM3>b=so=|Z!46k z@Gt5Jm*)E~;ZR?}8)Uq{GZY9_E84O;LZ$hBOSn`I*hI$rJ40)rRz+Lx25|8ROZe1O z__~bu#}JMcR{HCs8>*%0v#9zJ!lz&ab#pn1jZ450W6_0Q2MwqqY+ZJ!2M8Yy`C}~P zcpB8Iiul+iK|SEV1YG0)3^jG}K7(50BqtYJ_9o^m2a{vv#aWLAeTe-)I-k~tUeSp^ z3{|TlOk4=+0pb$wUxnhN`93im>J;oJ<9!0&~w?M&d=HahU6W?r6#cEbIA+f4I-SGi;wmTow8a+o6iys%^$r& zCRhXO$#@?S8)nTQ?X9~+i#}Ba|NavC-!3$!^smc~r$ujy8g{8gQIcD@R+f$4-q8Xpo?Lg#E$Jn~O z_n7(-a&`Qx=56>$<(3+R8R}JK?&!QRVa#;7KB@j|Hnvmn8u!-i&@#ooy>-v^%7P*YjCeIi3O)sY*k&!eI=7*FYKtZ7jpv!Dcd?F?33CMz{MEGdja5TyhZY z4yM7%jx9BVa#bZC5Yxb!qF{54pEd|ZImZ|Rud%dttZ^6K6Sk1ylA)^#k&bvKt5;Lu zJvBO}xR11?zz8^xBcQ)Z}2oP?Q*Gv>)vOvhvVD#j221-knk6KI31C_d^3xgMd? zVt-Po2Q8*DNVUa8)QZaQ*X=QI|vodS63q^!LZg{1mSo`9GA*8 zmshAt{?GIW$9g~~7#xp!nH?#^eGWy7f?er6reijr&ln1bkElP*Dv{xP;kr0!u`lW1 zUO9c&1MOW%@st-a7a&Ov)+4(E}Ehyt$|59Uh448XzVt^`qh#bM=v!SK}J zLys*2yueFQ=w8KX`$^^c^b5Pi*3tK7wAi~H-r5(}-mtx%py`g}uZ;2uJN+47Q-ouKAHBU~HH)W!|j^K87;*wrhg z-+%cPC}&S@-04ddClYioURxD^xZU<2!-rfJuFb|4U_db)z4DrKn}@Tl7b;kQu%#S*dMKZ>zB%N6UX)nKfIrcaQ{4R-D@4v}U;JZEZU*3&%eE +#include +#include +#include + +#include "khazaddum.h" // NOLINT +#include "message.h" // NOLINT + +typedef long long ll; // NOLINT +typedef long double ld; // NOLINT + +#define MAXN 100000000 +#define MAXHEI 100000000 + +ll N; +int me; +std::vector heights; +std::vector minhei; + +// We want to calculate the answer to the question "if we dig up to x +// centimeters deep, how much will we excavate?". The answer to this question +// is a piecewise quadratic function, and we want to know, for a given x, what +// are the coefficients of this function on the range the x belongs to. +// The equation we care about will be alpha x^2 + beta x + gamma. +// We will first accumulate changes to alpha, beta, and gamma at points in time +// (in the _deltas vectors), then sort and accumulate those vectors, and then +// binary search in them for answers. +std::vector> alpha_deltas; +std::vector> beta_deltas; +std::vector> gamma_deltas; +std::vector> alph; +std::vector> bet; +std::vector> gamm; + +// This register the changes to the alph, bet and gamm values for an interval +// of length "multiplier" that has heights of h1 and h2 at edges, and +// min heights of mh1 and mh2 at edges. +void PushValues(ld h1, ld h2, ld mh1, ld mh2, ld multiplier) { + ld s = mh1 - h1; + ld e = mh2 - h2; + if (s > e) std::swap(s, e); + // If the road is exactly parallel to the mountain surface, the excavation + // function will first be zero, and then jump to being linear with coefficient + // one (or rather, multiplier) at s. We need to subtract a constant, so that + // the linear function begins with zero at s. + if (s == e) { + beta_deltas.push_back(std::make_pair(s, multiplier)); + gamma_deltas.push_back(std::make_pair(s, -multiplier * s)); + } else { + // This defines a quadratic function with a double-zero at s, and + // reaching (e-s) / 2 at e. In the range between s and e, the area excavated + // will be a triangle, growing linearly (so the area grows quadratic). At + // the beginning, the area is zero, and the derivative is zero - which is + // where the double-zero at s comes from. At the end, we have a triangle + // with a base of e - s, and height of multiplier. + alpha_deltas.push_back(std::make_pair(s, multiplier * 0.5 / (e - s))); + beta_deltas.push_back(std::make_pair(s, -multiplier * s / (e - s))); + gamma_deltas.push_back( + std::make_pair(s, multiplier * s * s * 0.5 / (e - s))); + // This cancels out the quadratic function above, inserting instead a + // linear function that has value (e - s) / 2 at e, and a coefficient of 1. + alpha_deltas.push_back(std::make_pair(e, -multiplier * 0.5 / (e - s))); + beta_deltas.push_back(std::make_pair(e, multiplier * ((s / (e - s)) + 1))); + gamma_deltas.push_back(std::make_pair( + e, multiplier * (-s * s * 0.5 / (e - s) - (e + s) * 0.5))); + } +} + +void accumulate(std::vector> &deltas, + std::vector> &result) { + std::sort(deltas.begin(), deltas.end()); + ld current_pos = -1; + ld val = 0; + for (const auto &delta : deltas) { + if (delta.first != current_pos) { + result.push_back(std::make_pair(current_pos, val)); + current_pos = delta.first; + } + val += delta.second; + } + result.push_back(std::make_pair(current_pos, val)); +} + +ld upto(std::vector> &coefficients, ld pos) { + int lo = 0; + int hi = coefficients.size(); + while (hi - lo > 1) { + int med = (hi + lo) / 2; + if (coefficients[med].first <= pos) { + lo = med; + } else { + hi = med; + } + } + return coefficients[lo].second; +} + +// We don't have get/put functions for doubles. So, we'll hack them on top of +// Get/Put LL. +void PutDouble(int node, double value) { PutLL(node, *(ll *)(&value)); } +double GetDouble(int node) { + ll value = GetLL(node); + return *(double *)(&value); +} + +int main() { + me = MyNodeId(); + int nodes = NumberOfNodes(); + + N = GetRangeLength() + 1; + // Note - the ranges owned by particular nodes have to overlap, because we + // care about the intervals in between (each has to belong to someone), and + // not about the vertices. + ll beg = ((N - 1) * me) / nodes; + ll end = ((N - 1) * (me + 1)) / nodes + 1; + ll myN = end - beg; + ld to_dig = GramsToExcavate(); + for (int i = 0; i < myN; ++i) { + heights.push_back(GetHeight(i + beg)); + minhei.push_back( + std::max(heights[i], i ? (minhei[i - 1] - 1) : heights[i])); + } + for (int i = myN - 1; i >= 0; --i) { + minhei[i] = + std::max(minhei[i], (i == myN - 1) ? heights[i] : minhei[i + 1] - 1); + } + // Now, send all the preceding nodes the min height on the left, and all the + // succeeding nodes min height on the right. + for (int node = 0; node < me; ++node) { + PutLL(node, beg); + PutLL(node, minhei[0]); + Send(node); + } + for (int node = me + 1; node < nodes; ++node) { + PutLL(node, end - 1); + PutLL(node, minhei[myN - 1]); + Send(node); + } + // And gather this data, and adjust. + for (int node = 0; node < me; ++node) { + Receive(node); + ll where = GetLL(node); + ll height = GetLL(node); + ll height_at_beg = height - (beg - where); + assert(beg >= where); + minhei[0] = std::max(minhei[0], height_at_beg); + } + for (int node = me + 1; node < nodes; ++node) { + Receive(node); + ll where = GetLL(node); + ll height = GetLL(node); + assert(where >= end - 1); + ll height_at_end_minus_one = height - (where - (end - 1)); + minhei[myN - 1] = std::max(minhei[myN - 1], height_at_end_minus_one); + } + for (int i = 1; i < myN; ++i) { + if (minhei[i] < minhei[i - 1] - 1) { + minhei[i] = minhei[i - 1] - 1; + } else { + break; + } + } + for (int i = myN - 2; i >= 0; --i) { + if (minhei[i] < minhei[i + 1] - 1) { + minhei[i] = minhei[i + 1] - 1; + } else { + break; + } + } + // Now all the min heights should be globally correct. + for (int i = 0; i < myN - 1; ++i) { + // If the two min heights are equal, and they are not set exactly at the + // level of the heights, then the min height graph takes a little dip in + // the middle of the [i, i+1] interval. We treat this as two intervals, + // [i, i+0.5] and [i+0.5, i]. + // The indices in the tree will be multiplied by two, so that we can handle + // them as integers. + if (minhei[i + 1] == minhei[i] && + (minhei[i + 1] + minhei[i] != heights[i + 1] + heights[i])) { + PushValues(heights[i], 0.5 * (heights[i] + heights[i + 1]), minhei[i], + 0.5 * (minhei[i] + minhei[i + 1] - 1), 0.5); + PushValues(0.5 * (heights[i] + heights[i + 1]), heights[i + 1], + 0.5 * (minhei[i] + minhei[i + 1] - 1), minhei[i + 1], 0.5); + } else { + PushValues(heights[i], heights[i + 1], minhei[i], minhei[i + 1], 1); + } + } + accumulate(alpha_deltas, alph); + accumulate(beta_deltas, bet); + accumulate(gamma_deltas, gamm); + // In the worst case, we have a few high peaks (of height ~MAXHEI), and the + // rest is flat. So, the zero-dig road goes along ~MAXHEI, and the first + // MAXHEI we dig in excavates relatively little. If GramsToExcavate is around + // MAXHEI * RangeLength, then we have to dig another MAXHEI in to excavate + // enough (since on the second MAXHEI we're guaranteed to dig through the + // ground). + double hi = 2 * MAXHEI; + double lo = 0; + // We will do 1000-ary search - that is, we'll split the range into 1000 + // sub-ranges, each node will send back the values for each of the border + // values, the master will sum them up, and identify the correct sub-interval. + // Rinse and repeat. + const int search_arity = 1000; + // At this point, each node owns a partially quadratic function. We need to + // N-ary search for the dig depth where we excavate enough. + int master = 0; + while ((hi - lo) / hi > 1e-8) { + for (int i = 1; i < search_arity; ++i) { + ld med = (lo * (search_arity - i) + hi * i) / search_arity; + ld a = upto(alph, med); + ld b = upto(bet, med); + ld c = upto(gamm, med); + ld res = a * med * med + b * med + c; + PutDouble(master, res); + } + Send(master); + if (me == master) { + std::vector values(search_arity - 1); + for (int node = 0; node < nodes; ++node) { + Receive(node); + for (int i = 1; i < search_arity; ++i) { + double dug = GetDouble(node); + values[i - 1] += dug; + } + } + double new_lo = -1; + double new_hi = -1; + for (int i = 1; i < search_arity; ++i) { + if (values[i - 1] > to_dig) { + new_lo = + (lo * (search_arity - (i - 1)) + hi * (i - 1)) / search_arity; + new_hi = (lo * (search_arity - i) + hi * i) / search_arity; + break; + } + } + if (new_lo == -1) { + new_lo = (lo + hi * (search_arity - 1)) / search_arity; + new_hi = hi; + } + for (int node = 0; node < nodes; ++node) { + PutDouble(node, new_lo); + PutDouble(node, new_hi); + Send(node); + } + } + Receive(master); + lo = GetDouble(master); + hi = GetDouble(master); + master = (master + 1) % nodes; + } + if (me == master) { + printf("%.8lf\n", lo); + } + return 0; +} diff --git a/distributed_codejam/2018_finals/khazaddum/statement.html b/distributed_codejam/2018_finals/khazaddum/statement.html new file mode 100644 index 00000000..c01dfb42 --- /dev/null +++ b/distributed_codejam/2018_finals/khazaddum/statement.html @@ -0,0 +1,130 @@ +

    Problem

    + +

    Khazad-dûm

    +

    + The dwarves are trying to build a new path through the mountains. + The mountains might be difficult terrain, with high peaks and deep valleys, + and the road cannot be too steep, so some excavating and/or bridge-building + will be needed. +

    + The dwarves want the moon to shine on the road in all places, so they will + not be tunneling. Where the road goes above the ground level, they will build + a bridge; where the road goes below the original ground level, they will dig + up the mountain to remove all the rock that would have been above the road. + This excavation serves two purposes: first, it enables the road to be built + in the fresh air, and second, it provides material which will be used to + pave and decorate the road. +

    + The dwarves have produced a cross-sectional map of the part of the mountains + they will build through. Starting from point 0 (one edge of that + cross-section), they have measured the elevation (in centimeters) at every + 1-centimeter increment along the path. Between each two mapped points, the + mountains are linear: if at centimeter K the mountains are AK + centimeters high, and at centimeter K + 1 the mountains are AK+1 + centimeters high, then at centimeter K + t, for t in the continuous interval + [0, 1], the mountains are t AK+1 + (1 - t) AK + centimeters high. Note that a height of "zero" is an arbitrary baseline, and + it is possible for the dwarves to dig arbitrarily deep. The road can start + and end at any height (so we can enter the cross section below, at, or above + the original ground level at point zero, and similarly exit at any height). +

    + The dwarves must decide how high the road will go at every point. + The incline of the road can never be larger than 45 degrees — that is, + the road cannot gain or lose more than X centimeters vertically over a + horizontal distance of X centimeters for any (not necessarily integer) X. + The road can go higher than the original height of the mountains (bridges + will be built in these places), or lower (the mountains will be excavated + in these places). In order to provide enough material to build + the road, at least GramsToExcavate() grams of the mountains need to be + excavated; one square centimeter of cross-section of the mountains weighs + one gram. Note that only the rock above the road level can be excavated, one + cannot simply excavate some rock from under a bridge, because that would + look ugly and would not meet the dwarven standards of craftsmanship! +

    +

    + These constraints (the incline no larger than 45 degrees, and at least + GramsToExcavate() material excavated) allow multiple possible plans. + Due to lessons learned in the past, the dwarves want not to dig too deep, + so out of all the plans, they will choose the one in which the depth of the + road — the largest negative vertical distance between the original height + of the mountains at a certain point along the road (any point, not only the + ones where measurements were taken), and the road surface at that point — + is as small as possible. Can you help the dwarves find this minimum depth? +

    + +

    Input

    +

    + The input library is called "khazaddum"; see the sample inputs below for + examples in your language. It defines three methods: +

    +
      +
    • GetRangeLength(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the length of the mountain range, in + centimeters (so, the number of points measured is one larger).
      • +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    • GramsToExcavate(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the minimum number of grams the dwarves + have to excavate.
      • +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    • GetHeight(position): +
        +
      • Takes one 64-bit integer, position (the position along the + cross-section, in centimeters, from the starting point). If position + is smaller than 0 or larger than GetRangeLength(), this will + crash.
      • +
      • Returns a 64-bit integer: the elevation at that point, in + centimeters.
      • +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    + +

    Output

    +

    + Output a single real number: the minimum depth (in centimeters) the dwarves + have to dig. The answer will be considered correct if it is within an + absolute or relative error of 10-6 of the correct answer. See the + FAQ + for an explanation of what that means, and what formats of real numbers we + accept. +

    + +

    Limits

    +

    + Time limit: 9 seconds.
    + Memory limit per node: 512MB.
    + Maximum number of messages a single node can send: 1000.
    + Maximum total size of messages a single node can send: 8 MB.
    + 1 ≤ GramsToExcavate() ≤ 108 × GetRangeLength().
    + 0 ≤ GetHeight(i) ≤ 108.
    +

    + +

    Small dataset

    +

    + Number of nodes: 10.
    + 1 ≤ GetRangeLength() ≤ 1000.
    +

    + +

    Large dataset

    +

    + Number of nodes: 100.
    + 1 ≤ GetRangeLength() ≤ 108.
    +

    + +

    + The following image illustrates the solution to the second sample case. The + black line represents the road (which includes a bridge); the light and dark + red areas, respectively, represent the parts of the mountain range that the + dwarves have excavated and have not excavated. +

    +

    + Illustration of the second sample case. +

    diff --git a/distributed_codejam/2018_finals/klingon/solution.cpp b/distributed_codejam/2018_finals/klingon/solution.cpp new file mode 100644 index 00000000..1fca504e --- /dev/null +++ b/distributed_codejam/2018_finals/klingon/solution.cpp @@ -0,0 +1,357 @@ +#include +#include +#include +#include "message.h" // NOLINT +#include "klingon.h" // NOLINT + +typedef long long ll; // NOLINT + +int A; +int W; +int Q; +// My node ID +int M; +// Number of nodes. +int N; +// ID of the current "master" node. We'll rotate the responsibility of being +// the master to ignore the "number of messages sent" limit. +int master; + +// The answers in the "correct" and "incorrect" tables are the values you should +// provide to the "doAnswer" function, not the actual answers. + +// The correct answers for the questions we already know the correct answers to. +int correct[26000]; +// Known incorrect answers to the questions (these are easier to get). +int incorrect[26000]; +// The number of known correct answers. +int n_correct = 0; +// The final answer to the problem. +int code; +// The correct question-answer pairs found in the search we're currently doing. +std::vector> current_correct; + +// Semi-randomize our answers. While it's a pretty weak randomness, it should be +// strong enough that a single node should be somewhat unlikely to hit a very +// large number of correct answers. +inline int Transform(int a, int q) { + return (a + 1 + ((q * (q ^ 12345) + (q/3)))) % A; +} + +int doAnswer(int a, int q) { + return Answer(Transform(a, q)); +} + +// Provide the answers to all the questions we know the answers to. +void begin() { + for (int q = 0; q < n_correct; ++q) { + assert(doAnswer(correct[q], q) == 0); + } +} + +// Answer a consistent whatever, until we either reset the game, or (somewhat +// accidentally) win. If we win, we'll store the secret code on the "code" +// variable, and return -1, otherwise we'll return how many questions we needed +// to ask to reset the game. +// If we were able to incorrectly answer, we would just answer questions +// incorrectly here for as long as needed, and we would know that the number of +// incorrect previously is W+1 minus the one of incorrect answers we needed +// here. Unfortunately, we don't know incorrect answers, and so we answer at +// random, assuming we'll mostly hit incorrect answers. +// Leaves the game in the reset state. +ll finish() { + ll ans = 0; + int q; + // We need any reasonably random way of generating answers that is unlikely + // to hit a long streak of correct answers. + for (q = 0; ans == 0; q++) { + ans = doAnswer(0, q); + } + if (ans != -1) { + code = ans; + return -1; + } + return q; +} + +// Will provide the given answer in the range [from, to). Before n_correct, will +// provide correct answers. In the range [n_correct, n_correct+W+1), except +// [from, to), will provide incorrect answers. After n_correct+W+1, will use +// finish() to provide answers, and will return 1+finish(), or 0 if the call to +// finish() was not needed. Will return -1 and store the code on "code" if we +// accidentally finish the game. +// The key observation here is that if we return a positive number, at least one +// of the answers in the [from, to) range had to be correct - otherwise, we'd +// have given incorrect answers in the whole n_correct, n_correct + W + 1 range, +// and we wouldn't have needed the call to finish. And conversely, if this +// returns zero, we know all the answers in the [from, to) range were incorrect. +// +// Leaves the game in a reset state. +int answer_in_range(int a, int from, int to) { + begin(); + int ans = -1; + assert(from >= n_correct); + assert(to <= n_correct + W + 1); + for (int q = n_correct; q < from; ++q) { + assert(doAnswer(incorrect[q], q) == 0); + } + for (int q = from; q < to; ++q) { + ans = doAnswer(a, q); + if (ans > 0) { + code = ans; + return -1; + } + } + for (int q = to; q < n_correct + W + 1; ++q) { + ans = doAnswer(incorrect[q], q); + if (ans > 0) { + code = ans; + return -1; + } + } + // Only the last answer can possibly be -1, since it's the W+1st unknown + // answer. + if (ans == -1) { + return 0; + } + int result = finish(); + if (result == -1) { + return -1; + } + return result + 1; +} + +// Assumes that running answer_in_range(a, from, to) returns cur_num_over, and +// that cur_num_over is greater than zero (that is, that "a" is a correct answer +// to some question in the [from, to) range). Will fill in the current_correct +// vector with all the (q, a) pairs where q is in the [from, to) range, and a +// is the correct answer to q. +// Leaves the game in a reset state. +void fill_in_answers_binary(int a, int cur_num_over, int from, int to) { + // If there is a correct answer in the range, and the range is of length one, + // we can pinpoint the correct answer pretty easily :) + if (to - from == 1) { + current_correct.push_back(std::make_pair(from, a)); + return; + } + // If the range is longer, split it in half, and check both parts. + int med = (from + to) / 2; + int required_to_finish = answer_in_range(a, from, med); + // The assumption is we already ran the game answering "a" in the whole + // [num_correct, num_correct+W+1) range, and didn't finish the game - and now + // we will give no fewer incorrect answers than that run-through. So, we can't + // finish the game. + assert(required_to_finish >= 0); + // If there were no correct answers in the range, search the latter range. + if (required_to_finish == 0) { + fill_in_answers_binary(a, cur_num_over, med, to); + return; + } + // If we required the same finish length, it means all the correct answers are + // in this range, so search it. + if (required_to_finish == cur_num_over) { + fill_in_answers_binary(a, cur_num_over, from, med); + return; + } + // Now, we have correct answers in both the sub-ranges. First, let's search + // the first range (since we know how many answers were required to finish). + fill_in_answers_binary(a, required_to_finish, from, med); + // We also need to know how many answers are required to finish the second + // half. + required_to_finish = answer_in_range(a, med, to); + assert(required_to_finish >= 0); + fill_in_answers_binary(a, required_to_finish, med, to); +} + +// We try to identify incorrect answers for all the questions in the +// [num_correct, num_correct + W + 1) range. +// We'll do this by first answering "0" to all the questions (thanks to the +// pseudorandomization we do, this won't be correct too often), and checking +// how many extra answers we had to provide. +// Then, we will, for each question in the range, switch the answer to that one +// question to "1", and see what happens to the number of extra answers. If the +// number of extra answers goes up, it means that "1" is correct, and "0" isn't. +// If it goes down, "0" is correct, and "1" isn't. If it stays the same, they're +// equally correct, which is to say, they're both incorrect (since they can't be +// both correct). So, in each case, we learn an incorrect answer. +// We will do this in a distributed fashion (although it might be that doing it +// on every node would run in time) - each node checking the answers in some +// sub-range of [num_correct, num_correct + W + 1), sending the indices of the +// answers where "0" was correct to the master, and receiving the full list of +// the answers where "0" was correct back. +// Leaves the incorrect array filled in with incorrect answers up to num_correct +// + W inclusive; and the game in a reset state. +int identify_all_incorrect() { + // We'll reuse the "answer_in_range" method. This method answers with the + // answer provided in the provided range, and with the answer from + // "incorrect" outside this range. We'll cheat, fill in incorrect with + // 0, and use it that way (even though "incorrect" will not necessarily all be + // incorrect this way). We'll store the indices on which it happens that a + // is correct on a separate vector. + std::vector zero_correct_indices; + int beg = M * (W + 1) / N; + int end = (M + 1) * (W + 1) / N; + for (int q = n_correct; q < n_correct + W + 1; ++q) incorrect[q] = 0; + bool found_solution = false; + int over_zero = answer_in_range(0, n_correct, n_correct); + if (over_zero == -1) { + found_solution = true; + } + if (!found_solution) { + for (int q = n_correct + beg; q < n_correct + end; ++q) { + int res = answer_in_range(1, q, q+1); + // We might accidentally finish the game. It's very unlikely, but it's + // theoretically possible. + if (res == -1) { + found_solution = true; + break; + } + if (res < over_zero) zero_correct_indices.push_back(q); + } + } + // Everybody sends their data to the master. + PutChar(master, found_solution); + if (found_solution) { + PutInt(master, code); + } else { + PutInt(master, zero_correct_indices.size()); + for (int q : zero_correct_indices) { + PutInt(master, q); + } + } + Send(master); + // Master receives and accumulates the data. If there's a correct answer, + // master prints it. + if (M == master) { + found_solution = false; + zero_correct_indices.clear(); + for (int n = 0; n < N; ++n) { + Receive(n); + if (GetChar(n)) { + int sol = GetInt(n); + if (!found_solution) printf("%d\n", sol); + found_solution = true; + } else { + int cnt = GetInt(n); + for (int i = 0; i < cnt; ++i) { + zero_correct_indices.push_back(GetInt(n)); + } + } + } + for (int n = 0; n < N; ++n) { + PutChar(n, found_solution); + if (!found_solution) { + PutInt(n, zero_correct_indices.size()); + for (int q : zero_correct_indices) { + PutInt(n, q); + } + } + Send(n); + } + } + // Everybody receives correct answers. + Receive(master); + if (GetChar(master)) { + // The master received the correct answer and printed it out. + return -1; + } else { + int cnt = GetInt(master); + for (int i = 0; i < cnt; ++i) { + incorrect[GetInt(master)] = 1; + } + } + master = (master + 1) % N; + return 0; +} + +int main() { + A = GetNumberOfPossibleAnswers(); + W = GetAllowedNumberOfWrongAnswers(); + Q = GetNumberOfQuestions(); + M = MyNodeId(); + N = NumberOfNodes(); + master = 0; + while (true) { + // First, try to identify an incorrect answer. Note that this isn't required + // if A > W - we could just find an answer that's fully incorrect - but that + // would require one extra round of communication, and extra code. Instead, + // let's just always run the default search-for-incorrect code. + if (identify_all_incorrect() == -1) return 0; + // Now, that we have an incorrect answer for every question in the range, + // let's try to identify the correct answers. + int beg = (M * A) / N; + int end = ((M + 1) * A) / N; + current_correct.clear(); + bool solution_found = false; + for (int a = beg; a < end; ++a) { + int num_over = answer_in_range(a, n_correct, n_correct + W + 1); + if (num_over == -1) { + solution_found = true; + break; + } + if (num_over) { + fill_in_answers_binary(a, num_over, n_correct, n_correct + W + 1); + } + } + // Now, send the data to the master. + PutChar(master, solution_found); + if (solution_found) { + PutInt(master, code); + } else { + PutInt(master, current_correct.size()); + for (auto p : current_correct) { + PutInt(master, p.first); + PutInt(master, p.second); + } + } + Send(master); + // Receive and aggregate on the master. + if (M == master) { + solution_found = false; + current_correct.clear(); + for (int n = 0; n < N; ++n) { + Receive(n); + if (GetChar(n)) { + int sol = GetInt(n); + if (!solution_found) printf("%d\n", sol); + solution_found = true; + } else { + int cnt = GetInt(n); + for (int i = 0; i < cnt; ++i) { + int q = GetInt(n); + int a = GetInt(n); + current_correct.push_back(std::make_pair(q, a)); + } + } + } + // Resend back to everybody. + for (int n = 0; n < N; ++n) { + PutChar(n, solution_found); + if (!solution_found) { + assert(current_correct.size() == W + 1); + for (auto p : current_correct) { + PutInt(n, p.first); + PutInt(n, p.second); + } + } + Send(n); + } + } + // Everybody receives from master, and updates their information about + // correct answers. + Receive(master); + if (GetChar(master)) return 0; + // Note - we could decrease the network traffic a bit by resorting the data + // on the master, and just sending the answers in order, without the + // questions. + for (int ans = 0; ans < W + 1; ++ans) { + int q = GetInt(master); + int a = GetInt(master); + correct[q] = a; + } + n_correct += (W + 1); + master = (master + 1) % N; + } + assert(false); // Unreachable code. + return 0; +} diff --git a/distributed_codejam/2018_finals/klingon/statement.html b/distributed_codejam/2018_finals/klingon/statement.html new file mode 100644 index 00000000..c5cde5df --- /dev/null +++ b/distributed_codejam/2018_finals/klingon/statement.html @@ -0,0 +1,116 @@ +

    Problem

    + +

    Klingon

    +

    + You are playing a computer game about Klingons. A part of this game is a quiz, + about Klingons, in the Klingon language. + It would probably be much easier if you spoke even one word of Klingon. + Unfortunately, you do not. Fortunately, you are allowed arbitrarily many attempts to pass the + quiz, and the questions are always the same. +

    + The quiz consists of Q questions. Each question has A answers, + and only one of those answers is correct. You will answer the questions one at a time, + and you only get one attempt at each question. If you answer + all Q questions without giving more than W wrong answers, + you will get the secret code that you can redeem for your very own Klingon + starship. +

    +

    + The game does not tell you whether you have gotten a particular question right or wrong + (or maybe it does, but in Klingon...), but it does keep track of how many wrong answers you + have given so far. If you give a wrong answer after having already given W other wrong + answers (that is, if you give the (W+1)th wrong answer), then instead of moving on, the + game will tell you that you have lost, play a sad sound, and reset. After the reset, your count + of wrong answers resets to zero, and you will be asked the same questions (starting from the + beginning), in the same order, and with the same correct answers. +

    +

    + To make your task easier, you have found 100 servers (nodes) that all run independent + instances of the same quiz (with the same questions and correct answers). That is, each + node maintains its own independent state of which question you are on, how many wrong + answers you have given so far, etc. +

    + +

    Input

    +

    + The input library is called "klingon"; see the sample inputs below for + examples in your language. It defines four methods: +

    +
      +
    • GetNumberOfQuestions(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of questions Q in the quiz.
      • +
      • Expect each call to take 0.02 microseconds.
      • +
      +
    • +
    • GetAllowedNumberOfWrongAnswers(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of wrong answers W you can give without + losing the game.
      • +
      • Expect each call to take 0.02 microseconds.
      • +
      +
    • +
    • GetNumberOfPossibleAnswers(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of possible answers A to every question.
      • +
      • Expect each call to take 0.02 microseconds.
      • +
      +
    • +
    • Answer(answer): +
        +
      • Takes one 64-bit integer, answer (the answer you are providing) in + the range 0 ≤ answer < GetNumberOfPossibleAnswers().
      • +
      • Calling Answer() means that you answer the current question, and you choose the answer you + provide as the argument. If answer is outside of the allowed range, the program will + terminate and you will receive a "Wrong Answer" verdict. Answer() returns a 32-bit integer denoting the results of your answer: +
          +
        • + -1 if your answer was the (W+1)th incorrect answer in this playthrough. After + this, the game resets (so, the next question you'll be answering is the first question). +
        • +
        • + A positive number that is the secret code if your answer was not the (W+1)th + incorrect answer, and you were answering the last question. Subsequent calls to Answer() + will return 0. +
        • +
        • + 0 in all other cases. +
        • +
        +
      • +
      • Expect each call to take 0.02 microseconds.
      • +
      +
    • +
    + +

    Output

    +

    + Output a single integer: the secret code obtained at the end of the game. + Note that you must actually output the code, on a single node (as usual); + just receiving the code does not constitute solving the problem. +

    + +

    Limits

    +

    + Time limit: 10 seconds.
    + Number of nodes: 100 (for the Small dataset, too!)
    + Memory limit per node: 256MB.
    + Maximum number of messages a single node can send: 1000.
    + Maximum total size of messages a single node can send: 8 MB.
    + 1 ≤ GetNumberOfQuestions() ≤ 25000.
    + GetAllowedNumberOfWrongAnswers() = 3000.
    + 2 ≤ GetNumberOfPossibleAnswers() ≤ 50000.
    +

    + +

    Small dataset

    +

    + 0 is never the correct answer to any question. +

    + +

    Large dataset

    +

    + No extra restrictions. +

    diff --git a/distributed_codejam/2018_finals/virus/solution.cpp b/distributed_codejam/2018_finals/virus/solution.cpp new file mode 100644 index 00000000..d72329a5 --- /dev/null +++ b/distributed_codejam/2018_finals/virus/solution.cpp @@ -0,0 +1,252 @@ +#include +#include +#include +#include +#include +#include +#include "message.h" // NOLINT +#include "virus.h" // NOLINT + +#define MAX_UNHEALTHY_NODES 3 + +// Overall solution strategy: +// The first problem to solve is that the infected nodes don't know they're +// infected. The way we solve this is that we divide the nodes in pairs, and the +// pairs exchange messages twice. This way each pair which contains two clean +// nodes will know it (and the pair as a whole will contain information needed +// to deduce which nodes in the pair were the source of infection, if any). +// The second problem is to get the information out of the infected pairs, +// without infecting the nodes that the information gets to. The way to solve +// this is "negative" information - that is, we will pass information by _not_ +// sending a message. More precisely, for instance, if we want all the healthy +// pairs to report, we'll have all the healthy pairs send a message somewhere. +// Note that the receiver doesn't know how many messages they're to receive, so +// we have to implement a timeout. The way this works is that the even nodes +// will be the receivers (receiving via Receive(-1)), while the odd nodes will +// sleep for a time, and then send a "stop waiting" message to the even node. +// +// After that, the rest is implementation. We'll get the list of healthy nodes, +// and then out of each unhealthy pair get the information as to what was the +// source of the infection. The implementation here requires at least 8X + 2 +// nodes, where X is the max unhealthy nodes (the calculation is - X unhealthy +// pairs, 3X listener pairs, and one master), a more efficient implementation is +// likely possible. + +void Wait() { + usleep(300000); +} + +void Give(int target) { + VirusPutChar(target, 1); + VirusSend(target); +} + +bool Get(int target) { + VirusReceive(target); + return GetChar(target); +} + +int main() { + // In general, we'll send single-char messages consisting of a one. If we ever + // receive a zero, we're infected. + int N = NumberOfNodes(); + int M = MyNodeId(); + + assert(N % 2 == 0); + int partner = M ^ 1; + + // Exchange messages with the partner. If the partner was an origin, we will + // get an infected message (and we're now infected). + Give(partner); + bool partner_infected = !Get(partner); + // Now, give back to the partner. This message will be infected if either we, + // or the partner was infected. + Give(partner); + bool either_infected = !Get(partner); + // Note that we can be in three states now: we can know that none of us was + // the origin (both variables false), or that I am the origin and the partner + // isn't (if the first variable is false and the second is true), or that the + // partner is the origin, and my state is unknown (if the first message is + // true, the second will be necessarily true in that case). + // Also note that our current state with the partner is identical. + + // Now, there's a certain amount of setup needed; and it will be valid for + // only certain nodes. Comments at each variable say for which nodes will it + // be set up correctly. + // List of all healthy even nodes. Valid on all even nodes. + std::vector healthy_even_nodes; + // The complement of the list above. Valid on all even nodes. + std::vector unhealthy_even_nodes; + // Node that will eventually print out the answer. Valid on all even nodes. + int master = -1; + // For each unhealthy node pair, the list of three nodes that will listen to + // them. Valid on all even nodes. + std::vector listeners[MAX_UNHEALTHY_NODES]; + // Will be set (to the ID of the even unhealthy node listened to) on each node + // listed in listeners. Otherwise, set to -1 (on all nodes). + int listened = -1; + // Is set (to the index of the listener in the listeners vector) on every + // listener. Set to -1 on non-listeners. + int listener_index = -1; + // The listeners for a given unhealthy node pair. Valid on both elements + // of the node pair, empty elsewhere. + std::vector my_listeners; + + // All the even healthy nodes signal all the even nodes. + if ((M & 1) == 0 && !either_infected) { + for (int i = 0; i < N; i += 2) { + Give(i); + } + } + + // All the even nodes pick up signals in a loop. The odd nodes wait + // (hopefully enough for all the signals to get in), and then send a stop + // signal to the even nodes to break them out of the loop. This works + // regardless of whether the receiving node is infected or not. + if (M & 1) { + Wait(); + Give(M ^ 1); + } else { + while (true) { + int healthy = VirusReceive(-1); + GetChar(healthy); + if (healthy & 1) break; + healthy_even_nodes.push_back(healthy); + } + } + + // We need to wait before sending any more messages. Otherwise, the messages + // we send now might get mixed up with the messages reporting healthiness + // above. + Wait(); + Wait(); + + if ((M & 1) == 0) { + // The sort is needed here for determinism later on (where we designate the + // master node, and listener nodes, based on the order in this array; and we + // need all the even nodes to designate the same master and listeners). + std::sort(healthy_even_nodes.begin(), healthy_even_nodes.end()); + // Look for each even node in the set of healthy nodes. If we don't find a + // node, it must be a part of an unhealthy pair. + for (int i = 0; i < N; i += 2) { + if (std::find(healthy_even_nodes.begin(), healthy_even_nodes.end(), i) == + healthy_even_nodes.end()) { + unhealthy_even_nodes.push_back(i); + } + } + assert(unhealthy_even_nodes.size() <= MAX_UNHEALTHY_NODES); + // We presume all the even nodes have the same set of healthy nodes. So, + // let's designate some special nodes in there. First, we designate the + // first healthy node as the overall master. Note that the unhealthy even + // nodes are in the same position, they also know about all the healthy + // nodes. + master = healthy_even_nodes[0]; + // Then, for each unhealthy node pair, we designate three listeners. + int curr = 1; + for (int i = 0; i < unhealthy_even_nodes.size(); ++i) { + for (int j = 0; j < 3; ++j) { + listeners[i].push_back(healthy_even_nodes[curr]); + // Make note if I'm a listener. + if (healthy_even_nodes[curr] == (M & ~1)) { + listened = unhealthy_even_nodes[i]; + listener_index = j; + } + curr += 1; + } + } + } + // Now, each unhealthy node pair needs to determine who was the + // origin of the infection out of the pair. For this, both the even node + // and the odd node need to send data (because the even node doesn't know it + // all). So, the dedicated master will send the data to the odd unhealthy + // nodes. For ease of coding, we'll also send it to the even nodes. + // Note the comparison "M == master" works correctly on odd nodes, since + // "master" is initialized to -1. + if (M == master) { + for (int i = 0; i < unhealthy_even_nodes.size(); ++i) { + for (int j = 0; j < 3; ++j) { + VirusPutChar(unhealthy_even_nodes[i] ^ 1, listeners[i][j]); + VirusPutChar(unhealthy_even_nodes[i], listeners[i][j]); + } + VirusSend(unhealthy_even_nodes[i] ^ 1); + VirusSend(unhealthy_even_nodes[i]); + } + } + // In the meantime, the healthy odd nodes don't know whether they're + // listeners, and their even counterparts should tell them. + if (!either_infected) { + if ((M & 1) == 0) { + VirusPutChar(M ^ 1, listener_index + 1); + VirusSend(M ^ 1); + } else { + VirusReceive(M ^ 1); + listener_index = GetChar(M ^ 1) - 1; + if (listener_index != -1) listened = 1; + } + } + + if (either_infected) { + master = VirusReceive(-1); + for (int i = 0; i < 3; ++i) my_listeners.push_back(GetChar(master)); + // Now, the infected nodes send data to the listeners. See below for + // details. + if (partner_infected) { + Give(my_listeners[1 + (M & 1)]); + } else { + // I am infected, my partner isn't + Give(my_listeners[0]); + } + } + // There are three cases now: + // * If they are both infected, the odd node sends a message to listener 2, + // the even node sends a message to listener 1, and listener 0 gets nothing. + // * If only the odd node is infected, then the even node sends a message to + // listener 1, while the odd node sends messages to listener 0. Listener 2 + // gets nothing. + // * if only the even node is infected, then the odd node sends a message to + // listener 2, while the even node sends a message to listener 0. Listener 1 + // gets nothing. + if (listened != -1) { + if (M & 1) { + Wait(); + Give(M ^ 1); + } else { + int sender = VirusReceive(-1); + if (sender == (M ^ 1)) { + // Aha - we got a message from our odd node, meaning no message from the + // infected node we're listening to. So, we're still healthy, and we + // know a part of the infected nodes. Let's tell the master. + if (listener_index == 0) { + VirusPutChar(master, 2); + VirusPutChar(master, listened); + VirusPutChar(master, listened ^ 1); + } else if (listener_index == 1) { + VirusPutChar(master, 1); + VirusPutChar(master, listened); + } else { + assert(listener_index == 2); + VirusPutChar(master, 1); + VirusPutChar(master, listened ^ 1); + } + VirusSend(master); + } + } + } + // And now the master needs to pick up those messages. + if (M == master) { + std::vector infection_origins; + for (int i = 0; i < unhealthy_even_nodes.size(); ++i) { + int sender = VirusReceive(-1); + int data_size = GetChar(sender); + for (int j = 0; j < data_size; ++j) { + infection_origins.push_back(GetChar(sender)); + } + } + std::sort(infection_origins.begin(), infection_origins.end()); + PrintNumber(infection_origins.size()); + for (int i = 0; i < infection_origins.size(); ++i) { + PrintNumber(infection_origins[i]); + } + } + return 0; +} diff --git a/distributed_codejam/2018_finals/virus/statement.html b/distributed_codejam/2018_finals/virus/statement.html new file mode 100644 index 00000000..b28f171b --- /dev/null +++ b/distributed_codejam/2018_finals/virus/statement.html @@ -0,0 +1,101 @@ +

    Problem

    + +

    Virus

    +

    + A virus has crept into a small cluster of DCJ judging machines. We have + isolated the issue, and it will not affect the contest, but we now want to + identify the root cause of the issue - the exact node or nodes where the virus + was inserted. +

    + The virus has three effects: +

      +
    1. Whenever any message is sent from an infected node to another node + — that is, when an infected node invokes the Send + function with the first argument different from its node ID — all + the bytes in a message are turned to zeroes. That is, if the receiving + node calls GetChar, it will get a zero byte (that is, the + byte 00000000). If it calls GetInt or GetLL, + it will get zero. Note that this does not happen for messages the node + sends to itself.
    2. +
    3. Whenever anything is printed to stdout by an infected node, it is + changed to a single letter "X".
    4. +
    5. Whenever any infected node sends a message, this message spreads the + virus. If another node Receives that message, it is, from + that moment on, also infected.
    6. +
    +

    + Your program must print only the IDs of the originally infected nodes — + that is, the nodes that were infected before your + program started to run. Do not include other nodes that were not originally + infected, but became infected in the course of solving the problem. Each test + case is independent; infected nodes do not "carry over" between test cases. +

    + +

    Input

    +

    + The input library is called "virus"; see the sample inputs below for + examples in your language. It works a bit differently than in other DCJ + problems. Note that technically no input library is needed for this + problem. However, to assist in testing, we provide a helper input + library. It defines four functions. These functions, in your local + environment, simulate the behavior of the virus. In the DCJ environment, + of course, there is no need to simulate the behavior of the virus, + because it is really there! However, for convenience, we still provide + the virus library; its functions will simply forward to corresponding + functions in the message library (so that you can test and submit the same + code). The functions provided are: +

    +
      +
    • VirusPutChar(target, value)
    • +
    • VirusSend(target)
    • +
    • VirusReceive(source) +
        +
      • These functions take the same arguments as their message library + equivalents, but the test implementations provided will simulate the + virus library. The implementation that will be used in the DCJ system + will just forward to the message library. +
      • +
      +
    • +
    • PrintNumber(number): +
        +
      • Takes a 64-bit integer: the number to print.
      • +
      • Prints a line containing that one number to stdout.
      • +
      • In the test version, it will print the letter X instead + if the node is infected.
      • +
      +
    • +
    • The test version of the virus library has the list of infected nodes + hardcoded into it in the first few lines. You have to change that in order + to test different scenarios. +
    • +
    + +

    Output

    +

    + In the first line of the output, print one number N: the number of + originally infected nodes. +

    +

    + In the next N lines, print the IDs of the originally infected nodes, + one per line, in increasing order. +

    + +

    Limits

    +

    + Time limit: 8 seconds.
    + Number of nodes: 40 (for the Small dataset, too!)
    + Memory limit per node: 256 MB.
    + Maximum number of messages a single node can send: 1000.
    + Maximum total size of messages a single node can send: 8 MB.
    +

    + +

    Small dataset

    +

    + There will be exactly one originally infected node. +

    + +

    Large dataset

    +

    + The number of originally infected nodes will be between 0 and 3, inclusive. +

    diff --git a/distributed_codejam/2018_r1/analysis_intro.html b/distributed_codejam/2018_r1/analysis_intro.html new file mode 100644 index 00000000..ef65aaaf --- /dev/null +++ b/distributed_codejam/2018_r1/analysis_intro.html @@ -0,0 +1,62 @@ +

    + The online round of our fourth Distributed Code Jam tournament presented + four more parallelization challenges. Stones and Towels were + the "easier" two of the four, but it was not trivial to find good distribution + mechanisms and get the details right. Kenneth presented a new twist on + our theme (introduced in 2017) of nodes behaving differently — in this + case, different nodes had different parts of the input data. Finally, + One More Time revived a problem type + from + 2016 + in which contestants had to understand a piece of hopelessly slow code and + rewrite it to be more efficient. +

    + Distributed Code Jam, which is never exactly known for being easy, was harder + than usual in this round. Even for our top competitors, it was a challenge to + fully solve even two of the problems. The last problem was tough enough that + we only got our first correct Small submission around two and a half hours + into the contest. The number of submissions heated up quite a bit in the last + half hour of the contest, and the large number of 100-node Small datasets + meant that the judging system fell behind; we apologize to those of you who + experienced delays. +

    + Twenty seconds before the end of the round, Errichto.rekt successfully + solved One More Time, jumping into first place and thereby becoming + the first contestant ever to sweep both the Code Jam and Distributed Code Jam + semifinal rounds. mareksom and apiapiapiad were second and + third with fast 64-point performances (all three of the first problems). + The tentative cutoff is at 51 points; our defending Distributed Code Jam + champion ecnerwala made this cutoff. We also want to extend a special + shout-out to Xhark for being the only other contestant besides + Errichto.rekt to solve the hardest problem. +

    + This wraps up the online rounds of the Code Jam 2018 tournament! We will see + our Code Jam and Distributed Code Jam advancers in Toronto in two months for + the World Finals. Thanks to everyone who participated in DCJ! +

    +
    +

    + Cast +

    + Problem A (Testrun): Written by Louis Friend. +

    + Problem B (Stones): Written by Onufry Wojtaszczyk. +

    + Problem C (Towels): Written by Ian Tullis and Onufry Wojtaszczyk. +

    + Problem D (Kenneth): Written by Igor Naverniouk and Onufry Wojtaszczyk. +

    + Problem E (One More Time): Written by Onufry Wojtaszczyk. +

    + All problems prepared by Onufry Wojtaszczyk, with reviews and other + contributions from Mohammed Hossein Bateni, Md Mahbubul Hasan, Pablo Heiber, + Ian Tullis, and Won-seok Yoo. +

    + Analysis authors: +

    +
      +
    • Stones: Ian Tullis and Onufry Wojtaszczyk
    • +
    • Towels: Onufry Wojtaszczyk
    • +
    • Kenneth: Ian Tullis and Onufry Wojtaszczyk
    • +
    • One More Time: Pablo Heiber and Onufry Wojtaszczyk
    • +
    diff --git a/distributed_codejam/2018_r1/kenneth/analysis.html b/distributed_codejam/2018_r1/kenneth/analysis.html new file mode 100644 index 00000000..e37b663a --- /dev/null +++ b/distributed_codejam/2018_r1/kenneth/analysis.html @@ -0,0 +1,141 @@ +

    Kenneth: Analysis

    +

    + Let us call the full recorded signal R. The goal of this problem is to find + the shortest possible string S such that R is composed of the concatenation + of some number of copies of S. |S| (that is, the length of S) is the + "frequency" (as defined in the problem statement) that we are looking for. +

    + By calling GetPieceLength() on every node, we can find |R|, which is at most + 109. |S| must be a divisor of this number; we can calculate the + full set of possible divisors, of which there are + at most 1344. +

    + For |S| to be a valid "frequency" for the string, we need R[i] = R[i-|S|] for + any i. There are two things we need to check to validate this condition: + it must be true within each node, and, if it is true, all of the nodes must + agree on what S actually is. For instance, if we are checking |S| = 4, and + node 0 has DABCDABCDA and node 1 has CDABCDABCD, + the condition holds within each node, but node 0 thinks S is DABC + and node 1 thinks S is ABCD. Note that a candidate length + |S| = 4 implies that S must start at the third character on node 1. +

    + Let U denote the piece of S stored on some node. For checking within that + node, the naive approach of simply checking all letters U[i] will be too slow + — we have up to 107 letters on each node, and up to 1344 + divisors, and over half of those could be smaller than 105. We + need a faster way of checking a single divisor. +

    + Fortunately, the condition U[i] = U[i-|S|] for any i is equivalent to simply + saying that the prefix of U of length |U|-|S| is also a suffix of U. There + are a number of ways to check this; the simplest and fastest one is probably + the + Knuth-Morris-Pratt (KMP) algorithm. + Specifically, we have each node calculate the KMP "partial match" table for + its piece. From the KMP table, we can calculate the set of all + suffix-prefixes of U (and allow for log-time lookup in it, or even constant + time if we are willing to spend the memory on that). This makes it easy to + check the condition for each of our candidate value of |S|, without doing + redundant work. +

    + An alternative approach is to use + polynomial hashing + to calculate the hash of any substring, and check that the hashes of the + prefix and suffix of length |U|-|S| are the same. This may be a better choice + in the long run, since, as we will see, we will need a polynomial hash for a + later step of the solution anyway. +

    +

    Small dataset

    +

    + In the Small dataset, each of the 100 nodes has data of the same length + |U|, and |S| is guaranteed to be at most half of |U|, so |S| is at most + |R| / 200. This guarantees that the entire string S is present at least once + on every node, and also narrows down the number of candidate values of |S| we + must check. So, our Small solution is as follows: +

    +
      +
    1. On each node: +
        +
      1. Precalculate rolling hashes for each position in the node's + piece.
      2. +
      3. For each candidate value of |S| ≤ |R| / 200: +
          +
        1. Use KMP or the hashes to determine whether |S| is valid from this + node's point of view.
        2. +
        3. Find the hash of the S implied by that |S|.
        4. +
        +
      4. +
      5. For each valid candidate |S|, send |S| and the hash to the master. +
      6. +
      +
    2. +
    3. On the master: Check all possible values of |S|, starting from the + smallest. If a value is a valid candidate on every node, and the hashes for + that candidate on all of those nodes match, then that value is our answer. +
    4. +
    +

    Large dataset

    +

    + For the Large dataset, the process for checking whether a candidate |S| is + valid on a given node is the same. What changes is the validation, since it + is not necessarily the case that all of S is contained within any particular + node. +

    + Consider some candidate |S|. We may eliminate it in some node by using the + KMP or hash check. Otherwise, each node knows some substring, or possibly + some prefix and some suffix, of the corresponding S, and we need to check + whether all these pieces actually match up. So, let us pick some instance of + S in the full recorded signal R — for instance, the first |S| + characters. These are possibly split between nodes somehow. Let's call this + instance of S the "canonical instance"; we will check whether all the nodes + have data that matches the canonical instance. +

    + The canonical instance may be split between one or more nodes; let us + consider one such node. Given a candidate length |S|, that node knows which + parts of the candidate string S it has. It also knows the lengths of the + signal pieces on the other nodes, so, for each of those nodes, it might be + able to predict at least part of what that node "should" see, and in which + positions. So, it can relay that "prediction" to the other nodes. +

    + Of course, we can't afford to send predictions as entire strings, so we'll use + polynomial hashing. The huge advantage is that once a node has the polynomial + hash table calculated, then when it receives a hash of a prediction, it can + check in constant time whether it matches the expected value. +

    + So, our Large solution is: +

      +
    1. On each node i: +
        +
      1. Precalculate rolling hashes for each position in the node's + piece.
      2. +
      3. For each candidate value of |S|: +
          +
        1. Use KMP or the hashes to determine whether |S| is valid from this + node's point of view.
        2. +
        3. Calculate the polynomial hash of Si "intersected" with + Sj (that is, node i's predictions about node j) for each + j ≠ i. (It might be two hashes, if a beginning and an end piece + of S are visible on node i, and both of these intersect with a + particular node j).
        4. +
        +
      4. +
      5. Send that data to all other nodes. The message from one node to + another contains at most 1344 x 2 hashes, plus 1344 booleans, so it + uses a total of 22KB of data, more or less. +
      6. +
      7. + Check whether all the received hashes match its own data, for each |S|. + This also does not take very many operations, since each check takes + linear time. This produces a bit vector of length at most 1344; send + that to the master. +
      8. +
      +
    2. +
    3. On the master: Check all possible values of |S|, starting from the + smallest, until we find one that is valid in every vector. +
    4. +
    +

    + The rest comes down to implementation details like avoiding weak hashes + (e.g., hashing modulo 264) that could be broken by test cases. +

    + diff --git a/distributed_codejam/2018_r1/kenneth/statement.html b/distributed_codejam/2018_r1/kenneth/statement.html new file mode 100644 index 00000000..5079bac9 --- /dev/null +++ b/distributed_codejam/2018_r1/kenneth/statement.html @@ -0,0 +1,89 @@ +

    Problem

    + +

    Kenneth

    +

    +There are strange signals being beamed from outer space, and you have set up nodes +to record them. Now you would like to find the frequency of those signals +in order to be able to block them out. +

    + +

    +The recorded signal consists of some string s, repeated some number of +times. The length of s is called the frequency of the signal. +It is guaranteed that you recorded the whole signal, from beginning to end. +

    + +

    +Each node now holds a part of the recorded signal. That is, the whole signal +(which is s repeated a certain number of times) is broken up into +NumberOfNodes() contiguous pieces, and the ith node holds the +ith piece of the signal. The pieces do not necessarily contain whole +repetitions of s — for example, if the signal is the string AAB repeated +three times, and NumberOfNodes() is 4, the first node might contain +AAB, the second AA, the third BAA, and the fourth just B. +

    + +

    +Your task is to determine the frequency (that is, the length of the string +s). If there are many possible strings s, choose the shortest +one possible. +

    + +

    Input

    +

    + The input library is called "kenneth"; see the sample inputs below for + examples in your language. It defines two methods: +

    +
      +
    • GetPieceLength(node_index): +
        +
      • Takes a 64-bit integer: the index of the node we want to know + about.
      • +
      • If the index is less than 0 or greater than 99, this will crash.
      • +
      • Returns a 64-bit integer: the length of the signal piece on that + node.
      • +
      • Expect each call to take 0.07 microseconds.
      • +
      +
    • +
    • GetSignalCharacter(position): +
        +
      • Takes a 64-bit number: the position of the character in the + signal (counting starting from 0).
      • +
      • If the position is less than sum(GetPieceLength(i)) for i < + MyNodeId(), this will crash.
      • +
      • Similarly, if the position is equal to or greater than + sum(GetPieceLength(i)) for i ≤ MyNodeId(), this will crash.
      • +
      • Otherwise, returns the character from the given position in the + signal. The character is an uppercase letter of the English + alphabet.
      • +
      • Expect each call to take 0.07 microseconds.
      • +
      +
    • +
    + +

    Output

    +

    + Output a single integer: the smallest possible frequency of the signal. +

    + +

    Limits

    +

    + Time limit: 4 seconds.
    + Number of nodes: 100 (for the Small dataset, too!)
    + Memory limit per node: 256 MB.
    + Maximum number of messages a single node can send: 1000.
    + Maximum total size of messages a single node can send: 8 MB.
    + 1 ≤ GetPieceLength(node_index) ≤ 107.
    + GetSignalCharacter(position) returns a uppercase letter between A and Z, inclusive.
    +

    + +

    Small dataset

    +

    + GetPieceLength returns the same value for all nodes.
    + The frequency is no larger than GetPieceLength(0) / 2.
    +

    + +

    Large dataset

    +

    +No extra limitations. +

    diff --git a/distributed_codejam/2018_r1/one_more_time/analysis.html b/distributed_codejam/2018_r1/one_more_time/analysis.html new file mode 100644 index 00000000..af0af8d2 --- /dev/null +++ b/distributed_codejam/2018_r1/one_more_time/analysis.html @@ -0,0 +1,89 @@ +

    One More Time: Analysis

    +

    + This problem has two parts: figuring out what the code does, and doing it better. +

    +

    What is the problem?

    +

    + The most important realization is that the value of calculate(index) does not + depend on the node in which the call is made. This is because the only part of + the body of calculate that is node-dependent is the first 3 lines; + those lines call get_number, which in turn "questions" some other node + for index, which is answered by returning calculate(index), too. So, this + part either hangs forever or ends up returning the result of the last 4 lines of + the given code back to the original questioner. +

    + To see that it doesn't hang forever, notice that replacing + MyNodeId() inside the condition in the first line of + calculate(index) with the definition of + responsible_node calculated in get_number + gives us + index < 1 + ((index - 1) * NumberOfNodes() / GetN()) * GetN() / NumberOfNodes() + which is always false. Because the division is truncated, we can't just simplify to + index < index. However, + ((index - 1) * NumberOfNodes() / GetN()) * GetN() + is just ((index - 1) * NumberOfNodes() - (((index - 1) * NumberOfNodes()) % GetN()). + Dividing by NumberOfNodes() we obtain + (index - 1) - ((((index - 1) * NumberOfNodes()) % GetN()) / NumberOfNodes()) and + index - 1 < GetN(), which makes the subtrahend equal to 0. + A faster way to arrive to the same conclusion is to notice that the statement assumes the given + code is correct for the given inputs, so it must terminate. +

    + This leaves us with the problem of calculating F(GetN()), where + F is defined by F(i) = F(i-1) + GetM() * F(i-2) + F(i/3) + GetA(n) + with F(0) = F(1) = F(2) = 1. For the Small dataset, since GetM() = 0, this + reduces to F(i) = F(i-1) + F(i/3) + GetA(n). +

    +

    A faster approach

    +

    + Non-distributedly, we can calculate this in O(GetN()) with a simple + dynamic programming algorithm that keeps an array of values we know for F, and calculates + them in increasing order. To distribute this, we have to think further. +

    + We can take advantage of the fact that to calculate F over a range of values + [A, B], we only need to know F(A-1), F(A-2) and F over the range [A/3, B/3]. + We can make a particular node responsible for the ranges [3iA, 3iB] + for each i, which leaves us only the values F(3iA-1) and F(3iA-2)) + to require externally. To simplify the notation, assume we start by computing F in the + range [0, X] for a sufficiently large X in every node, using the single-node version + above, so that they all have a basis to start that doesn't require information from other + nodes. +

    + Then, we can proceed in stages. In stage i, a node computes F on all values in the + range [3iA, 3iB], using the values computed in the previous stage, + and the values F(3iA-1) and F(3iA-2)) it receives via message from another + node, if they are not in the [0, X] range. The problem is that this is a serial algorithm. + The only node that can proceed is the one starting with a range [X + 1, B], which can then pass + some values to some other node that can now calculate [B + 1, C], etc, but all nodes except + one are waiting on some other node for its two extra values! This distribution strategy does + not gain us anything. +

    + To do better, let us split the Small and Large datasets. For the Small dataset, + each node only requires one pending value F(3iA-1). + The sequence for the Small dataset is linear on the + pending values. That is, if F(3iA-1) decreases by 1, then so do F(3iA), + F(3iA+1), etc. + So, during stage i, every node can calculate its range assuming the extra + value it doesn't know is 0, and then, when that value arrives from another node, + add it to every calculated value. +

    + For the Large dataset, the sequence behaves like a modified Fibonacci + sequence. That is, if F(3iA-1) decreases by 1, + then F(3iA) decreases by 1, F(3iA+1) decreases by 1 + M, + F(3iA+2) decreases by 2M + 1, + F(3iA+3) decreases by M2 + 3M + 1, and in general + F(3iA+k) decreases by Fib(k+1), where Fib(k) is the k-th term in the + Fibonacci sequence + Similarly, if we decrease F(3iA-2) by 1, F(3iA+k) decreases + by GetM() × Fib(k). As for the Small dataset, each node can + calculate the values in its range for stage i assuming the two pending values are + zero, and then apply the actual values when they arrive using the formula above. + Doing this efficiently requires pre-computing the Fibonacci sequence on each node, + but we only need few terms: if the maximum range of any stage is of size K, we need + at most K+2 terms of the sequence. We can do this at the beginning of the process + with minimal impact on the overall running time. +

    + A comment on the value of X above: while the minimum possible value for it is rather small, + in practice it's better to set it to a high value like 107 to save the first + few stages in which the ranges calculated by each node are small and the running time + is dominated by the latency of passing around messages. +

    diff --git a/distributed_codejam/2018_r1/one_more_time/statement.html b/distributed_codejam/2018_r1/one_more_time/statement.html new file mode 100644 index 00000000..f8b5f9e0 --- /dev/null +++ b/distributed_codejam/2018_r1/one_more_time/statement.html @@ -0,0 +1,146 @@ +

    Problem

    +

    ...One More Time.

    +

    + We really thought we had it covered, especially after we didn't make this mistake in 2017. + But - one more time - + we have lost the problem statement and the solutions, and all we have left are these two correct, + but far too slow (and misguided) solutions, one per language. Fortunately, we still have the test + data. Can you still solve the problem? +

    + +

    +The C++ solution: +

    +#include <message.h>
    +#include <stdio.h>
    +#include "more.h"
    +
    +long long int get_number(long long index) {
    +  int responsible_node = (index - 1) * NumberOfNodes() / GetN();
    +  PutLL(responsible_node, index);
    +  Send(responsible_node);
    +  Receive(responsible_node);
    +  return GetLL(responsible_node);
    +}
    +
    +long long int calculate(long long int index) {
    +  if (index < 1 + MyNodeId() * GetN() / NumberOfNodes()) {
    +    return get_number(index);
    +  }
    +  if (index < 3) return 1LL;
    +  return (calculate(index - 1) + GetM() * calculate(index - 2) + calculate(index / 3) + GetA(index))
    +      % GetP();
    +}
    +
    +int main() {
    +  if (MyNodeId() < NumberOfNodes() - 1) {
    +    while (true) {
    +      int questioner = Receive(-1);
    +      long long index = GetLL(questioner);
    +      if (index == 0) return 0;
    +      PutLL(questioner, calculate(index));
    +      Send(questioner);
    +    }
    +  } else {
    +    printf("%lld\n", calculate(GetN()));
    +    for (int node = 0; node < NumberOfNodes() - 2; ++node) {
    +      PutLL(node, 0);
    +      Send(node);
    +    }
    +    return 0;
    +  }
    +}
    +

    +

    +The Java solution: +

    +public class Main {
    +  private static long getNumber(long index) {
    +    int responsible_node = (index - 1) * message.NumberOfNodes() / more.GetN();
    +    message.PutLL(responsible_node, index);
    +    message.Send(responsible_node);
    +    message.Receive(responsible_node);
    +    return message.GetLL(responsible_node);
    +  }
    +
    +  private static long calculate(long index) {
    +    if (index < 1 + message.MyNodeId() * more.GetN() / message.NumberOfNodes()) {
    +      return Main.getNumber(index);
    +    }
    +    if (index < 3) return 1LL;
    +    return (Main.calculate(index - 1) + more.GetM() * Main.calculate(index - 2) +
    +            Main.calculate(index / 3) + more.GetA(index)) % more.GetP();
    +  }
    +
    +  public static void main(String[] args) {
    +    if (message.MyNodeId() < message.NumberOfNodes() - 1) {
    +      while (true) {
    +        int questioner = message.Receive(-1);
    +        long index = message.GetLL(questioner);
    +        if (index == 0) {
    +          return;
    +        }
    +        message.PutLL(questioner, Main.calculate(index));
    +        message.Send(questioner);
    +      }
    +    } else {
    +      System.out.println(Main.calculate(more.GetN()));
    +      for (int node = 0; node < message.NumberOfNodes() - 2; ++node) {
    +        message.PutLL(node, 0);
    +        message.Send(node);
    +      }
    +    }
    +
    +

    Input

    +

    +The input library is called "more"; see the sample inputs below for examples in +your language. It defines four methods: + + + + + + + + + + + + + + + + + + +
    Method name and parametersParameter limitsReturnsApproximate time for a single call
    GetN()a 64-bit integer0.05 microseconds
    GetP()a 64-bit integer0.05 microseconds
    GetM()a 64-bit integer0.05 microseconds
    GetA(i)3 ≤ i ≤ GetN()a 64-bit integer0.05 microseconds
    +

    + +

    Output

    +

    +Output what either of the solutions above would output, if they ran without any limits +on memory, time, number of messages or total size of messages. +

    + +

    Limits

    +

    +Time limit: 7 seconds.
    +Memory limit per node: 256 MB.
    +Maximum number of messages a single node can send: 1000.
    +Maximum total size of messages a single node can send: 8 MB.
    +2 ≤ GetP() ≤ 106.
    +0 ≤ GetA(i) < GetP(), for all i.
    +3 ≤ GetN() ≤ 109.
    +0 ≤ GetM() < P.
    +Number of nodes: 100 (for the Small dataset, too!).
    +

    + +

    Small dataset

    +

    + GetM() = 0.
    +

    + +

    Large dataset

    +

    + No extra restrictions. +

    diff --git a/distributed_codejam/2018_r1/stones/analysis.html b/distributed_codejam/2018_r1/stones/analysis.html new file mode 100644 index 00000000..caf208bd --- /dev/null +++ b/distributed_codejam/2018_r1/stones/analysis.html @@ -0,0 +1,107 @@ +

    Stones: Analysis

    +

    Small dataset

    +

    + The optimal strategy is not to always jump as far as each stone lets us jump; + this might cause us to skip a stone that could have let us jump even farther. + However, we cannot afford to explicitly check each possible jump length from + each stone, even if we use dynamic programming. +

    + We can solve the Small dataset on a single node as follows. We will iterate + through the stones from start to finish, and as we do this, we will keep + track of a few things: +

    +
      +
    • num_jumps: how many jumps we have made so far (initially 0)
    • +
    • current_farthest: the farthest stone we could reach with the jumps we + have made
    • +
    • possible_farthest: the farthest stone we could in theory reach from all + the stones processed so far
    • +
    +

    + As our iteration examines each stone, we update possible_farthest, replacing + it if our current stone would let us get even farther than that. Whenever our + iteration passes the current_farthest stone, we increase num_jumps by one, + and say that with this one extra jump, we can reach any stone that we could + reach from all the stones processed so far. That is, we set current_farthest + to possible_farthest. Then, we continue to iterate and update + possible_farthest, etc., until we are done. +

    + This solution takes constant memory and takes time linear in the number of + stones. +

    +

    Large dataset

    +

    + To solve the Large dataset, we will use the following observation: if there + is a stone X from which we can jump to A, and a stone Y from which we can + jump to B, where X < Y and A > B, then the answer does not change if we + increase the jump length in Y to reach A. The reason is that even if we make + the change, any path that would use stone Y can be modified to use X instead + (and so the fact we increased the jump length at Y doesn't improve the + answer, and clearly does not make it worse). This allows us to replace the + sequence of jump targets (i.e., the sequence i + GetJumpLength(i)) with the + smallest larger non-decreasing sequence. After this transformation, the + optimum solution is always to jump as far as possible (since the farther away + we land, the farther away the next jump will go). +

    + However, if we need to split the data up across multiple nodes, we cannot + easily execute the above greedy strategy. For example, suppose that we have + 1000000 stones per node, and that on node 0, we find that we should jump from + stone 0 to stone 1000, and from stone 1000 to stone 999000, and from stone + 999000 to stone 1234567, which is on node 1. But what if some later stone + on node 0 would let us get even farther, to 1600000, whereas the stones + between 1234567 and 1600000 waste time by only allowing tiny jumps? If we + were not distributing the solution, our greedy strategy would have caught + this by making the value at stone 1234567 at least 1600000. But when we are + calculating the transformed jump target values on node 1 alone, it has no way + of knowing about this possible jump to 1600000 from node 0, and it may come + up with a smaller value. So we also need to consider the case in which we jump + to the last stone in node 0, and then jump from there to the next node, or + even to some other more distant node. (Why the last stone? We already know we + can get out of node 0 in one more jump from stone 999000, so we have our pick + of all remaining nodes 999000-999999 as the next target, and it can only help + us to pick 999999.) +

    + So, in each node (in parallel), we want, for each stone, to precompute two + things: +

    +
      +
    1. If we jump as far as possible each time, what's the last stone we visit + in this node's range?
    2. +
    3. How many jumps do we need from here to reach that last stone?
    4. +
    +

    + After completing these calculations in a node, we will know the last stone we + will definitely land on in that current node. Then, we must alert the next + node to two possibilities: either we jump as far as possible from that stone + (and land in some future node), or we jump from there to the very last stone + in the current node, and from there to some future node. (Other options are + strictly worse given the transformation we made earlier.) We can summarize + these two possibilities in three values: +

    +
      +
    • the cost so far
    • +
    • the stone we would jump to from the last stone we will definitely land + on in the current node
    • +
    • the stone we would jump to from the last stone in the current node
    • +
    +

    + Note that we send only two possible jump lengths out of each node: where we + can land with COST jumps, and where we can land with COST + 1 jumps. This + makes sense for any node — notice that if we can get out of that node in + COST jumps, we can get to any of the stones in that and previous nodes in + COST jumps, and so we can get to any reachable stone from those stones in + COST + 1 jumps. So, it never makes sense to make COST + 2 or more jumps. +

    + When a node receives the three values from the previous node, it must first + check whether the least far of those two jumps goes over the entire node. If + so, it passes along the info to the next node. If not, it evaluates the + two jumps (keeping in mind that the farther of the two might still go over + the entire node). It uses the per-position calculations it has already + performed to calculate and send its own set of three values, maintaining the + invariants that the farther of the two jumps always has a cost one larger. + (We leave the remaining implementation details to the reader.) +

    + This solution is O(NumberOfStones / NumberOfNodes) in both time and memory. + The precomputation phase dominates the communication phase, assuming that + the time to transmit messages is small. +

    diff --git a/distributed_codejam/2018_r1/stones/statement.html b/distributed_codejam/2018_r1/stones/statement.html new file mode 100644 index 00000000..163d2132 --- /dev/null +++ b/distributed_codejam/2018_r1/stones/statement.html @@ -0,0 +1,70 @@ +

    Problem

    + +

    Stones

    +

    + You are standing on one side of a very broad river, and the love of your life is on the other + side. There is no reason to think for too long -- you will need to jump across! +

    + +

    + Fortunately, the river contains a single line of stepping stones that you can jump on. Because the + stones are not necessarily all equally easy to stand on, the distance that you can jump + can differ from stone to stone! For each stone, you know the farthest stone you can jump to + from it. (You can jump to that stone, or any other stone between it and your current stone.) + So, maybe there is some need to think about this - you want to make as few jumps as possible, + since every jump carries a risk that you will trip, fall into the water, and embarrass yourself + while the love of your life is watching. +

    + +

    Input

    +

    + The input library is called "stones"; see the sample inputs below for + examples in your language. It defines two methods: +

    +
      +
    • GetNumberOfStones(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of stones in the river.
      • +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    • GetJumpLength(location): +
        +
      • Takes a 64-bit integer: the index (1-based) of the stone you are considering jumping + from, or 0 (to denote jumping from the shore you are beginning on).
      • +
      • Returns a 64-bit integer: the maximum distance (in stones) that you can jump forward from + this one. If you can land on the opposite shore, the returned number will be + larger than the number of stones in front of you.
      • +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    + +

    Output

    +

    + Output a single integer: the minimum number of jumps you need to make to cross the river. +

    + +

    Limits

    +

    + Time limit: 3 seconds.
    + Memory limit per node: 256 MB.
    + Maximum number of messages a single node can send: 1000.
    + Maximum total size of messages a single node can send: 8 MB.
    + 1 ≤ GetNumberOfStones().
    + 1 ≤ GetJumpLength(location) ≤ 109 for any location in + 0 ≤ location ≤ GetNumberOfStones(). +

    + +

    Small dataset

    +

    + Number of nodes: 10.
    + 1 ≤ GetNumberOfStones() ≤ 107.
    +

    + +

    Large dataset

    +

    + Number of nodes: 100.
    + 1 ≤ GetNumberOfStones() ≤ 109.
    +

    diff --git a/distributed_codejam/2018_r1/towels/analysis.html b/distributed_codejam/2018_r1/towels/analysis.html new file mode 100644 index 00000000..a06d2fa5 --- /dev/null +++ b/distributed_codejam/2018_r1/towels/analysis.html @@ -0,0 +1,70 @@ +

    Towels: Analysis

    +

    + If a single-node solution were allowed, this would be a relatively simple simulation question - + we would keep the cleanliness of top towels of each stack in a priority queue, so that the + cleanest towel is at the top, and simulate the process of people entering the gym and picking + towels. Ah, but here even the Small dataset required a distributed solution! +

    +

    + One key observation is to notice that if towel A is immediately below towel B on + some stack, and A is cleaner than B, then after B is picked up, the next + person is guaranteed to pick A - since B was the cleanest towel for the first + of the two people, A will be the cleanest towel for the second of them. This extends + further on - all the towels below B that are cleaner than B will be taken + immediately after B. We can join all such towels in a "towel group" represented + by B (since all these towels will be taken together). +

    +

    + After we do this, note that the leading towels of towel groups on any stack are sorted by + cleanliness (since for each towel we merged all the cleaner towels immediately below it into the + same group, so the towel that's left has to be dirtier), and the gym users will pick towel groups + in order of increasing dirtiness. +

    +

    + Now, if we decide which towel is the dirtiest towel D that will be picked, we will be able to + almost fully simulate the process on each stack - we will never pick any towel dirtier than D, + we will pick the whole group below any towel cleaner than D, and the only unknown is how + many towels from the group headed by D we will pick. This suggests a binary search + approach to find the dirtiest towel we will pick up. There are three different ways to proceed from + this point. In each of them, we divide the stacks roughly equally between the nodes, with each node + responsible for some of the stacks. Once we know D, and how many towels are picked up + before D gets picked, we can send this information to all the nodes, and the node that is + responsible for D can iterate over the last stack, figure out the towel we will pick, and + output the result. +

    +

    + The first approach is to simply run a binary search to find D, coordinated by a master + node. Within each step of the binary search, on each node, we will simulate on each node picking towels up to + dirtiness D, and report back how many towels we picked up (possibly a range, if D + happens to be within our node). Then, the master node sums up how many towels were picked up in + total - if it's more than the desired number, we need a cleaner D, and if it's too few, we need + a dirtier D. With N = NumberOfStacks * NumberOfTowelsInStack and + M = NumberOfNodes, we will need O(N / M) operations in each step, and + O(log(N)) steps. There is also one technical hurdle - with around 30 rounds of + communication, and 100 messages sent from the master in each round, we will run over the limit + on the number of messages sent from the master. To fix this, we can use a tiered strategy (the + master sends the request for calculation to, say, 10 children, and each of those sends to 9 + children of its own). This approach takes O((N log N) / M) time, and + O(log N) rounds of communication, which can run in time, although some care is needed. +

    +

    + An alternative approach to distributing binary search is to trade communication for computation. + The idea is that instead of the master sending one value at a time to children, it will send, + say, 1000 values, and the child will return the number of towels picked up up to that dirtiness + value for each of those 1000 values. This means we will be done in three iterations, which solves + the problem of too many messages. Within each node, we can either run a full simulation, noting + when we reach one of the interesting values, or we can define the groups, sort all of them, and + then do a binary search to answer each query in O(log N/M) time. Both these approaches are + considerably faster than the first one. +

    +

    + Finally, we can try to have the best of all worlds. If, in an iteration of binary search, we learn + that we should go lower (that is, choose fewer towels), we will never need to process half of the + towels again. If we learn we should go higher, we can go over the first half of the towels, throw + them out, and also never process them again. In both cases, for the Large dataset, we need to keep + the list of stacks in a list, not an array, so we can avoid processing stacks that have no + interesting towels in them. This way, the size of the set to consider is halved in each iteration, + and so the total computation time on each node will sum up to O(N/M), and not O(N + log N / M). This approach is also compatible with the 1000-ary search (although the gain + is smaller there). +

    diff --git a/distributed_codejam/2018_r1/towels/statement.html b/distributed_codejam/2018_r1/towels/statement.html new file mode 100644 index 00000000..add5c7b7 --- /dev/null +++ b/distributed_codejam/2018_r1/towels/statement.html @@ -0,0 +1,93 @@ +

    Problem

    + +

    Towels

    +

    + At the start of each day, your local gym puts out P stacks of towels. + Each stack has K towels in it. Towels are of differing cleanliness - + each towel has a unique cleanliness rank assigned, which is an integer + from 1 to P × K, with 1 being the cleanest. +

    + A total of P × K people use the gym each day, and they + arrive one at a time. Each person looks at the towels on top of all the + non-empty stacks, and then takes the cleanest one of those towels. +

    +

    + As a longtime user of this gym who pays a lot of attention to towel hygiene, + you know the cleanliness rank of every towel in every pile. You are planning + to visit the gym today, and you know exactly how many people will pick a + towel before you do. When you arrive at the gym, you will pick the cleanest + towel among the top towels in non-empty stacks, in the same way that other + gym users do. What is the cleanliness rank of the towel you will get? +

    + +

    Input

    +

    + The input library is called "towels"; see the sample inputs below for + examples in your language. It defines four methods: +

    +
      +
    • GetNumberOfStacks(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of stacks of towels.
      • +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    • GetNumberOfTowelsInStack(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of towels in each stack.
      • +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    • GetNumberOfGymUsersBeforeYou(): +
        +
      • Takes no argument.
      • +
      • Returns a 64-bit integer: the number of gym users who pick a towel + before you do.
      • +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    • GetTowelCleanlinessRank(stack, position): +
        +
      • Takes two 64-bit integers, stack (the index of the stack) and position + (the position of the towel in the stack, counting from the bottom), in + the ranges + 0 ≤ stack < GetNumberOfStacks() and + 0 ≤ position < GetNumberOfTowelsInStack().
      • +
      • Returns a 64-bit integer: the cleanliness rank of the towel.
      • +
      • Expect each call to take 0.05 microseconds.
      • +
      +
    • +
    + +

    Output

    +

    + Output a single integer: the cleanliness rank of the towel you will pick. +

    + +

    Limits

    +

    + Time limit: 10 seconds.
    + Number of nodes: 100 (for the Small dataset, too!)
    + Memory limit per node: 1.5 GB.
    + Maximum number of messages a single node can send: 1000.
    + Maximum total size of messages a single node can send: 8 MB.
    + 1 ≤ GetTowelCleanlinessRank(stack, position) ≤ GetNumberOfStacks() + × GetNumberOfTowelsInStack().
    + GetTowelCleanlinessRank(stack, position) is different for each + valid (stack, position) pair.
    + 1 ≤ GetNumberOfTowelsInStack() ≤ 107.
    + 0 ≤ GetNumberOfGymUsersBeforeYou() < GetNumberOfStacks() × + GetNumberOfTowelsInStack().
    +

    + +

    Small dataset

    +

    + 1 ≤ GetNumberOfStacks() ≤ 100.
    +

    + +

    Large dataset

    +

    + 1 ≤ GetNumberOfStacks() × GetNumberOfTowelsInStack() ≤ 109.
    +