Download as pdf or txt
Download as pdf or txt
You are on page 1of 10

CIIC’2012 Task Solutions

Pedro Ribeiro
A - Symmetries in the Mirror

Task proposed by: Pedro Ribeiro (Portugal)


Task type: strings, ad-hoc next palindrome, simple arbitrary precision arithmetic

Palindromes are a very well known special class of strings. In their general form, palin-
dromes are words than can be read the same in either direction: backwards or forwards. In
this problem you are asked to work with a similar definition, but in a more constrained form.
You are only concerned with numbers and they must present reflection symmetry (RS), a
property based on the author’s own memories of playing with his calculator in front of a
mirror. Given a number, we must compute which is the closest number (to the one given)
that presents RS.
Before starting the presentation of possible solutions to this task, let’s define two concepts:

• previous(n): is the largest number smaller than n that presents RS.

• next(n): is the smallest number bigger than n that presents RS.

For any given number n, the closest number with RS will either be n itself, previous(n)
or next(n). Any other smaller or larger number with RS will be further away from n. Our
task can now be reduced to finding out these two numbers, given n.

Naive solution (Brute Force)


An intuitive first approach is to start iterating though all numbers. If the given number n
has itself RS, we have our answer. If not, we can start decrementing n (one by one) until we
hit a number with RS, which will be previous(n). If we increment n (one by one), the first
number with RS is next(n). With these values, we only need to print the one closest to n.
In order to find out if a single number has RS, we can do a simple cycle: we compare the
first digit with the last one, the second digit with the penultimate and so on. If both digits
are 0, 1 or 8, or if one if 2 and the other is 5, we have an RS number.
This approach was enough to score 30% of the points.

Improving our search


In order to pass more test cases, we need to do better. If we try to analyze which numbers
have RS, we can see that they are a small fraction of the entire quantity of numbers. For test
cases worth 60% of the points, we know that all tested numbers are smaller or equal than
109 (which also fits perfectly on any normal 4-byte integer type). By testing, even without
any mathematical proof, we can discover that all RS numbers up to 109 are not that many
and that we can store them in memory. If we have these numbers pre-computed, given any

1
number n we can easily use binary search to find out previous(n) and next(n). Care must
be taken when the n = 109 , because next(109 ) is (obviously) larger than 109 . In fact it is...
109 + 1.
How could we generate the numbers? There were several ways of doing this efficiently.
Basically, we could limit the used digits to the ones that matter and generate (in order) the
first half of each RS number, with the second half being a consequence of the first half.
This approach was enough to score 60% of the points.

Dealing with larger numbers


The task admitted numbers up to 100 digits, for which this last approach was not feasible,
because the amount of RS numbers would be to big to fit in memory (and at the same
time the numbers would not fit a native integer variable type). Given this, we need another
(more efficient) way to compute previous(n) and next(n). And in fact we can do this in time
proportional to the number of digits of n!
Let’s by now focus on the computation of next(n). Given any number, how can we
transform it on an RS number? Since the second half of the number is completely determined
by the first half, let’s concentrate on this first half. Let’s suppose the number is constituted
by the following digits:
d1 , d2 , . . . , dm−1 , dm
We start by looking at d1 . If it is 0 (which only happens on di , i > 1), 1, 2, 5 or 8, it is
ok for being a candidate to RS. If not, then we must use the smallest largest number that
could induce RS. For example, if the d1 = 6, then 8 is the smallest digit bigger than it than
can produce an RS number. We continue like this for all digits up to the first half and we
obtain an RS number.
Care must be taken for some special cases. For instance, if the digit is 9, then it must be
transformed in 0 and the previous digit must also be increment so that the obtained number
is larger. Another case is when the obtained number is smaller then n (for example, 2008
would see it’s first half being maintained - 20 - which would therefore generate the second
half as 05, generating the number 2005 which is smaller the original number). In this case
we must now find the next SM number, which can be obtained by increasing the inner most
digit (which makes the number grow less - note that changing the last digit would imply also
changing the first digit), while maintaining RS.
We will spare the reader of more fine grained details of this procedure, but we are sure you
can now have at least an idea and intuition on how to implement it. The ad-hoc previous(n)
procedure could be done in a similar fashion. It was easy to forget something or make small
mistakes while coding, but a more naive implementation could be used to exhaustively test
your own specialized procedures on small inputs.
A final touch was finally needed. In order to find out which number was closer (previous
or next) you also needed to somehow implement a simple arbitrary precision subtraction (or
something equivalent).
With all of this, we could now solve our task for 100% of the points.

2
B - The New Rafael’s House

Task originally proposed by: Mario Cruz and Ivan Arias (Colombia)
Task modifications: Pedro Ribeiro (Portugal)
Task type: Search, Backtracking, Branch and Bound, BFS, A*

