We're now waiting for the contest to start. Once the contest starts,
the problems will automatically load, and Problem A will display here.
You don't have to solve that problem first; you will be able to select
another problem from the list to the left. Once you've solved one
problem, don't forget to work on the others!
Overview | Problem A | Problem B | Problem C
The last Round 1 again had an attendance in the vicinity of 5000 — pretty impressive, given that 2000 people already advanced. Our contestants were faced with a substring-counting problem Consonants, where to solve the large you had to deal with million-character names; followed by the tricky puzzle of Pogo and an implementation-intense problem called Great Wall.
The large input size in consonants caused the solutions of a large number of contestants to time out — quadratic solutions weren't cutting it, after all, and nearly 60% of contestants who attempted the large failed. The large of pogo was even more tricky — while for the small you could do some variation around exhaustive search, for the large you needed a few observations to come up with a greedy solution. Only 19% of contestants who attacked this problem succeeded. Finally, the Great Wall had a rather complex input that scared many people off, the first submission for the small came over half an hour into the contest; and the large required some variation on the theme of interval trees.
The competition was off to a blazing start, with xiaowuc1 solving the small of Consonants in an astounding time below two minutes! The other problems proved more problematic, however. In the end, it turned out that solving any one problem by itself was not enough to advance. The most popular way to get a spot in Round 2 was to deal with Consonants large and both remaining smalls, followed by dropping the Great Wall small in favor of doing the other problems really fast.
Congratulations to everybody who got through to Round 2, and we hope you enjoyed all the Round 1 problems!
Cast
Problem A. Consonants written by Khaled Hafez and Petr Mitrichev. Prepared by Onufry Wojtaszczyk and Hackson Leung.
Problem B. Pogo written by David Arthur. Prepared by Onufry Wojtaszczyk and Ahmed Aly.
Problem C. The Great Wall written by Onufry Wojtaszczyk. Prepared by Adrian Kuegel and Steve Thomas.
Contest analysis presented by Onufry Wojtaszczyk, John Dethridge and Hackson Leung. Solutions and other problem preparation by Igor Naverniouk, Jan Kuipers, Tomek Czajka and Tomek Kulczynski.
Solving the small
It cannot be simpler than trying each possible substring given the name. For a given substring, we just check if there exists n consecutive consonants. If it is true, we count this substring into part of the n-value. There are O(L2) substrings, and it takes O(L) time to check for at least n consecutive consonants. In total each case takes O(L3) time to solve, which is acceptable to solve the small input. This approach is, of course, not fast enough to solve the large input.
Improving the naive algorithm
In fact we can skip the linear time checking for all possible substrings. Here we assume the index is zero based. Suppose we start from the i-th character. We also have c that starts as zero. When we iterate up to the j-th character, if it is a consonant, we increase c by 1, otherwise reset it to zero. Actually c is the number of consecutive consonants that starts after the i-th character and ends at the j-th character. If we meet the first instance such that c ≥ n, we can conclude that every substring which starts at the i-th character and ends at the k-th character, where k ≥ j, is the desired substring. Then we know that we can add L - j to the answer, and proceed to the next starting character. This algorithm runs in O(L2) time, which is still not sufficient in solving the large input. But the concept of computing c is the key to solve the problem completely.
Further improving
Let us extend the definition of c to every character, call it ci: the number of consecutive consonants that ends at the i-th character. For example, suppose the string is quartz, then c0 = 1, c2 = 0, and c5 = 3. We can use similar approach mentioned in the last section to compute every ci in O(L) time. Also define a pair (x, y) to be the substring that starts at the x-th character and ends at the y-th character.
Knowing from the previous section, if we know that ci ≥ n, then we know that substrings (i - ci + p, i + q), where 1 ≤ p ≤ ci - n + 1 and 0 ≤ q ≤ L - i - 1, are the desired substrings. It implies that there are (ci - n + 1) × (L - i) substrings. If you proceed like this, you missed some substrings. Consider the string axb with n = 1. We see that c1 = 1 but we only count 2 substrings, namely x and xb. We miss the prefix options, namely ax and axb. It looks like we can consider the substrings (p, i + q), where 0 ≤ p ≤ i - n + 1 and 0 ≤ q ≤ L - i - 1. Unfortunately, in this case we may count certain substrings multiple times. Consider the string xaxb with n = 1, where we count xax and xaxb twice since c0 = c2 = 1.
To correctly count the substrings, we need to choose the appropriate range of p. In fact, we just need one more value: the last j < i such that cj ≥ n. Let r = j - n + 2 if there is such j, or r = 0 otherwise. Then we have the right set of substrings (p, i + q), where r ≤ p ≤ i - n + 1 and 0 ≤ q ≤ L - i - 1. In fact, r means the longest possible prefix so that (r, i - n) contains at most n - 1 consecutive consonants and therefore we avoid repeated counting. Hence for each ci ≥ n we count (i - n - r + 2) × (L - i). Summing up we have the answer. r is updated whenever we see that ci ≥ n before iterating the next position. Therefore it takes constant time to update the value. Overall the running time is O(L), which is enough to solve the large input.
Despite the complications, the algorithm is extremely simple. The following is a sample solution:
def Solve(s, n):
L = len(s)
cnt, r, c = 0, 0, 0
for i in range(L):
c = c + 1 if s[i] not in "aeiou" else 0
if c >= n:
cnt += (i - n - r + 2) * (L - i)
r = i - n + 2
return cnt
The small dataset
The small dataset did not require finding the optimal solution, instead accepting any solution that solved the problem within 500 moves. This allowed for a variety of approaches. One such approach is as follows: note that with two subsequent jumps in two opposite directions you can move one unit in a chosen direction. By a sequence of at most 200 such pairs (so in total at most 400 moves) you can reach any point with |X|, |Y| ≤ 100.
There were also other approaches possible, for instance a shortest-path search, if one could prove or guess that it's OK to limit the search space somehow. It turns out that one can actually just search for a path within the points with |x|, |y| ≤ 100, we will see why in the next section. Thus, Dijkstra's algorithm or just breadth-first search will provide a short enough path to the target.
The large dataset
For the large dataset, we not only need to return the best possible solution, but also deal with more distant targets, so neither of the approaches above will work. We will begin with a few easy observations:
- First, if we want to reach the target in N moves, we have to have 1 + 2 + ... + N ≥ |X| + |Y|.
- Moreover, if we want to reach the target in N moves, the parity of the numbers 1 + 2 + ... + N and |X| + |Y| has to be the same. This is because the parity of the sum of the lengths of jumps we make in the North-South direction has to match the parity of |Y|, and the sum of lengths of West-East jumps has to match the parity of |X|.
It turns out that if
N satisfies these two conditions, it is possible to reach (X, Y) with
N jumps.
Let's consider a point (X, Y), and any N satisfying the two conditions above. For the sake of brevity, assume |X| ≥ |Y|, and X ≥ 0 (it's easy to provide symmetric arguments for the other four cases). In this case, we will assume the last move was East. This means the first N-1 moves have to reach (X-N, Y). We will proceed recursively, so we just have to prove that (X-N, Y) and N-1 satisfy the conditions above.
For N = 1 or 2, it's easy to enumerate all the possible X and Y. For N = 1, there are four possibilities, and in each case our strategy produces the correct move. For N = 2, if we assume X is positive and greater than |Y|, the only possibilities are (3, 0), (2, 1), (2, -1) and (1, 0); after the move they turn to (1, 0), (0, 1), (0, -1) and (-1, 0) — all of which satify the conditions for N = 1.
For larger N, the parity condition is trivial — both considered parities stay the same if N was even, and both change if N is odd. For the inequality condition, if N ≤ X, both sides just decrease by N, so the only interesting case is if X < N. However, in this case, we move to (X - N, Y), and the sum of absolute values is N - X + |Y| ≤ N - X + X = N ≤ 1 + ... + N - 1. Thus, the conditions are satisfied after one move, and we can continue with our strategy.
This logic again translates to simple code:
def Solve(x, y):
N = 0
sum = 0
while sum < abs(x) + abs(y) or (sum + x + y) % 2 == 1:
N += 1
sum += N
result = ""
while N > 0:
if abs(x) > abs(y):
if x > 0:
result += 'E'
x -= N
else:
result += 'W'
x += N
else:
if y > 0:
result += 'N'
y -= N
else:
result += 'S'
y += N
N -= 1
return result.reversed()
The small input
Despite the very long statement, solving the small input wasn't actually that hard. Still, the long statement scared many contestants off, which is probably why we saw the first submission only after half an hour of the contest, and relatively few submissions to the problem in general. With at most 10 tribes, at most 10 attacks and all the attacks happening on a short section of the Wall, we can just simulate all that happens. Let's look at it a bit more carefully.
Since delta_p is limited by 10, a tribe attacks at most 10 times, and the initial attack is between -100 and 100, all the attacks will occur between -200 and 200. Thus, we can afford to remember the height of the wall at each interesting point. This brings us to the first trick of this problem — what are the points we should be interested in?
Note that since the edges of attacked areas are always integers, the height of the wall in each open interval (x, x+1) for integral x is always constant. Moreover, the height at the integral points is never lower than at any of the two neighboring open intervals, since any attack that affects any of these intervals will also affect the integral point next to it. As wi < ei, any attack always affects at least one whole interval, and so the success of the attack depends only on the height of the wall in the intervals, and not on the edges. Thus, it is enough to keep information about the height of the wall in points of the form x + 0.5 for integral x. There are 400 such points to consider in the small input, and the height of each is initially zero.
There are a 100 attacks to consider. We can begin by generating all of them explicitly (noting the beginning and end point, day and strength for each of them), and sorting them by time of occurrence. For each day on which at least one attack occurs, we first check for each attack whether it succeeds (by examining the wall height at each attacked interval). Afterwards, for all attacks we go over all affected intervals and increase the height of the wall if necessary. Note that it is important to increase the wall height only after checking all the attacks that occur on a given day.
The large input
The numbers are much bigger for the large input. We can have 106 attacks, and they can range over an interval of length over 108. Let's analyse which parts of the previous approach will work, and which will not.
We can still generate all the attacks explicitly, and sort them by time. We probably need a more concise way to represent the Wall, though, and we surely need a faster way to check whether an attack succeeds and updating wall heights.
The problem of concise representation can be solved by noticing that since we have only 106 attacks, we will have around 106 interesting points. A sample way to take advantage of this it to "compress" all attack coordinates — sort all the coordinates that are beginnings or ends of attacks, and consider as interesting only the points in the middles of intervals of adjacent endpoints. We will end up with at most 2 x 106 points, and each will represent an interval such that the height of the wall on this interval is always the same. Using this tric to compress the attack coordinates, we can assume all attacks happen in a space of at most 2 x 106 points. We can rename these points to be consecutive for convenience.
To attack the problem of checking attack success and updating the wall, we will need some variant of an interval tree. We will present two interval-tree based approaches below.
An interval tree is a tree, in which each node represents an interval [m x 2k, m x 2k] for some m,k. The parent of a node containing an interval I will be the node representing a twice longer interval containing I (so, if I is [m x 2k, m x 2k], the parent is [(m / 2) x 2k+1, (m/2 + 1) x 2k+1]). This is the common pattern for interval trees, the trick is in what to store in nodes.
High and low
In the first approach, we will try to answer the questions directly by the means of using a modified interval tree. We will store two values in each node — hi and lo. The "hi" value will be pretty standard, and will be defined so that the height of the wall at any given point is the maximum "hi" value of all the intervals containing this point. This can be updated in logarithmic time when any interval of the wall is attacked - we can split any interval into a logarithmic number of intervals represented by nodes, and update the hi value in each of them. This will allow us to update the wall height, and to figure out what the height of the wall at a given point is, each in logarithmic time. We still need a way to figure out whether an attack will succeed in logarithmic time, though.
We will use the "lo" values for that. For a given node X and a path to a leaf from X we can define the maximum "hi" value on this path as the "partial height" of the leaf node. This is what would be the height, if we disregarded all the nodes above X (in particular, "partial heights" measured from the root node are simply wall heights). We now define the "lo" value of X as the smallest partial height of a descendant of X. We need to see how this is useful, and how to update it in logarithmic time when updating the "hi" values.
Note that if we have a "lo" value for a node calculated, we can easily figure out the height of the lowest wall point in this interval - it's the maximum of the "lo" value of this node and the "hi" values of all the ancestors of this node. Thus, to figure out whether an attack will succeed on a general interval we split it into a logarithmic number of intervals represented by nodes, and figure out the lowest wall segment in each of these sub-intervals. If any of these is lower than the strength of the attack, it will succeed. This is logarithmic-squared as described, but it's easy to implement it to actually be logarithmic.
Now note that the "lo" values have a simple recursive definition - take the minimum of the "lo" values of the children, or the "hi" value of the node itself, whichever is higher. This means that when updating the "hi" value for a node, we only need to update the "lo" values for this node and its ancestors - meaning we can update "lo" values in logarithmic-squared time for each attack (and, again, it's simple to update them in logarithmic time).
Order by strength
Another approach that allows us to solve this problem with an interval tree is to order the attacks by strength, descending, and not by chronology. In this approach, for each point we know what was the earliest time at which it was attacked. Note that since we process from the strongest attack, any section of the wall that was attacked earlier by an attack we already processed is immune to attacks that come later and are processed later. Thus, to learn whether the attack is successful we need to find what's the latest attack time in the whole interval this attack covers; and subsequently we need to update the attack times to the minimum of the time that was stored so far, and the time of the currently processed attack.
This is called a min-max interval tree (we update with the minimum, and we query for the maximum). We encourage you to figure out what to store in the nodes to make this work in logarithmic time!
Category | |
Asked | |
Question | |
Answered | |
Answer |
The Great Wall Announcement | | 50:35 | | For Tribe 1 on the second sample case, it seems that n1==1.
Then, why is there second attack of Trive 1 for [8,13] at height 10 on day 3? | | 1:14:36 | | n1 in the second test case should be 2, and the statement is updated so the details for the first Tribe is "1 2 0 5 10 2 8 0" instead of "1 1 0 5 10 2 8 0". The statement is updated, sorry. |
You cannot ask questions at this time. Please email us at codejam@google.com.