2018 ICPC World Finals
May 2, 2018, 12:40 pm
The ICPC World Finals in Beijing just wrapped up, and I had the great
privilege of representing the University of Texas with my teammates, Arnav
Sastry and Daniel Talamas. This was the first time any of us competed at the
World Finals, and it was an amazing experience.
My team didn't perform as well as we'd hoped, but we didn't do
embarrassingly poorly either. We ended up solving four problems (out of eleven)
and placing 65th (of 140). Here, I'll write a brief summary of our experience
in the competition.
Getting started
When the countdown finished, Arnav logged into the computer and handed me
the problem packet. While Arnav was writing our .vimrc, .bashrc, and template,
I started reading the problems front-to-back while Daniel read back-to-front. I
told Arnav that B looked like an implementation problem, and, being the fastest
typist on our team, he took a look and started coding.
While Daniel and I continued reading problems, other teams got accepted
verdicts on three problems: F, B, and K. I diverted my attention to K.
First AC
While Arnav coded B, I came up with a greedy constructive algorithm for K.
Soon I convinced myself of its correctness, and as soon as Arnav submitted B I
took the keyboard to code K. Arnav's solution to B got accepted, and we
celebrated our quick AC. The solution to K was relatively simple to implement,
and it was finished in just a few minutes. Passing samples, I asked Daniel for
a sanity check on my idea and submitted K.
Seventh place
Arnav got back in the hot seat and began coding a solution to G using Java's
AWT library. Geometry has historically been our weak point, but we know how to
do basic things like polygon union and intersection using this library. In the
past we have gotten some tricky geometry problems with this trick, so with
nothing else to code we decided it was worth a shot. We didn't have high hopes,
because at this point no other team had gotten G.
In the meantime, our K solution got accepted, and we rose to 7th place on
the leaderboard.
Arnav finished our solution to G, and running on samples it was nowhere near
accurate enough. The sample output was 50, and our code gave 49.997. With no
ideas for a real solution, we gave up on G for the time being.
DP on edges
At this point, Daniel figured out the solution to A: a DP on edges. He
explained it to me, and I started to code, but he preempted me and coded it
himself. Unfortuately, the code failed samples, and it took us a very long time
to find the bug: in our DFS, we forgot to update the memoization table.
While Daniel tried to find his bug, Arnav worked on F (the apparent next
easiest problem) and I worked on C. I mistakenly thought that some team had
already gotten AC on C, and I had an idea that I thought could work. As I
wrote pseudocode, it became clear that my idea had a big flaw, and Daniel
pointed out that no one else had C, so I abandoned it.
Arnav was unsure if his solution to F would run in time. He coded it anyway,
and it gave wrong answer. Daniel found his bug in A and submitted, getting
accepted.
Time limit squeezing
We knew that if we could get F, H and I were our next best bets.
Unfortunately, even after finding an edge case and fixing a bug in F, we got
TLE. After some more optimizations, we got another TLE; after one more, WA.
Finally, we fixed that bug and got AC, but with a massive penalty for wrong
submissions. We found out after the contest that some teams wrote a solution
without the optimizations we included, but broke early when nearing the time
limit, and these solutions got AC as well. I'm very impressed by the teams that
got F on their first attempt!
H is for hard
H was another geometry problem: find the minimum number of line segments
you need to use to cut every wire going across a rectangular door (also given
as line segments). Arnav and I realized quickly that you can always do it in
two cuts, so all that is needed is to check if it is possible in one. Thinking
what remained would be easy, we focused our attention on H rather than I, the
other problem that may have been within reach.
This turned out to be a mistake. We spent the last two hours trying to
get H, with only a few wrong attempts and wrong conjectures. In hindsight, our
strengths were much more in favor of I. Nonetheless, while Arnav and I coded H,
Daniel thought about I.
The hard part of H (like many geometry problems) is finding the right WLOGs:
there were up to \(10^6\) line segments, so the obvious \(O(N^2)\) sweep line
approach doesn't work. We needed some way to reduce the runtime.
We guessed that our cut should always link two opposing sides, and we tried
an approach that produced four candidate line segments and then checked all of
them. Although it passed every case we could think of, it gave WA when we
submitted.
The worst part: we still don't know how to solve it. We figured out I on the
plane ride back to the States, but H still eludes us. I fear that I will be
haunted by this problem forever.
Wrapping up
We spent the last 10 minutes speed-coding a solution for H based on another
(incorrect) conjecture, but we couldn't finish in time and so we didn't get to
submit it.
We were disappointed to have only solved four problems, but we had a great
time at the contest. The problems were generally interesting, and I hope to
return next year, wherever it ends up being!
K revisited
My favorite problem of the set was K (probably because I solved it). You are
given a graph with \(N\) vertices and \(M\) edges, and you want to construct a
tree with \(N\) vertices that minimizes the number of nodes whose degrees
differ from their respective degrees in the original graph.
The obvious greedy thing to do is write down the degrees of each node, and,
while the sum of the degrees is greater than \(2N - 2\), cut the largest degree
down to \(1\) (or to some other value if changing it to \(1\) would remove too
many edges).
The question is: given our new degree sequence, can we construct a tree with
these degrees?
My approach was to sort the nodes again by their new degrees, and build a
rooted tree. We keep a stack of nodes who don't have parents, and for a node
\(u\) with degree \(d\) we pop \(d - 1\) nodes from the stack and assign them
parent \(u\). We must special case the root.
It was not immediately obvious that this process never gets stuck (tries to
pop from an empty stack). While Arnav coded B, I came up with a nifty proof.
Let \(D\) be the degree sequence. If we are trying to add node \(k + 1\), the
number of nodes currently in our active stack is \(k - \sum_{i=1}^{k} (D[i] -
1)\). Simplifying, this is \(2k - \sum_{i=1}^{k} D[i]\).
We want to show that this is always at least \(D[k + 1] - 1\). Rearranging,
we want
\[\sum_{i=1}^{k+1} D[i] \leq 2k + 1\]
for all \(k\).
We can't prove this using standard induction, because when we are partway
through the process we don't know anything about the future degrees. In
fact, the only restriction on the future degrees is that the sum of all degrees
is \(2N - 2\).
But then the claim is obvious for \(k = N - 1\): \(\sum_{i=1}^{N} D[i] =
2N - 2 \leq 2N - 1\). The off-by-one here is because we want a slightly
stronger claim for the last node (because the root does not get a parent). But
we satisfy this stronger claim too.
For \(k = 0\), the claim is obvious as well: the first degree must be \(1\),
since the degrees are sorted and if all degrees are at least \(2\) the sum of
degrees must be at least \(2N > 2N - 2\).
So the partial sum of degrees is under the linear function at the beginning
and at the end. How can we show that it is under the linear function along its
entire domain?
Since we ordered the degrees in increasing order, the partial sum of degrees
as a function of \(k\) is convex! A convex function always stays under the line
connecting two points on its graph. This completes the proof that the
construction works for any degree sequence with the right sum, including the
greedy sequence we found earlier.
The other problems of the set were also interesting, but this was the one
from which I learned the most.
It's been a while since I posted on this blog, so hopefully I start doing
that more in the coming months. I probably won't, but I will try. Thank you for
reading!
Trees on Graphs
April 4, 2017, 11:02 pm
A couple of weeks ago, I was solving a Codeforces problem and I came across
a trick I hadn't seen before. Here, I'll give that problem, describe the
solution, and then show how to apply it to a more complicated problem.
Codeforces 786B: Legacy
In this problem,
we are asked to compute the single source shortest paths in a weighted directed
graph, with all the weights nonnegative. The catch: there are three types of
edges. There are normal edges of the form \((u, v)\), but there are also edges
that look like \((u, [l, r])\) and \(([l, r], u)\). Here, \([l, r]\) refers to
all the nodes whose index is between \(l\) and \(r\), inclusive. There are
up to \(10^5\) nodes and edges.
The naive solution is to convert all edges of the form \((u, [l, r])\) to
\(r - l + 1\) edges of the form \((u, i)\) (and to do the same for all edges of
the form \(([l, r], u)\)). Of course, there are \(O(N \cdot M)\) edges in this
transformed graph (where \(N\) deontes the number of nodes in the original
graph, and \(M\) is the number of edges), so this solution will MLE or TLE.
Let's digress for a minute and consider a different problem: given an array
of numbers and \(Q\) queries, each of the form \([l, r]\), output the minimum
value in the subarray defined by each query. There are multiple solutions to
this problem, but perhaps the most common is to use a segment tree: we build a
binary tree on top of the array and decompose queries into \(O(\log N)\)
disjoint intervals (represented by nodes in the tree) whose union is the
desired query.
For example, say the array is \([5, 3, 5, 10, 7, 2, 8, 14]\), and our query
is \([3, 6]\) (we are using \(1\)-indexing). This query can be represented as
the union of \([3, 4]\) and \([5, 6]\), two nodes present in our segment
tree.
The two red nodes represent the intervals \([3, 4]\) and \([5, 6]\), so if
we identify them in \(O(\log N)\) time we can then find the answer to our
original query in \(O(\log N)\) time by taking the minimum of the values in
those nodes. (Exercise: prove that all intervals are the disjoint union of
\(O(\log N)\) nodes, and code a segment tree.)
Now, back to our shortest paths problem. We will use the same idea:
decompose an interval \([l, r]\) into \(O(\log N)\) disjoint smaller intervals
on which we can do some preprocessing.
In this case, we will need two trees: one with edges directed down the tree
and one with edges directed up the tree. All these edges will have weight
zero.
Now, if we want to add an edge from some node to the interval \([3, 6]\), we
can instead add an edge to each of the two green nodes in the first tree.
Similarly, if we want to add an edge from the interval \([3, 6]\), we can
instead add an edge from each of the two blue nodes in the second tree.
We've added \(O(M \log N)\) edges to our graph, and running Dijkstra's
algorithm on the new graph easily runs in time.
AtCoder RC069F: Flags
This problem is from a
recent AtCoder contest. I
didn't get to do this contest live, but I found the problems really enjoyable.
Flags was the
hardest problem, largely because it combines a few different techniques.
We want to place \(N\) flags in a line while maximizing the distance
between the closest two flags. Each flag has two positions in which it is
allowed to be placed, and \(N\) can be up to \(10^4\).
We will binary search on the answer; denote our current guess \(d\).
Checking whether it is possible to place all the flags with at least distance
\(d\) between every pair is a 2-SAT problem.
A common technique for solving a 2-SAT problem like this is to build the
"implication graph": a directed graph in which the nodes represent boolean
variables and their negations, and edges represent logical conditionals. So an
edge exists between two nodes \(u\) and \(v\) if \(u\) "implies" \(v\), and if
we assign \(u\) the truth value "true", then \(v\) must also be marked "true".
Then the 2-SAT problem is satisfiable if it is not the case that some node
directly or indirectly implies its negation, and its negation directly or
indirectly implies the node itself. We can easily check this condition by
finding the strongly connected components of the implication graph.
So let's build the implication graph. Each flag has two nodes, one for each
of its possible locations. We will assign each of these nodes a truth value,
and we require that, for a solution to be valid, the two nodes associated with
each must be assigned opposite truth values. That is, each flag must be placed
in exactly one of its two positions. For convenience, we will say that if node
\(u\) represents placing a particular flag in its first location, then \(\neg
u\) refers to the node representing placing that flag in its second location
(and vice versa).
We will add an edge from \(u\) to \(\neg v\) if the distance between the
posistions associated with \(u\) and \(v\) is less than \(d\). This edge
indicates that if we assign \(u\) "true", then all the potential positions that
are nearby are disqualified: for all of those flags, we must take the other
option. Implementing this naively will yield a time complexity of \(O(N^2
\log X)\), where \(X\) is the size of the space over which we're binary
searching (probably \([0, 10^9]\)). This is too slow.
However, if we sort all our nodes by position (breaking ties arbitrarily),
then we can binary search before and after each node \(u\) to get two intervals
of nodes. We want to add an edge from \(u\) to the negation of every node in
those intervals.
That we're adding edges to intervals is a big hint to use the tree-on-graph
technique we used in the previous problem. If we can use that trick here, we
will reduce our number of edges from \(O(N^2)\) to \(O(N \log N)\). And in
fact, the trick works the same way here, with an extra hiccup. Our tree's
leaves shouldn't connect directly to the nodes they represent, but rather to
their negations. The implementation is still straightforward, but this shows
that we can use the tree-on-graph trick even when the ordering of nodes we use
to build the tree is different than the ordering we use to decide what
intervals to query when we add additional edges.
Consider the following sample case.
4
1 3
2 5
1 9
3 8
There are four flags. The first one can be placed at position \(1\) or
\(3\), the second can be placed at \(2\) or \(5\), the third can be placed at
\(1\) or \(9\), and the fourth can be placed at \(3\) or \(8\).
First, we will create our 2-SAT nodes. \(s\) will be the node representing
placing the first flag at position \(1\), and \(\neg s\) will be placing the
first flag at position \(3\). The same pattern will be used for \(t\), \(\neg
t\), \(u\), \(\neg u\), \(v\), and \(\neg v\).
Below are the nodes in their natural order.
Next, we will build our tree so that we can add edges to intervals. Note
that the leaves of the tree are the negations of the nodes in the natural
order above.
Now, if we want to add an edge to the negations of the node in the interval
\([3, 6]\), we can just add an edge to each of the nodes marked in cyan.
This completes the solution to the problem. There are \(O(N \log N)\) edges
in our new graph, and we can build the implication graph, find its strongly
connected components, and ensure that no node is in the same SCC as its
negation.
In this problem, the "trees on graphs" technique was only a small part of
the whole solution, but for me it was the hardest part to see. In general,
this technique can be used whenever we have a graph where some edges are to or
from intervals of the nodes (under some ordering). The only caveat is that we
must ensure that we preserve the properties of the graph we need for our
solution. In the first problem, we needed to preserve path costs so that
Dijkstra's algorithm would work. In the second problem, we needed to preserve
the implication relationships between nodes: if (and only if) node \(u\) is
reachable from node \(v\) in the original graph, \(u\) should be reachable from
\(v\) in the transformed graph.
Keeping that in mind, I'm sure this can be applied to many other problems. I
look forward to solving more of them in the future!
Codeforces Educational Round 18
March 29, 2017, 7:28 am
I haven't participated in many Codeforces rounds lately, but I did do
Educational Round 18.
Codeforces hosts "Educational Rounds" periodically, which are unrated and
usually focus on teaching common techniques and tricks.
Problem C was a simple greedy algorithm with lots of edge cases, and problem
D was essentially implementation. Problem E was interesting, but unfortunately
I couldn't solve it during the contest. Nonetheless, here are my thoughts on
it.
E: Colored Balls
In this problem, we have \(N \leq 500\) colors of balls (with \(x_i \leq
10^9\) balls of color \(i\)), and we want to partition the balls into
monochromatic sets such that no two sets differ in size by more than \(1\). Our
goal is to minimize the number of sets.
First, it is clear that this is the same as maximizing the number \(k\) such
that every set is of size \(k\) or \(k + 1\): after finding this number, we can
find the minimum number of sets for a particular color \(i\) by taking
\(\lfloor \frac{x_i}{k + 1} \rfloor\) and then adding \(1\) if the remainder is
nonzero. (To see why, consider a variant of the division algorithm: \(x_i =
q(k + 1) - r\), with \(0 \leq r \leq k\). The above procedure gives us \(q\) in
this formulation, and that corresponds to taking \(q - r\) sets of size \(k +
1\) and \(r\) sets of size \(k\).)
Each \(x_i\) has some number of potential valid values of \(k\), and we want
to find the maximum \(k\) that is valid for all \(x_i\). Our approach will be
as follows: generate the whole set of valid \(k\) values for \(x_1\), and then
cull this set for each of \(x_2, x_3, \dots\).
There are a few parts to this solution. First, let's try to characterize the
valid values of \(k\) for a particular \(x_i\). We want to know for which
values of \(k\) there exist nonnegative integers \(a, b\) such that \(x_i =
a(k) + b(k + 1)\). Since \(k\) and \(k + 1\) are always coprime, there are
infinitely many (potentially negative) integers \(a\) and \(b\) satisfying this
equation. If we can find one solution, then we can characterize all the
solutions. The obvious solution is to take \(a = -x_i\) and \(b = x_i\), so all
the solutions are of the form \(a = -x_i + (k + 1)z\) and \(b = x_i - kz\), for
\(z \in \mathbb{Z}\). If there are any solutions with both \(a \geq 0\) and \(b
\geq 0\), then one such solution will be with \(z = \lfloor \frac{x_i}{k}
\rfloor\). \(b\) will always be nonnegative with this value of \(z\), so we
only need to check if \(a \geq 0\) also: \(a = -x_i + (k + 1)\lfloor
\frac{x_i}{k} \rfloor \geq 0\), which is the case when \(\lfloor \frac{x_i}{k}
\rfloor \geq x_i \bmod k\) (where \(\bmod\) is taken to mean remainder, as in
most programming languages).
This gives an easy way to check if a given value of \(k\) will work for a
particular \(x_i\), and so we can use this in the culling step in our
algorithm. However, this doesn't help us generate all the valid values for
\(x_1\). It's not even clear that there aren't too many values to store in a
set!
It turns out that there are never more than \(3\sqrt{x_i}\) values of \(k\)
for any \(x_i\). To prove this, we will just describe the procedure to
enumerate them; this will also complete our algorithm.
First, note that for \(k \leq \sqrt{x_i}\), the condition (\(\lfloor
\frac{x_i}{k} \rfloor \geq x_i \bmod k\)) always holds. This gives us
\(\sqrt{x_i}\) values. For the others, we iterate over all possible values of
the left-hand side of the inequality (there are only \(\sqrt{x_i}\) to
consider), and for each, we identify at most \(2\) values of \(k\) that are
valid.
We will denote our guess for the left-hand side of the inequality by \(y\).
We will show that only \(\lfloor \frac{x_i}{y} \rfloor\) and \(\lfloor
\frac{x_i}{y} \rfloor - 1\) may be valid values of \(k\). It will be easy to
check each of these using our condition and add them to our set if they are
indeed valid.
So we want to show that only \(k = \lfloor \frac{x_i}{y} \rfloor\) and \(k =
\lfloor \frac{x_i}{y} \rfloor - 1\) may satisfy both \(\lfloor \frac{x_i}{k}
\rfloor = y\) and \(x_i \bmod k \leq y\). We will show at least one condition
must fail whenever \(k > \lfloor \frac{x_i}{y} \rfloor\) or \(k < \lfloor
\frac{x_i}{y} \rfloor - 1\).
First, we handle the \(k > \lfloor \frac{x_i}{y} \rfloor\) case. In this
case, the first condition (\(\lfloor \frac{x_i}{k} \rfloor = y\)) cannot hold;
in fact, \(\lfloor \frac{x_i}{k} \rfloor < y\). It is enough to note that \(k >
\frac{x_i}{y}\) whenever \(k > \lfloor \frac{x_i}{y} \rfloor\), since \(k\) is
an integer. Then
\[\left\lfloor \frac{x_i}{k} \right\rfloor \leq \frac{x_i}{k} <
\frac{x_i}{x_i/y} = y.\]
The \(k < \lfloor \frac{x_i}{y} \rfloor - 1 \) case is more subtle. Without
loss of generality, assume the first condition holds (if not, then we are
done). We will prove that \(x_i \bmod k > y\). To do so, we again invoke the
division algorithm. For convenience, let \(m = \lfloor \frac{x_i}{y} \rfloor -
k\).
\begin{align}
x_i &= qk + r & (0 \leq r < k) \\
&= yk + r \\
&= y\left(\left\lfloor \frac{x_i}{y} \right\rfloor - m\right) + r
\end{align}
But since \(y \lfloor \frac{x_i}{y} \rfloor \leq x_i\), rearranging gives
\(r \geq my > y\), as desired.
This completes the proof that there are never more than \(3\sqrt{x_i}\) (in
practice, there are usually close to \(2\sqrt{x_i}\)) valid values of \(k\),
and so we can start by testing all of these for \(x_1\), adding valid ones to a
set, and then iterating over the other \(x_i\) and removing invalid ones. At
the end, we can choose the largest element of our set (the set can never be
empty because \(k = 1\) is always valid) and calculate the corresponding
minimum number of sets as described above.
Codeforces Round #400
February 26, 2017, 6:40 am
Thursday morning, Codeforces hosted the
ICM Technex 2017 round.
Unfortunately, I wasn't able to compete, but I thought two of the problems were
interesting.
D: The Doors Problem
This was a nice 2-SAT style problem. We have a set of \(M\) doors, each of
which is locked or unlocked. We also have \(N\) switches (toggling a switch
toggles the state of all doors connected to that switch). Furthermore, each
door is connected to exactly two switches. We want to know if it is possible to
unlock all the doors by toggling some subset of the switches.
So we create a graph with \(2N\) nodes and \(2M\) edges. Each switch \(i\)
is associated with two nodes: one of these represents toggling switch \(i\)
(denoted by \(u_i\)) and the other represents not toggling switch
\(i\) (denoted by \(v_i\)). Each door is represented by two edges. If the door
is initially unlocked, then we must either toggle both its switches, or toggle
neither. Thus, if the door is connected to switches \(i\) and \(j\), we add
undirected edges \((u_i, u_j)\) and \((v_i, v_j)\). On the other hand, if the
door is initially locked, we must toggle one of its switches but not the other,
so we add edges \((u_i, v_j)\) and \((v_i, u_j)\). Now the nodes of our graph
are partitioned into connected components that must be assigned the same truth
value: if a node \(u_i\) belongs to a component assigned "true", then we must
toggle switch \(i\); if \(u_i\) belongs to a component assigned "false", then
we must not toggle switch \(i\). Similarly, if \(v_i\) belongs to a "true"
component, then we must not toggle switch \(i\); if \(v_i\) belongs to a
"false" component, then we must toggle switch \(i\).
All that remains is to determine whether there is a truth assignment for our
components that is consistent: that is, there does not exist an \(i\) with
\(u_i\) and \(v_i\) receiving the same truth value. This is the case exactly
when, for all \(i\), \(u_i\) and \(v_i\) belong to different components.
This can be implemented by explicitly building the graph and finding the
connected components, or by using a union-find data structure.
E: The Holmes Children
In this problem, we are asked to evaluate a function \(F_k(n)\), where
\(F_k\) is defined as a composition of \(k + 1\) functions, alternating between
\(f\) and \(g\). \(f(n)\) is defined to be the number of ordered coprime pairs
\((x, y)\) where \(x + y = n\), and \(g(n)\) is the summatory function of this
function: \(g(n) = \sum_{d|n} f(d)\).
First, it is not hard to see that \(f\) is Euler's \(\phi\)-function:
\(f(n)\) is the number of positive integers at most \(n\) that are coprime to
\(n\). This is because \(\gcd(x, y) = \gcd(x, x + y) = \gcd(x, n)\), and after
fixing \(x\), \(y\) is already determined.
Now, it turns out that \(g\) is actually the identity function (\(g(n) = n\)
for all positive integers \(n\)). To prove this, recall that \(f\) is
multiplicative: that is, for coprime positive integers \(a\) and \(b\), \(f(ab)
= f(a)f(b)\), and that the summatory function of a multiplicative function is
also multiplicative. Thus, \(g\) is multiplicative, and it will suffice to show
that \(g(p^k) = p^k\) for primes \(p\) and nonnegative integers \(k\). To do
this, note that the divisors of \(p^k\) are \(1, p, p^2, \dots, p^k\), and that
\(f(p^i) = p^i - p^{i-1}\). Then \(g(p^k) = 1 + \sum_{i=1}^{k} p^{i} - p^{i-1}
= p^k\) (this is a telescoping sum). Since \(g(p^k) = p^k\) and \(g\) is
multiplicative, \(g(n) = n\) for all positive integers \(n\). (This is because
we can decompose any \(n\) into prime powers, all of which are clearly
coprime.)
So all that's left is applying \(f\) to \(n\) \(10^{12}\) times. First, to
apply \(f\) once, we just have to find the prime factorization of \(n\) (recall
that \(f\) is multiplicative); this takes \(O(\sqrt{n})\) time. Since \(n\) can
be up to \(10^{12}\), it seems as if we have to do \(10^{18}\) operations.
However, repeated application of \(f\) decreases \(n\) very rapidly. To
formalize this, we make two observations: first, if \(n\) is even, then \(f(n)
\leq n/2\). This follows from the fact that if \(n\) is even, every even
integer up to \(n\) is not coprime with \(n\). Second, if \(n \geq 3\),
\(f(n)\) is even. This is not too hard to prove: \(f(p^k) = p^k - p^{k-1}\),
which is even if \(k \geq 2\) or \(p\) is odd. Since \(f\) is multiplicative,
if \(n\) is not \(1\) or \(2\), then \(f(n)\) is even.
So if \(n\) is initially odd, a single application of \(f\) will make it
even. Then we need only \(O(\log n)\) iterations to reduce \(n\) to \(1\).
Thus, even though the given bound for \(k\) is \(10^{12}\), the answer won't
change for \(k \geq 40\) or so, and we can stop early if \(n\) ever becomes
\(1\), so our solution will easily run in time.
HackerRank University CodeSprint 2
February 20, 2017, 11:32 am
I realized that I haven't posted anything here since 2014, and it looks bad
to have a "blog" with nothing on it. I really enjoyed the problems in
HackerRank's
University CodeSprint 2
this weekend, so I figured I would write down the ideas from my solutions
here.
Problems A (Breaking the Records) and B (Separate the Numbers) were
straightforward implementation, and C (Game of Two Stacks) was a basic binary
search/two pointers problem. Below are my thoughts on the other three problems
I solved.
D: The Story of a Tree
We are given an (undirected, unrooted) tree and a set of queries which are
each in the form of a directed edge. We are asked to consider rooting the tree
at each of its \(N\) vertices, and in each case to count the number of queries
that are "correct"--that is, queries \((u, v)\) in which \(u\) is the parent of
\(v\). (It is guaranteed that there is an edge between \(u\) and \(v\).)
The obvious solution is \(O(N^2)\): for every root, DFS on the tree and
count the number of correct queries. This will be too slow for this problem,
but it's not hard to bring it down to \(O(N)\).
First, we will run the naive solution for an arbitrary root. Then note that
the number of correct queries after rooting the tree at an adjacent node can
differ by at most \(1\): if the edge we traverse is a query. So we will run
another DFS starting at the same node as before (because we know the answer for
this root), and every time we traverse an edge, we check if that edge is a
query (in each direction, \((u, v)\) and \((v, u)\)) and update our current
answer accordingly. Each of the two DFSs is linear, so the solution as a whole
is linear.
E: Sherlock's Array Merging Algorithm
We are given the output of a strange array-merging procedure, and we are
asked the number of distinct inputs that could have produced this output. The
procedure is as follows: we start with a collection of arrays \(V\), and for
each iteration, we remove the first item from each array, and insert these
\(|V|\) items into our final array in sorted order. Not all the starting arrays
have to be the same length (we just remove empty arrays after we remove their
last item).
The first observation to make is that each iteration of the merging
procedure produces a sorted subarray in the final array. We then want to count
the number of ways to partition the final array into sorted subarrays of
non-increasing length... sort of. If we do that, we will be undercounting: some
partitions correspond to more than one initial collection. For example,
partitioning \(123\) into \(12|3\) corresponds to both \(V = [13, 2]\) and
\(V = [1, 23]\). Instead of saying \(DP[i][j]\) is the number of ways to
partition the first \(i\) elements with the last partition having size \(j\),
we will call it the number of "permuted partitions". (I'm not sure if there is
a formal name for this.) Intuitively, we want to multiply by the number of ways
to match up this segment with the previous one.
Then it is clear that \(DP[i][j] = \sum_{k \geq j} (DP[i-j][k]
\frac{k!}{(k-j)!})\). These transitions are \(O(N)\), and there are \(O(N^2)\)
states. With constraints of \(N \leq 1200\), I expected this to time out, but
as it turns out, because of the small constant factor this is fast enough. I
suspect there is a way to reduce the time to \(O(N^2)\), but I'm not sure
how.
G: Minimum MST Graph
This problem took me all day Saturday, largely because I had an
off-by-one bug in one of my for loops but I was sure the error was
elsewhere.
We are given \(N\), \(M\), and \(S\)--the number of nodes, number of edges,
and MST weight--of an unknown graph. Our goal is to give the minimum sum of
weights of edges in a graph satisfying these three criteria.
First, instead of giving each edge a weight, we will give each node a
weight. Each edge's weight is then the maximum of the weights of its two
endpoints. We will assume that node \(0\) has the smallest weight, node \(1\)
has the second smallest, etc., and furthermore that node \(0\) has weight \(0\)
(every other node will have weight at least \(1\)). We will define an array
\(B\) based on these values. That is, \(B[i]\) is the weight of node \(i\).
Now we will define a second array \(A\) which contains information about the
structure of the graph we are constructing. In particular, \(A[i]\) is the
number of "backward edges" from node \(i\). If an edge connects nodes \(i\) and
\(j\) where \(i < j\), we say that edge is a "backward edge" from node \(j\).
This is useful because of the ordering of the nodes: the weight of node \(i\)
is no more than the weight of node \(j\), and so the weight of an edge is the
same as the weight of the node for which it is a backward edge.
So we have \(B[0] = 0\), and \(B[i] \geq 1\) for \(i > 0\). We also have
\(A[0] = 0\), and \(1 \leq A[i] \leq i\) for \(i > 0\). (If \(A[i] = 0\), then
our graph may not be connected, and even if it is, we can make our answer no
worse by making all \(A[i] \geq 1\); if \(A[i] > i\), then it must have
multiple edges or self loops. I encourage you to verify this for yourself.) For
simplicity, we will stop considering node \(0\) from now on, and assume that
our arrays are only defined on indices \(1 \leq i \leq N - 1\).
Note that \(\sum A[i] = M\) and \(\sum B[i] = S\). Subject to all these
constraints, we want to minimize \(\sum A[i] B[i]\).
This is a much more manageable and comprehensible problem, but it still
seems hard. First, let's suppose \(B\) is fixed and build an optimal \(A\). To
do this, first recall that \(B\) is increasing (albeit not strictly). Now, we
can set \(A[i] = 1\) for \(1 \leq i \leq N - 1\). We will add the remaining \(M
- (N - 1)\) edges to our graph greedily. For each edge, we should increment
\(A[i]\) for the smallest \(i\) that is not yet "full"--that is, for which
\(A[i] < i\). It is easy to see that this will lead to an array \(A\) with some
prefix of its elements being set to their index, potentially followed by at
most one element that is not \(1\), potentially followed by a bunch of \(1\)s.
So it turns out that \(A\) does not depend on \(B\) at all; that is, the
structure of our optimal graph depends only on \(N\) and \(M\)!
Having found \(A\), we only have to find the array \(B\) that minimizes \(A
\cdot B\). There is a greedy algorithm for this, too: let \(C[i]\) be the
"cost" of incrementing the last \(i\) elements of \(B\). It is equal to the sum
of the last \(i\) elements of \(A\), divided by \(i\). Intuitively, \(C[i]\) is
the amount by which our answer increases per unit of weight we allocate. Then
if we currently have \(X\) weight to distribute, we find the \(i \leq X\) for
which \(C[i]\) is minimum, and we increment the last \(i\) elements of \(B\).
When we have no more weight to distribute, we are done. Simply computing the
desired dot product yields the answer.
With reasonable implementation, this algorithm has time complexity \(O(N +
S)\), which will pass all but the last subtask (getting 70% score). The last
subtask isn't as interesting: it just takes some careful reasoning and algebra
to find a closed-form answer that can be computed in \(O(1)\) time.
Well, thanks for reading, and maybe I'll post some more competitive
programming-related stuff on here in the future if I again find myself trying
to avoid doing homework.
Hello, World!
November 23, 2014, 2:09 pm
I've created this website to serve as a personal portfolio and
résumé. I hope to post occasional updates
about my life or technical things on this blog.
If you want to hire me or chat, please email me at ethan @ this website.
Thanks!