Links:
Google Code Jam Statistics: http://www.go-hero.net/jam/
Google Code Jam Scoreboard: http://code.google.com/codejam/contest/scoreboard?c=204113
Overview
Round 2 proved to be a bit trickier than Round 1, especially in terms of advancement as only the top 500 go through. The time setting didn't help either for many contestants. Unfortunately I didn't make it through but congratulations to everyone who did! It has been a major blast of fun :)
So what happened? There are several key points I've learnt from participating this round.
1. Don't focus too much on constraints!
For problem A, my first instinct was a greedy based algorithm which was rather close to the correct algorithm. However, I quickly convinced myself (wrongly) that it would fail for cases in which you would possibly make a move which turns the remaining subproblem infeasible. This does not happen though as the strictly increasing ordering of the final output prevents this from happening. This was further misled by looking at the large constraint which had a maximum of 40. Surely a greedy algorithm can't work right? Wrong! :)
Although it is always good to look at constraints (and perhaps I've relied too heavily on it) to dictate what algorithm the problem setter is expecting don't let it "blind" (i.e. let it confirm your incorrect doubts of an algorithm without fully going through it) you though as it did to me.
2. Don't get too focused on a particular methodology and learn to back out of an idea if you can't get anywhere with it!
After solving A-small and D-small, the only way I would advance is to solve both C-small and C-large (or B but fewer people tried to solve it so I also tried C). Instead of wasting time on finding the correct solution to A-large, I proceeded with problem C.
First thought was bipartite matching (and that hunch was correct) as it felt natural (because I've actually solved a very similar problem). I couldn't reduce the graph properly with the correct edge labelling criteria so I completely abandoned this idea (bad move as I did in fact at one point have the correct algorithm but made a false assumption which would of made it incorrect). I eventually deduced it to finding the minimum number of complete induced subgraphs (clique problem). However this is NP-complete, so I really should of backed out of this reduction but I was persistent as this was the only "correct" (although it would time out) solution I had. Sometimes it pays off to let go of an idea if you can't get anywhere with it which is easier said than done!
Despite being slightly disappointed I had a fun time. Unfortunately no Australians (unofficially at this point) have made it through to Round 3. Good luck for all those participating in Round 3 though :)
Official Contest Analysis is available: http://code.google.com/codejam/contest/dashboard?c=204113#s=a
Showing posts with label Google Code Jam. Show all posts
Showing posts with label Google Code Jam. Show all posts
Sunday, September 27, 2009
Monday, September 14, 2009
Google Code Jam Round 1C
Overview:
This was the last opportunity for qualifiers to advance into Round 2. Congratulations to everyone who has made it to Round 2! Bad luck to those that didn't, there's always next year!
This round was greeted with a set of interesting problems. Problem A was the easy problem in the set, and most competitors were able to solve the small input with some incorrect submissions for the large case. Problem B proved rather interesting and had multiple correct approaches. One of which required a little mathematical manipulation to derive a short and safe implementation, the other had the danger of precision issues but was based on a simpler concept without the use of much mathematics. Problem C was a fairly subtle DP problem in disguise but this did not deter the top competitors to solve all three problems in under 40 minutes.
Links:
Google Code Jam Statistics: http://www.go-hero.net/jam/
Google Code Jam Scoreboard: http://code.google.com/codejam/contest/scoreboard?c=189252
Google Code Jam Unofficial Scoreboard/Online Source Viewer (Thanks to Zibada on TC): http://zibada.ru/gcj/2009/round1a.html, http://zibada.ru/gcj/2009/round1b.html, http://zibada.ru/gcj/2009/round1c.html
Problem A: All Your Base
Category: Greedy
URL: http://code.google.com/codejam/contest/dashboard?c=189252#s=p0
The problem asks us to determine the minimum possible number we can derive from an unknown number representation system. To do this we must assign values to each of the characters present in the symbols. For example let's say we are given "cats" as an input we need to determine what the 'c' means numerically, what the 'a' means numerically etc. Since we want the smallest possible number, it suggests a greedy strategy of assigning what each letter means. In our case, we first see 'c' and therefore wish for this to be as small as possible. However, there is an additional constraint that no number starts with a 0 and since 'c' is the first symbol it cannot be 0. Therefore, the next obvious choice is to give it a value of 1. Next, we process 'a' and assign it a value of 0 since it hasn't been used yet and it's not the first symbol. Following this, 't' gets assigned 2 and 's' gets assigned 3.
Now the next task is to determine the base system the aliens work on, it can be easily shown we want the base as small as possible - yet large enough to allow us to use all the symbols. Therefore, the base system is simply the number of unique characters/letters in the alien number. We are then tasked with converting this to "human" form (i.e. decimal representation) which can be done iteratively like any base conversion algorithm. Implementation wise, we can keep track of our character assignments in a map data structure. When we encounter a letter, we first check whether or not we have seen the letter - if we haven't then we greedily assign it the next available (and smallest) number. If we have, then we simply skip over the character.
Problem B: Centre of Mass
Category: Maths, Ternary Search
URL: http://code.google.com/codejam/contest/dashboard?c=189252#s=p1
This is a tricky problem to tackle due to precision issues, so it's safer to go for a closed-form solution. Basically, we are given N fireflies each starting off in different positions and each going their own directions with a constant velocity. We need to calculate the distance and time in which the centre of mass of the N fireflies are closest to the origin (0,0,0). We need to make one key observation to heavily simplify this problem. If we look closely, we can collapse all of the N fireflies into one single entity (this is heavily suggested by the notes section in the actual problem statement). If we only have a single point (i.e. the centre of mass) we simply need to find the point in time/distance in which it comes closest to the origin. With some visualisation we can see that the centre of mass movement is divided into two cases:
- There is no movement hence the earliest time and closest distance it is to the origin is at time = 0.
- There is movement (i.e. velocity) then the earliest time and closest distance is unique (as there would be a point in which it gets closer and closer and after the minimum distance it gets larger and larger).
With these two conditions, we have multiple approaches to solve the problem. The first approach is to use a ternary search over the set of possible times, as there is only one minima for the movement case - we are guaranteed that the algorithm will converge to the spot eventually. Ternary search works on functions which have only one extrema that is either strictly increasing then strictly decreasing or the other way around. For more information on this search technique look at: http://en.wikipedia.org/wiki/Ternary_search. The second approach is to differentiate the distance equation - as we know there is only 1 solution we can confidently differentiate and solve for d/dt = 0 to obtain the unique solution. We will now proceed to demonstrate this approach.
We first need to collapse the N fireflies into the centre of mass vector with respect to time. This can be done by a straight summation over the coefficients of the starting positions and velocities. Let S = (Sx, Sy, Sz) be the summation of the starting positions of the N fireflies (this is a straight application of the formula given in the notes section). Let D = (Dx, Dy, Dz) be the summation of the velocities of the N fireflies (similar argument applies to the velocity). Then we yield a distance (with respect to time T) equation of:
D = sqrt((Sx + Dx * T)^2 + (Sy + Dy * T)^2 + (Sz + Dz * T)^2)
We can ignore the sqrt for differentiation purposes as it will get cancelled out later when it's multiplied by 0 (another reason is the function is strictly increasing, and as a result has no effect on the location of the extrema):
D = (Sx + Dx * T)^2 + (Sy + Dy * T)^2 + (Sz + Dz * T)^2
Now let's differentiate (using the Chain Rule):
dD/dT = 2*(Sx + Dx * T)*(Dx) + 2*(Sy + Dy * T)*(Dy) + 2*(Sz + Dz * T)*(Dz)
Let dD/dT = 0,
0 = 2*(Sx + Dx * T)*(Dx) + 2*(Sy + Dy * T)*(Dy) + 2*(Sz + Dz * T)*(Dz)
0 = 2*((Sx + Dx * T)*Dx + (Sy + Dy * T)*Dy + (Sz + Dz * T)*Dz)
0 = (Sx + Dx * T)*Dx + (Sy + Dy * T)*Dy + (Sz + Dz * T)*Dz
Expanding:
0 = Sx*Dx + Dx*Dx*T + Sy*Dy + Dy*Dy*T + Sz*Dz + Dz*Dz*T
Pushing all the T terms into the LHS:
-Dx*Dx*T - Dy*Dy*T - Dz*Dz*T = Sx*Dx + Sy*Dy + Sz*Dz
T(-Dx*Dx - Dy*Dy - Dz*Dz) = Sx*Dx + Sy*Dy + Sz*Dz
Therefore T is equal to:
T = Sx*Dx + Sy*Dy + Sz*Dz / (-Dx*Dx - Dy*Dy - Dz*Dz)
Now we need to be careful with dividing by 0, if (-Dx*Dx - Dy*Dy - Dz*Dz) is 0 this means that there is no movement (i.e. our first condition). We can safely make a separate case for this as the closest time is at t=0. Using these concepts we just need to do it which is pretty simple:
Problem C: Bribe the Prisoners
Category: Dynamic Programming
URL: http://code.google.com/codejam/contest/dashboard?c=189252#s=p2
For the final problem, we are given a set of prison cells (from 1 to P) and inside each of these prison cells is one prisoner. Prisoners communicates with its neighbours if the cell is adjacent and there is at least one prisoner inside. As a prisoner is released, the neighbours find out (if any) and this effect gets echoed in both directions until it hits the end of the prison (i.e. left or right wall) or when there is no one in the neighbouring prison (because they were released earlier). We are then tasked to bribe each prisoner who hears about a prisoner getting released 1 gold coin to prevent them from destroying their cell. We need to identify the optimal sequence of prisoners to release as to minimise the number of gold coins spent.
The obvious way is to brute force all possible ordering of prisoners and compute the minimum gold coins spent. This works easily for the small case as there are only 5 prisoners awaiting to be released at the worst case. This obviously does not work for the large case as there can be as many as 100 prisoners to be released. To optimise this we turn to dynamic programming. Seeing there can be as many as 100 prisoners to be released we can't keep a bitmask of which prisoners we have released as it will certainly TLE and/or exceed memory limit (on all current systems). The key observation is to divide the prison into sub-prisons. This can be seen by noting that the ends of the prison cells act as barriers/walls in which the word does not go through. If we release prisoners from cells - the cells we have just freed implicitly acts like the ends of our original prison.
In addition, we need to make a small optimisation as the prisons can be as large as 10,000 in the large case. We need to note that we will never free from a cell which is not in the list of prisoners to be freed. Hence instead of keeping track of the index of the prison cells itself, we keep track of the index of the prisoners to be freed. A simple sort suffices to keep the implementation neat. Hence the space complexity is only O(Q*Q) and since Q is at most only 100 - this is pretty much nothing.
So we define the DP state as:
D(n,m) = the minimum number of gold spent to free prisoners between index n and m of the prisoners to be freed.
Our recurrence relation is then simply defined as:
D(n,m) = min { Cost(i) + D(n,i) + D(i,m) } where n < i < m. (free prisoner i)
D(n,m) = 0 if m - n < 2 (base case)
The base case is for situations where there are no prisoners to be freed between n and m. Cost(i) is defined as the cost to free prisoner index i given that the empty prison to the left is n and the empty prison to the right is m. This can be calculated in constant time as we know which locations index n and m corresponds to. There is a small trick for implementing this - we add explicit left and right barriers to our original prison to make it conform to our recursion. This way we don't need to handle for the original prison special case which would otherwise be unbounded.
Implementing this should be simple with either memoisation (top-down) or a bottom-up approach:
This was the last opportunity for qualifiers to advance into Round 2. Congratulations to everyone who has made it to Round 2! Bad luck to those that didn't, there's always next year!
This round was greeted with a set of interesting problems. Problem A was the easy problem in the set, and most competitors were able to solve the small input with some incorrect submissions for the large case. Problem B proved rather interesting and had multiple correct approaches. One of which required a little mathematical manipulation to derive a short and safe implementation, the other had the danger of precision issues but was based on a simpler concept without the use of much mathematics. Problem C was a fairly subtle DP problem in disguise but this did not deter the top competitors to solve all three problems in under 40 minutes.
Links:
Google Code Jam Statistics: http://www.go-hero.net/jam/
Google Code Jam Scoreboard: http://code.google.com/codejam/contest/scoreboard?c=189252
Google Code Jam Unofficial Scoreboard/Online Source Viewer (Thanks to Zibada on TC): http://zibada.ru/gcj/2009/round1a.html, http://zibada.ru/gcj/2009/round1b.html, http://zibada.ru/gcj/2009/round1c.html
Problem A: All Your Base
Category: Greedy
URL: http://code.google.com/codejam/contest/dashboard?c=189252#s=p0
The problem asks us to determine the minimum possible number we can derive from an unknown number representation system. To do this we must assign values to each of the characters present in the symbols. For example let's say we are given "cats" as an input we need to determine what the 'c' means numerically, what the 'a' means numerically etc. Since we want the smallest possible number, it suggests a greedy strategy of assigning what each letter means. In our case, we first see 'c' and therefore wish for this to be as small as possible. However, there is an additional constraint that no number starts with a 0 and since 'c' is the first symbol it cannot be 0. Therefore, the next obvious choice is to give it a value of 1. Next, we process 'a' and assign it a value of 0 since it hasn't been used yet and it's not the first symbol. Following this, 't' gets assigned 2 and 's' gets assigned 3.
Now the next task is to determine the base system the aliens work on, it can be easily shown we want the base as small as possible - yet large enough to allow us to use all the symbols. Therefore, the base system is simply the number of unique characters/letters in the alien number. We are then tasked with converting this to "human" form (i.e. decimal representation) which can be done iteratively like any base conversion algorithm. Implementation wise, we can keep track of our character assignments in a map data structure. When we encounter a letter, we first check whether or not we have seen the letter - if we haven't then we greedily assign it the next available (and smallest) number. If we have, then we simply skip over the character.
#include <iostream>
#include <vector>
#include <string>
#include <sstream>
#include <algorithm>
#include <stack>
#include <queue>
#include <map>
using namespace std;
map<char,int> memo;
int main() {
int nT; cin >> nT;
string str;
int k = 1;
while (nT-- && cin >> str) {
memo.clear();
int next = 0;
memo[str[0]] = 1;
for (int i = 1; i < str.size(); i++) {
if (memo.count(str[i]) == 0) {
memo[str[i]] = next;
next++;
if (next == 1) next++;
}
}
long long val = 0;
int base = next == 0 ? 2 : next;
for (int i = 0; i < str.size(); i++) {
val = val * base + memo[str[i]];
}
cout << "Case #" << k << ": " << val << "\n";
k++;
}
return 0;
}
Problem B: Centre of Mass
Category: Maths, Ternary Search
URL: http://code.google.com/codejam/contest/dashboard?c=189252#s=p1
This is a tricky problem to tackle due to precision issues, so it's safer to go for a closed-form solution. Basically, we are given N fireflies each starting off in different positions and each going their own directions with a constant velocity. We need to calculate the distance and time in which the centre of mass of the N fireflies are closest to the origin (0,0,0). We need to make one key observation to heavily simplify this problem. If we look closely, we can collapse all of the N fireflies into one single entity (this is heavily suggested by the notes section in the actual problem statement). If we only have a single point (i.e. the centre of mass) we simply need to find the point in time/distance in which it comes closest to the origin. With some visualisation we can see that the centre of mass movement is divided into two cases:
- There is no movement hence the earliest time and closest distance it is to the origin is at time = 0.
- There is movement (i.e. velocity) then the earliest time and closest distance is unique (as there would be a point in which it gets closer and closer and after the minimum distance it gets larger and larger).
With these two conditions, we have multiple approaches to solve the problem. The first approach is to use a ternary search over the set of possible times, as there is only one minima for the movement case - we are guaranteed that the algorithm will converge to the spot eventually. Ternary search works on functions which have only one extrema that is either strictly increasing then strictly decreasing or the other way around. For more information on this search technique look at: http://en.wikipedia.org/wiki/Ternary_search. The second approach is to differentiate the distance equation - as we know there is only 1 solution we can confidently differentiate and solve for d/dt = 0 to obtain the unique solution. We will now proceed to demonstrate this approach.
We first need to collapse the N fireflies into the centre of mass vector with respect to time. This can be done by a straight summation over the coefficients of the starting positions and velocities. Let S = (Sx, Sy, Sz) be the summation of the starting positions of the N fireflies (this is a straight application of the formula given in the notes section). Let D = (Dx, Dy, Dz) be the summation of the velocities of the N fireflies (similar argument applies to the velocity). Then we yield a distance (with respect to time T) equation of:
D = sqrt((Sx + Dx * T)^2 + (Sy + Dy * T)^2 + (Sz + Dz * T)^2)
We can ignore the sqrt for differentiation purposes as it will get cancelled out later when it's multiplied by 0 (another reason is the function is strictly increasing, and as a result has no effect on the location of the extrema):
D = (Sx + Dx * T)^2 + (Sy + Dy * T)^2 + (Sz + Dz * T)^2
Now let's differentiate (using the Chain Rule):
dD/dT = 2*(Sx + Dx * T)*(Dx) + 2*(Sy + Dy * T)*(Dy) + 2*(Sz + Dz * T)*(Dz)
Let dD/dT = 0,
0 = 2*(Sx + Dx * T)*(Dx) + 2*(Sy + Dy * T)*(Dy) + 2*(Sz + Dz * T)*(Dz)
0 = 2*((Sx + Dx * T)*Dx + (Sy + Dy * T)*Dy + (Sz + Dz * T)*Dz)
0 = (Sx + Dx * T)*Dx + (Sy + Dy * T)*Dy + (Sz + Dz * T)*Dz
Expanding:
0 = Sx*Dx + Dx*Dx*T + Sy*Dy + Dy*Dy*T + Sz*Dz + Dz*Dz*T
Pushing all the T terms into the LHS:
-Dx*Dx*T - Dy*Dy*T - Dz*Dz*T = Sx*Dx + Sy*Dy + Sz*Dz
T(-Dx*Dx - Dy*Dy - Dz*Dz) = Sx*Dx + Sy*Dy + Sz*Dz
Therefore T is equal to:
T = Sx*Dx + Sy*Dy + Sz*Dz / (-Dx*Dx - Dy*Dy - Dz*Dz)
Now we need to be careful with dividing by 0, if (-Dx*Dx - Dy*Dy - Dz*Dz) is 0 this means that there is no movement (i.e. our first condition). We can safely make a separate case for this as the closest time is at t=0. Using these concepts we just need to do it which is pretty simple:
#include <iostream>
#include <string>
#include <algorithm>
#include <cmath>
using namespace std;
int main() {
int T; cin >> T;
for (int tt = 0; tt < T; tt++) {
int N; cin >> N;
double dX = 0.0, dY = 0.0, dZ = 0.0;
double sX = 0.0, sY = 0.0, sZ = 0.0;
// summation of the different components
for (int i = 0; i < N; i++) {
double x, y, z, tx, ty, tz;
cin >> x >> y >> z >> tx >> ty >> tz;
dX += tx; dY += ty; dZ += tz;
sX += x; sY += y; sZ += z;
}
dX /= N; dY /= N; dZ /= N;
sX /= N; sY /= N; sZ /= N;
int before = 0;
double t;
// check for division by zero case
if (dX * dX + dY * dY + dZ * dZ < 1e-09) {
before = 1;
} else {
t = -(sX*dX + sY*dY + sZ*dZ) / (dX*dX + dY*dY + dZ*dZ);
}
double dist = 0;
// time can't be negative, if it is - then the optimal time is on t = 0
if (t < 0 || before) {
t = 0;
dist = sqrt(sX*sX + sY*sY + sZ*sZ);
} else {
dist = sqrt((sX+dX*t)*(sX+dX*t)+(sY+dY*t)*(sY+dY*t)+(sZ+dZ*t)*(sZ+dZ*t));
}
cout << "Case #" << (tt+1) << ": ";
cout << dist << " " << t << "\n";
}
return 0;
}
Problem C: Bribe the Prisoners
Category: Dynamic Programming
URL: http://code.google.com/codejam/contest/dashboard?c=189252#s=p2
For the final problem, we are given a set of prison cells (from 1 to P) and inside each of these prison cells is one prisoner. Prisoners communicates with its neighbours if the cell is adjacent and there is at least one prisoner inside. As a prisoner is released, the neighbours find out (if any) and this effect gets echoed in both directions until it hits the end of the prison (i.e. left or right wall) or when there is no one in the neighbouring prison (because they were released earlier). We are then tasked to bribe each prisoner who hears about a prisoner getting released 1 gold coin to prevent them from destroying their cell. We need to identify the optimal sequence of prisoners to release as to minimise the number of gold coins spent.
The obvious way is to brute force all possible ordering of prisoners and compute the minimum gold coins spent. This works easily for the small case as there are only 5 prisoners awaiting to be released at the worst case. This obviously does not work for the large case as there can be as many as 100 prisoners to be released. To optimise this we turn to dynamic programming. Seeing there can be as many as 100 prisoners to be released we can't keep a bitmask of which prisoners we have released as it will certainly TLE and/or exceed memory limit (on all current systems). The key observation is to divide the prison into sub-prisons. This can be seen by noting that the ends of the prison cells act as barriers/walls in which the word does not go through. If we release prisoners from cells - the cells we have just freed implicitly acts like the ends of our original prison.
In addition, we need to make a small optimisation as the prisons can be as large as 10,000 in the large case. We need to note that we will never free from a cell which is not in the list of prisoners to be freed. Hence instead of keeping track of the index of the prison cells itself, we keep track of the index of the prisoners to be freed. A simple sort suffices to keep the implementation neat. Hence the space complexity is only O(Q*Q) and since Q is at most only 100 - this is pretty much nothing.
So we define the DP state as:
D(n,m) = the minimum number of gold spent to free prisoners between index n and m of the prisoners to be freed.
Our recurrence relation is then simply defined as:
D(n,m) = min { Cost(i) + D(n,i) + D(i,m) } where n < i < m. (free prisoner i)
D(n,m) = 0 if m - n < 2 (base case)
The base case is for situations where there are no prisoners to be freed between n and m. Cost(i) is defined as the cost to free prisoner index i given that the empty prison to the left is n and the empty prison to the right is m. This can be calculated in constant time as we know which locations index n and m corresponds to. There is a small trick for implementing this - we add explicit left and right barriers to our original prison to make it conform to our recursion. This way we don't need to handle for the original prison special case which would otherwise be unbounded.
Implementing this should be simple with either memoisation (top-down) or a bottom-up approach:
#include <iostream>
#include <vector>
#include <string>
#include <sstream>
#include <algorithm>
#include <stack>
#include <queue>
#include <map>
using namespace std;
int memo[111][111];
vector<int> data;
int func(int leftIdx, int rightIdx) {
// base case
if (rightIdx - leftIdx < 2) {
return 0;
}
if (memo[leftIdx][rightIdx] != -1) return memo[leftIdx][rightIdx];
int res = 1<<30;
for (int i = leftIdx+1; i < rightIdx; i++) {
// split on prisoner index i
int calc = (data[i] - data[leftIdx] - 1) + (data[rightIdx] - data[i] - 1);
calc = calc + func(leftIdx, i) + func(i, rightIdx);
res = min(res,calc);
}
return memo[leftIdx][rightIdx] = res;
}
int main() {
int N;
cin >> N;
for (int t = 0; t < N; t++) {
int P, Q; cin >> P >> Q;
vector<int> pris;
for (int i = 0; i < Q; i++) {
int val; cin >> val; pris.push_back(val);
}
// add explicit barriers to the ends of the prisons
pris.push_back(0);
pris.push_back(P+1);
// ensure that the prisoner indexes are strictly increasing
sort(pris.begin(),pris.end());
data = pris;
memset(memo,-1,sizeof(memo));
int best = func(0, data.size()-1);
cout << "Case #" << t+1 << ": " << best << "\n";
}
return 0;
}
Friday, September 4, 2009
Google Code Jam 2009 Qualification Round
Overview:
The GCJ qualification round started smoothly until the input downloading system stopped working. This isn't crucial to the qualification round as participants can just move on to the next problem and submit all the problems at the same time when the system started working again. The number of competitors came close to almost 10,000 international participants - many of which were able to solve at least 1 problem. The problems itself required no specialised algorithmic or mathematical knowledge so they proved to be accessible to most participants. A score of 33 was required to progress to Round 1, so any combination of a correct small input and a large input solution was sufficient enough to qualify.
Links:
Google Code Jam Statistics: http://www.go-hero.net/jam/
Google Code Jam Scoreboard: http://code.google.com/codejam/contest/scoreboard?c=90101
Problem A: Alien Language
Category: Brute Force, String Manipulation
URL: http://code.google.com/codejam/contest/dashboard?c=90101#s=p0
The problem asks us to determine the number of dictionary words (which are guaranteed to be unique) such that it matches the given set of token inputs. There are several ways to solve this, one way is to convert the patterns into regular expressions and run through the dictionary list and compute whether or not the word matches the regex. Programming languages like Java or scripting languages like Perl or Python support this method natively. Converting the given pattern into regex is simple: convert the left brackets into '[' and convert the right brackets into ']'. For example, (ab)(bc)(ca) becomes [ab][bc][ca].
The alternative approach is to keep a set of dictionary words that are correct so far and keep reducing this set based on the constraints imposed by the patterns. For example, given the dictionary list in the sample input:
S1 = {abc, bca, cba, dac, dbc}
Given (ab)(bc)(ca) as the pattern, we start with the first token (ab). So we eliminate all strings/words in S1 which do not have 'a' or 'b' in the first index. This yields the second set:
S2 = {abc, bca}
We proceed to process the second token (bc). Again we eliminate all strings/words in S2 which do not have a 'b' or 'c' as the second letter/index. Since both words in S2 contain this, we don't eliminate any. We simply keep repeating this process until we reach the last index. The answer is simply the size of the set which remains.
Problem B: Watersheds
Category: Depth First Search, Recursion
URL: http://code.google.com/codejam/contest/dashboard?c=90101#s=p1
The problem asks us to determine the different drainage basins given a heightmap as input. There is a set of rules in which the rainfall falls down to which is given in the problem statement. We can go through each cell and do a simple modified depth first search (really just recursion) once we reach the sink, we check whether or not the sink has been labelled. If it hasn't, then we label it with the next smallest unused lower-case character. We then backtrack and assign this character to all the nodes which were used to get to the sink. A simple optimisation is to stop recursing if we reach a node in which has already been assigned a lower-case letter as opposed to waiting until we reach the sink. This isn't required as the constraints are quite low (100x100) in the worst case.
There are some minor implementation details: we have to ensure that we consider adjacent nodes in the correct order (i.e. North, West, East, South). This is easily achieved by a direction array/vector which prioritises the order. This way we don't have to worry about the cases in which the lowest adjacent altitudes are equal, it's implicitly done for us. The constraint of placing the colour so the map is lexicographically smallest can be achieved by greedily filling in the nodes by row-order then by column-order. We are guaranteed to fill a cell with the smallest available character if we start traversing from (x,y) as it will eventually reach a sink. The last note is to not consider nodes that are outside the heightmap (nodes which exist outside the land) as well as not considering adjacent cells which have a height larger or equal to the current cell's height.
Apart from following the given rules - it's a straightforward simulation problem. The constraints are rather lenient, so any decent algorithm should pass within the time limits.
Problem C: Welcome to Code Jam
Category: Recursion, Dynamic Programming
URL: http://code.google.com/codejam/contest/dashboard?c=90101#s=p2
The problem asks us to find the number of occurrences of "welcome to code jam" inside a given text. Take note that it's not necessarily a contiguous subsequence. This concept is best illustrated with a diagram for those unfamiliar:
Take a phrase P ("wel") and a text T ("wweel"). Then there are 4 occurrences:
|w|w|e|e|l| (the text T)
|w| |e| |l| (#1)
|w| | |e|l| (#2)
| |w|e| |l| (#3)
| |w| |e|l| (#4)
Hence your allowed to skip characters in T so long as you use the all the characters in P in order. This easily lends itself to a recursive solution, we just need to keep track of 2 indexes: the index of P and the index of T. This alone let's us to determine how far we are within a particular instance of the problem and lets us determine what next character in P we need to match within T. There are two conditions in which we end the recursion: we have reached the end of P (i.e. this is a valid occurrence of P inside T), we have reached the end of T but there are still characters to process in P (i.e. invalid occurrence of P in T as we haven't finished laying out the characters in P on T). We return 1 and 0 on the respective cases.
This solution will timeout for the large input as the number of possible occurrences skyrockets (this should be evident enough from the problem statement which states 400263727 occurrences of "welcome to code jam" in the small paragraph). The simple optimisation is to use memoisation and cache the indexes so we don't re-compute sub-problems we have already calculated before. Another approach is to convert the memoisation/recurrence relation into a bottom-up dynamic programming algorithm. A minor implementation note is keeping track of the last 4 digits, the safest and easiest is to use modulo arithmetic: we simply mod all our calculations with 10000. Note that it is not sufficient to do this at the end (before outputting) as the calculations may have already overflowed the integer type and modding it by 10000 will just yield the incorrect solution.
The GCJ qualification round started smoothly until the input downloading system stopped working. This isn't crucial to the qualification round as participants can just move on to the next problem and submit all the problems at the same time when the system started working again. The number of competitors came close to almost 10,000 international participants - many of which were able to solve at least 1 problem. The problems itself required no specialised algorithmic or mathematical knowledge so they proved to be accessible to most participants. A score of 33 was required to progress to Round 1, so any combination of a correct small input and a large input solution was sufficient enough to qualify.
Links:
Google Code Jam Statistics: http://www.go-hero.net/jam/
Google Code Jam Scoreboard: http://code.google.com/codejam/contest/scoreboard?c=90101
Problem A: Alien Language
Category: Brute Force, String Manipulation
URL: http://code.google.com/codejam/contest/dashboard?c=90101#s=p0
The problem asks us to determine the number of dictionary words (which are guaranteed to be unique) such that it matches the given set of token inputs. There are several ways to solve this, one way is to convert the patterns into regular expressions and run through the dictionary list and compute whether or not the word matches the regex. Programming languages like Java or scripting languages like Perl or Python support this method natively. Converting the given pattern into regex is simple: convert the left brackets into '[' and convert the right brackets into ']'. For example, (ab)(bc)(ca) becomes [ab][bc][ca].
The alternative approach is to keep a set of dictionary words that are correct so far and keep reducing this set based on the constraints imposed by the patterns. For example, given the dictionary list in the sample input:
S1 = {abc, bca, cba, dac, dbc}
Given (ab)(bc)(ca) as the pattern, we start with the first token (ab). So we eliminate all strings/words in S1 which do not have 'a' or 'b' in the first index. This yields the second set:
S2 = {abc, bca}
We proceed to process the second token (bc). Again we eliminate all strings/words in S2 which do not have a 'b' or 'c' as the second letter/index. Since both words in S2 contain this, we don't eliminate any. We simply keep repeating this process until we reach the last index. The answer is simply the size of the set which remains.
// Google Code Jam 2009 - Qualification Round (Alien Language)
#include <iostream>
#include <vector>
#include <string>
#include <sstream>
#include <algorithm>
#include <stack>
using namespace std;
vector<string> dict;
// determine the number of dictionary words that match the given pattern
int calculate(const vector<string>& tokens,
const vector<string>& remain, int idx) {
if (idx >= tokens.size()) return remain.size();
if (remain.size() == 0) return 0;
string t = tokens[idx];
vector<string> next;
for (int i = 0; i < remain.size(); i++) {
// a valid word given the current idx comparison -
// append it for the next iteration
if (t.find(remain[i][idx]) != string::npos) {
next.push_back(remain[i]);
}
}
// run the next iteration
return calculate(tokens, next, idx+1);
}
int main() {
int L, D, N;
cin >> L >> D >> N;
for (int i = 0; i < D; i++) {
string word; cin >> word;
dict.push_back(word);
}
sort(dict.begin(),dict.end());
// get rid of the trailing newline from using cin
string dump; getline(cin,dump);
for (int i = 0; i < N; i++) {
string line;
getline(cin,line);
vector<string> tokens;
// parse the (xxx) patterns into separate strings
for (int j = 0; j < line.size(); j++) {
if (line[j] == '(') {
string token;
j++;
while (line[j] != ')') {
token += line[j];
j++;
}
tokens.push_back(token);
} else {
string token = string(1,line[j]);
tokens.push_back(token);
}
}
cout << "Case #" << (i+1) << ": " << calculate(tokens, dict, 0) << "\n";
}
return 0;
}
Problem B: Watersheds
Category: Depth First Search, Recursion
URL: http://code.google.com/codejam/contest/dashboard?c=90101#s=p1
The problem asks us to determine the different drainage basins given a heightmap as input. There is a set of rules in which the rainfall falls down to which is given in the problem statement. We can go through each cell and do a simple modified depth first search (really just recursion) once we reach the sink, we check whether or not the sink has been labelled. If it hasn't, then we label it with the next smallest unused lower-case character. We then backtrack and assign this character to all the nodes which were used to get to the sink. A simple optimisation is to stop recursing if we reach a node in which has already been assigned a lower-case letter as opposed to waiting until we reach the sink. This isn't required as the constraints are quite low (100x100) in the worst case.
There are some minor implementation details: we have to ensure that we consider adjacent nodes in the correct order (i.e. North, West, East, South). This is easily achieved by a direction array/vector which prioritises the order. This way we don't have to worry about the cases in which the lowest adjacent altitudes are equal, it's implicitly done for us. The constraint of placing the colour so the map is lexicographically smallest can be achieved by greedily filling in the nodes by row-order then by column-order. We are guaranteed to fill a cell with the smallest available character if we start traversing from (x,y) as it will eventually reach a sink. The last note is to not consider nodes that are outside the heightmap (nodes which exist outside the land) as well as not considering adjacent cells which have a height larger or equal to the current cell's height.
Apart from following the given rules - it's a straightforward simulation problem. The constraints are rather lenient, so any decent algorithm should pass within the time limits.
// Google Code Jam 2009 - Qualification Round (Watersheds)
#include <iostream>
#include <vector>
#include <string>
#include <sstream>
#include <algorithm>
#include <stack>
using namespace std;
int heightmap[111][111]; // matrix to keep track of the input height map
char output[111][111]; // the final set of basins
char comp; // the minimum un-used character component
// directional vectors in preference of N, W, E, S
int dx[] = {-1, 0, 0, 1};
int dy[] = {0, -1, 1, 0};
// the grid size - used for checking boundaries
int szX, szY;
char dfs(int x, int y) {
// search for the lowest altitude given the rules defined
// in the problem statement
int lowest = 20000;
int nx = -1, ny = -1;
for (int k = 0; k < 4; k++) {
int mx = dx[k] + x;
int my = dy[k] + y;
if (mx < 0 || my < 0 || mx >= szX || my >= szY || heightmap[x][y] <=
heightmap[mx][my]) continue;
if (heightmap[mx][my] < lowest) {
lowest = heightmap[mx][my];
nx = mx;
ny = my;
}
}
// sink reached
if (nx < 0) {
if (output[x][y] != 0) return output[x][y];
return output[x][y] = comp++;
}
// backtrack the solution
output[x][y] = dfs(nx,ny);
}
int main() {
int T;
cin >> T;
for (int i = 0; i < T; i++) {
memset(heightmap,0,sizeof(heightmap));
memset(output,0,sizeof(output));
int X, Y;
cin >> X >> Y;
for (int j = 0; j < X; j++) {
for (int k = 0; k < Y; k++) {
int val; cin >> val;
heightmap[j][k] = val;
}
}
// reset the dfs parameters
comp = 'a';
szX = X;
szY = Y;
for (int j = 0; j < X; j++) {
for (int k = 0; k < Y; k++) {
// if we have assigned a character to the cell - don't need to dfs
if (output[j][k]) continue;
dfs(j,k);
}
}
// output the assigned basins
cout << "Case #" << (i+1) << ":\n";
for (int j = 0; j < X; j++) {
for (int k = 0; k < Y; k++) {
if (k != 0) cout << " ";
cout << output[j][k];
}
cout << "\n";
}
}
return 0;
}
Problem C: Welcome to Code Jam
Category: Recursion, Dynamic Programming
URL: http://code.google.com/codejam/contest/dashboard?c=90101#s=p2
The problem asks us to find the number of occurrences of "welcome to code jam" inside a given text. Take note that it's not necessarily a contiguous subsequence. This concept is best illustrated with a diagram for those unfamiliar:
Take a phrase P ("wel") and a text T ("wweel"). Then there are 4 occurrences:
|w|w|e|e|l| (the text T)
|w| |e| |l| (#1)
|w| | |e|l| (#2)
| |w|e| |l| (#3)
| |w| |e|l| (#4)
Hence your allowed to skip characters in T so long as you use the all the characters in P in order. This easily lends itself to a recursive solution, we just need to keep track of 2 indexes: the index of P and the index of T. This alone let's us to determine how far we are within a particular instance of the problem and lets us determine what next character in P we need to match within T. There are two conditions in which we end the recursion: we have reached the end of P (i.e. this is a valid occurrence of P inside T), we have reached the end of T but there are still characters to process in P (i.e. invalid occurrence of P in T as we haven't finished laying out the characters in P on T). We return 1 and 0 on the respective cases.
This solution will timeout for the large input as the number of possible occurrences skyrockets (this should be evident enough from the problem statement which states 400263727 occurrences of "welcome to code jam" in the small paragraph). The simple optimisation is to use memoisation and cache the indexes so we don't re-compute sub-problems we have already calculated before. Another approach is to convert the memoisation/recurrence relation into a bottom-up dynamic programming algorithm. A minor implementation note is keeping track of the last 4 digits, the safest and easiest is to use modulo arithmetic: we simply mod all our calculations with 10000. Note that it is not sufficient to do this at the end (before outputting) as the calculations may have already overflowed the integer type and modding it by 10000 will just yield the incorrect solution.
// Google Code Jam 2009 - Qualification Round (Welcome to Code Jam)
#include <iostream>
#include <vector>
#include <string>
#include <sstream>
#include <algorithm>
#include <stack>
using namespace std;
int dp[511][21]; // memo cache
string lineR; // used to keep track of the line we are given
string textR = "welcome to code jam"; // the phrase we are looking for
int calc(int lineIdx, int textIdx) {
// base cases
if (textIdx >= textR.size()) return 1;
if (lineIdx >= lineR.size()) return 0;
// seen it before - return the previously computed value
if (dp[lineIdx][textIdx] != -1) return dp[lineIdx][textIdx];
dp[lineIdx][textIdx] = 0;
for (int i = lineIdx; i < lineR.size(); i++) {
// valid transition - add all the combinations
if (lineR[i] == textR[textIdx]) {
dp[lineIdx][textIdx] = (dp[lineIdx][textIdx] + calc(i+1, textIdx+1)) % 10000;
}
}
return dp[lineIdx][textIdx] % 10000;
}
int main() {
int N;
cin >> N;
// get rid of the trailing newline character
string dump; getline(cin,dump);
for (int i = 0; i < N; i++) {
// reset the memo cache
memset(dp,-1,sizeof(dp));
string line; getline(cin,line);
lineR = line;
int v = calc(0,0);
// correct the output into the required 4 digits
ostringstream oss; oss << v;
string output = oss.str();
while (output.size() < 4) output = "0" + output;
cout << "Case #" << (i+1) << ": " << output << "\n";
}
return 0;
}
Thursday, July 30, 2009
Google Code Jam and DP Tutorial
With Google Code Jam 2009 finally being announced last week - it is now a good time to start practicing. I've been warming up with some Topcoder problems which I've missed since the recent contests have been held early in the morning :)
Google Code Jam: http://code.google.com/codejam/
On a side note, I've written a dynamic programming tutorial early last year aimed towards newer competitors who are unfamiliar with the concept. Unfortunately I no longer have a copy of the TeX file which means I can't edit it for corrections.
DP Tutorial can be accessed here: http://sites.google.com/site/wilanw/DP1.pdf
Google Code Jam: http://code.google.com/codejam/
On a side note, I've written a dynamic programming tutorial early last year aimed towards newer competitors who are unfamiliar with the concept. Unfortunately I no longer have a copy of the TeX file which means I can't edit it for corrections.
DP Tutorial can be accessed here: http://sites.google.com/site/wilanw/DP1.pdf
Subscribe to:
Posts (Atom)