The original formulation of this problem did not have any areas specifically marked for
not having carpet. We decided to include this new constraint so that the problem input
was not merely the width and height of the house, which would allow different types of
approaches. In fact, it could become possible to pre-compute the answers for all possible
20*20 cases, even with a solution that would take more than 1 second, and simply submit
code with all the answers. More than that, the inclusion of these special “no-carpet” areas,
allow us to create all kinds of shapes.

Incorrect approaches
We could wrongly suppose that a greedy approach would suffice for this problem. For
instance, some contestants tried to always use the largest possible square on any yet unused
cell. This would generate a possible “good” solution but that nevertheless was far from being
guaranteed to be the optimal, which is what the task asks for. Can the reader think of some
simple counter-examples?
Another incorrect approach was to think that the problem admitted a “typical” dynamic
programming (DP) solution. Suppose that for a given empty cell we would try all possible
square sizes S and partition the remaining area in two different rectangles on these two ways,
using DP to “memoize” the solution for smaller rectangles (see the 2 figures on the left):

This kind of solution would not consider tilings that need a square to be between the two
created partitions. In other words, we would be “cutting” through squares of the optimal
solution, ending up using more carpets than needed. The figure on the right illustrates a
counter-example, where by putting the bottom-left square we would not be able to divide
the remaining area in two distinct rectangles.

3
Both the greedy, DP and similar approaches would be able to get a small amount of
points on a reduced number of test cases, that were designed to at least reward the attempt
to make an admissible solution.

Naive solution with backtracking (Brute Force)


A natural way to solve this problem would be to simple try exhaustively all possible carpet
placements and in the end output the minimum number of squares found. This could be
done for instance with backtracking, by trying all possible squares on all still empty cells,
continuing recursively with the remaining area.
For simulating the placement of carpets, we could for instance just keep a matrix indi-
cating the current state of each cell, while updating and checking it when necessary.
This kind of approach, without nothing else added, would suffice to get around 40% of
the points.

Improving the search with branch and bound


The tests were made in incremental sizes and number of needed carpets, so that successive
optimizations could lead to more points.
There are certain branches of the search that one does need to follow, because we know
that they will not improve our result. One simple and intuitive cut is to stop searching a
branch when the current number of carpets is already equal to our current best minimum
number of squares, because continuing that placement will always be worse than our current
solution.
This cut works better if we have a good first heuristic guess of the minimum needed. We
could use one of the incorrect approaches given on the previous section to initialize our best
solution on a much smaller value than the one given by any random placement. Furthermore,
we could guide our search with those heuristics so that we first try moves that we guess will
be better, further reducing the minimum found and allowing us to cut more in the search.
Note that we still need to exhaustively try all possibilities, but we may be able to rule out
more combinations by adequately not following any path that cannot improve our solution.

Improving the state representation


Up until now, we have only spoken about using a matrix for representing the state of the
carpet placement. It is possible to do better than. For instance, if we always start filling
from the bottom to the upper part of the house, we can imagine the filled positions as an
histogram and represent a state with a simple array of integers, as the figure in next page
exemplifies.

4
With this kind of state we save memory and we can be more efficient on generating new
states.

Breadth-first search
Instead of using DFS, we could use BFS and traverse the states in the order of needed
carpets. By keeping the memory usage as low as possible, we could reach many different
combinations. We could also experiment in trading memory for time, by keeping a memory
of the visited states, avoiding the redundant work of visiting them again, because the state
can be reached using different carpet placements.
All of this in place would suffice to get around 80% of the points.

Using a best-first strategy (A*)


When solving the kind of task manually, one can easily see that a certain partial solutiosn
will never give origin to better solutions than others. We could use a similar rationale to
improve our search. For instance, we know than any corner surrounded by other carpets or
no placement cells from three sides will need to be the corner of a placed carpet. If we use
this (or similar measurements of the minimum number of carpets still needed to fill up the
house), we can have an optimistic notion on the least possible number of carpets that our
partial solution will need.
Now, instead of visiting the states using as order the number of carpets placed (simple
BFS), we can visit them using the number of carpets that will need to be placed at least
(current carpets plus minimum still needed), using what is known as A* algorithm.
This kind of approach was enough to receive 100% of the points.

Further improvements
For the used test cases, the described approach was enough. However, we could do better.
For instance, implemented solutions used features such as bidirectional search (also known
as meet-in-the-middle technique), where we started looking at the same time for placement
of carpets on opposite sides of the house. We also experimented with more constrained
versions of the state, squeezing the information for every possible bit and we used more
stronger heuristics than simply counting the unfilled corners.

5
C - Boxes

Task proposed by: Filipe Brandão (Portugal)


Task type: simulation, binary search, segment-tree

In this problem we were asked to simulate a specific procedure for filling up boxes with
objects, computing the minimum amount of boxes needed.

Naive solution (Brute Force)


The simplest solution was to just simulate everything. We start by trying to use one box
and see if that is enough. Then two boxes, three boxes and so on.
For simulating Rafael and Ana’s behavior we could simply store an array of unused
capacities and for each new object we could linearly search for the nearest box with enough
capacity, always starting from the correspondent end of the row.
Is we denote by N the total number of objects (R+A), this approach would have a
complexity of O(N 3 ), because for every possible number of objects, we would have to go
trough each object and linearly find its box.
This kind of approach was enough to receive 30% of the points.
A simple improvement could me made by considering a better lower bound for the number
of boxes but care should be taken for not overflowing (for instance, a simple sum of the
object’s weights would be larger than what is possible to store on an integer, since although
one box fits an integer, thousand of them do not).

Binary search the answer


We need a better solution than O(N 3 ). We can observe that if B boxes are enough for
storing all the objects, than B + 1 boxes are also enough. With this in mind we can apply
binary search to the minimum number B required.
This approach would have a complexity of O(N 2 logN ) since although we are able to
have a logarithmic search on the number of boxess, we still need do to a naive simulation,
by linearly searching for the box of each object.
This kind of approach was enough to receive 60% of the points.

Logarithmic simulation
For the given constraints we need to better than the linear search on the simulation. If
fact, we can have logarithmic time for finding the leftmost (or right most) box that still has
enough capacity, when performing a single simulation.
There are several hypotheses for this, but we will describe an approach based on segment-
trees. Imagine that we create a tree where all the boxes are in leafs, storing the capacity

6
still available, and that parent nodes always store the maximum value of its descendants.
The following figure illustrates a tree for a case in which we have boxes with capacities
[3, 5, 7, 7, 7, 4, 1, 1]:

It is easy to see that the depth of the tree will be in the order of log N. Now, when we are
looking for where to put a certain object we can traverse the tree just once accordingly. For
instance, imagine that we are Rafael (we want to use the leftmost possible box) and need to
place a box of size 6. We start by the root an look at the left child, which has max value
7 and therefore we know that we can go that way. We now look at the following left child,
which is 5 and smaller than what need. We therefore go to the right side. We continue this
behavior until we reach a leaf. For Ana, the algorithm is the same, but we prefer to go right
when both children have enough capacity for the object.
After putting an object in a box, we need to update the tree. We can do that by making
a single bottom-up traversal from the leaf to the node, updating the visited nodes with new
child maximums.
This whole procedure (finding the box plus updating the tree) run in O(logN ). With this
we can improve our algorithm for this task to O(N log 2 N ), since for each binary searched
amount of boxes we must pass through all objects and logarithmically compute their assigned
box.
This kind of approach enough to receive 100% of the points.

7
D - Complicated Traffic

Task proposed by: Pedro Ribeiro (Portugal)


Task type: graphs, maximin paths, dijkstra, heaps

A contest without a graph problem would be like going to Italy and not eating pasta :)
This was the problem set’s graph problem and essentially asked for paths which maximized
the minimum weight of the path, which is also known as maximin distance.

Naive solution (Brute Force)


An initial solution would be to simple test all possible paths from start to finish keeping the
best. For the smaller cases, the maximum K (number of renovations) was 1, which meant
that we could even try doubling each edge individually and the repeat the search to see if
we could get a better solution.
This approach, although exponential in complexity, was enough to receive up to 40% of
the points.

Improving the state representation


If we carefully analyze the problem, we can see that at a certain point of our path, “only”
three things matter:

• At which graph node we are;

• What was the minimum distance followed so far;

• How many renovations have we already made.

It does not matter exactly where we did the road renovations (as long as we don’t repeat
nodes in path), because the only thing that can influence the outcome of the path is the min
distance up to that point and how many renovations we can still use.
This could be used coupled with the brute force approach to solve cases for larger K’s,
which would slightly improve the score.

Using a “modified” Dijkstra


The brute force approach is however not enough. What we could do, instead of searching all
possible paths, was to use a modified version of Dijkstra’s algorithm for the SSSP problem
(single-source shortest path).
In “normal” Dijkstra we keep information about the distances to the nodes that we may
travel afterwards and we select the one that minimizes this distance. Each time we visit

8
an edge, we relax its adjacent edges and verify if its induces better paths for any of the
not visited nodes. Here we can do the same, with a slight modification: we need to search
for the maximum (we want to obtain the largest possible speed) and we need to adapt the
relaxation process to reflect maximin distance. We should also consider K nodes for each
graph node, representing arriving there with 0 renovations made, with 1 renovation, etc.
A simple Dikjstra algorithm takes time O(N 2 ) (for each step we must linearly choose the
maximum among the unvisited nodes) and this was enough for about 75% of the points.

Improving the relaxation process


As any contestant probably knows, Dijkstra can be implemented with complexity O(N logN )
by using an adequate data-structure for the relaxation process, such as an heap or a prior-
ity queue.
Applying an O(N logN ) Dijkstra to this problem was enough for obtaining 100% of the
points.

You might also like