Facebook Hacker Cup 2015 Qualification Round: full score with Prolog and Python
Facebook Hacker Cup 2015 Qualification Round has just ended. I've got a full score using Prolog for the first two problems and Python for the third one, and I want to share my solutions. All code in this article is the actual code I wrote and submitted during the round.
The easiest problem of the round, Cooking the Books, asks to find the largest and the smallest number that can be obtained from a given integer number by swapping a pair of the digits once. Swapping the first digit of a number with 0 is not allowed.
The given number can be from 0 to 999999999, so a brute-force approach of trying all possible swaps is feasible. Prolog is a good language for coding brute-force algorithms, and here is my B-Prolog program for the problem:
% B-Prolog 8.1 - http://www.probp.com/ % Usage: % sed 's/^ */[/; s/ *$/]./; s/ \+/, /g' in-file | bp -g "cl('CookingTheBooks.pl'),main,halt" -l | tail -n +5 > out-file swap_digit(A, B) :- append(Begin, [X | Middle], [Y | End], A), append(Begin, [Y | Middle], [X | End], B), \+ nth1(1, B, 0'0). :- table smallest(+, min). smallest(N, NewN) :- ( NewN = N ; swap_digit(N, NewN) ). :- table largest(+, max). largest(N, NewN) :- ( NewN = N ; swap_digit(N, NewN) ). do_case(Case_num, N) :- number_codes(N, Nstr), smallest(Nstr, Smallest), largest(Nstr, Largest), format("Case #~w: ~s ~s\n", [Case_num, Smallest, Largest]). main :- read([T]), foreach(Case_num in 1..T, [N], ( read([N]), do_case(Case_num, N) )).
The program assumes that the input is "prologified" with the sed
script given in the comments
– every input line becomes a list in a Prolog format and ends with a full stop.
This simplifies reading the input in Prolog.
The swap_digit
predicate defines a relation between a given number A
(in a list of digits form) and a number B
with one pair of digits X
and Y
swapped
using the B-Prolog's append
predicate.
Line 8 states that the first digit of the B
must not be 0. For details of Prolog syntax see Learn Prolog Now!.
The smallest
and largest
predicates state that the possible new number is either the old number itself, or the old number with a pair of digits swapped.
B-Prolog's tabling mode declarations make smallest
return the minimum of all possible new numbers, and largest
– the maximum.
Usage of the tabling simplifies the code by making loops for finding minimum and maximum implicit.
The second problem, New Year's Resolution, asks to find is it possible to get exactly specified amount of nutrients by selecting food items from a given list. Every food item in the list has a specified amount of each nutrient, and every food item can be used exactly 0 or 1 times. Every test case has a maximum of 20 food items in the list, and there are at most 20 test cases in the input.
This problem is a variant of the Knapsack problem. Also the problem can be treated as a simplified version of the diet problem (see Diet problem in Picat).
I modelled and solved the problem as a integer linear programming problem using ECLiPSe CLP:
% ECLiPSe 6.1 #194 - http://www.eclipseclp.org/ % Usage: % sed 's/^ */[/; s/ *$/]./; s/ \+/, /g' in-file | eclipse -b NewYearsResolution.ecl -e main > out-file :- set_stream(log_output, stderr). :- set_stream(warning_output, stderr). :- lib(eplex). model(Gp, Gc, Gf, Foods) :- length(Foods, N), length(Xs, N), Xs $:: 0..1, integers(Xs), ( foreach(Xi, Xs), foreach(Food, Foods), foreach(Pi, Ps), foreach(Ci, Cs), foreach(Fi, Fs) do [P, C, F] = Food, Pi $= Xi * P, Ci $= Xi * C, Fi $= Xi * F ), integers([Ps, Cs, Fs]), Gp $= sum(Ps), Gc $= sum(Cs), Gf $= sum(Fs). find :- eplex_solver_setup(max(0)), eplex_solve(_), eplex_cleanup. do_case(Case_num, Gp, Gc, Gf, Foods) :- printf("Case #%w: ", [Case_num]), ( model(Gp, Gc, Gf, Foods), find -> writeln("yes") ; writeln("no") ). main :- read([T]), ( for(Case_num, 1, T) do read([Gp, Gc, Gf]), read([N]), ( for(_I, 1, N), foreach(Food, Foods) do read(Food) ), do_case(Case_num, Gp, Gc, Gf, Foods) ).
This solution uses the same sed
script to "prologify" the input as the previous one.
The model uses Xs
– a list of 0..1 variables – to specify if a particular food item is used.
For every nutrient the required amount (Gp
, Gc
, Gf
)
must be the sum of amount of this nutrient in a particular food item (P
, C
, F
) multiplied by the corresponding element of the Xs
.
eplex_solver_setup(max(0))
– "maximizing" a constant – means that any feasible solution is good enough, since it's not an optimization problem.
See eplex library documentation for more info about linear programming in ECLiPSe,
and http://www.hakank.org/eclipse/ for lots of ECLiPSe CLP examples (some of them use eplex
).
The third problem, Laser Maze, asks to find the length of a shortest path (if it exists) from a start cell to a goal in a given maze guarded by laser turrets.
Symbols for the maze elements (quoted from the problem statement):
. (empty space) # (wall) S (starting position) G (goal) < > ^ v (laser turrets)
Moving is allowed in 4 directions (no diagonal moves). Walls and turrets are unpassable. After each move, every laser turret rotates clockwise and shoots a laser beam in the direction it faces. Laser beams cannot go through walls and turrets. Being in the cell hit by a laser beam is not allowed.
The maximum maze size is 100 × 100.
Here is my Python (I used version 2.7.5+) program that utilizes breadth-first search to find the solution:
from collections import deque def preprocess(grid): R = len(grid) C = len(grid[0]) obstacles = set([]) dangers = set([]) for i in range(R): for j in range(C): cell = grid[i][j] if cell == '#': obstacles.add((i, j)) if cell == 'S': start = (i, j) if cell == 'G': goal = (i, j) if cell in "<^>v": obstacles.add((i, j)) # to the left for b in range(j - 1, -1, -1): if grid[i][b] in "#<^>v": break dangers.add((i, b, "<v>^".find(cell))) # to the right for b in range(j + 1, C, +1): if grid[i][b] in "#<^>v": break dangers.add((i, b, ">^<v".find(cell))) # to the top for a in range(i - 1, -1, -1): if grid[a][j] in "#<^>v": break dangers.add((a, j, "^<v>".find(cell))) # to the bottom for a in range(i + 1, R, +1): if grid[a][j] in "#<^>v": break dangers.add((a, j, "v>^<".find(cell))) return (R, C, start, goal, obstacles, dangers) def bfs(R, C, start, goal, obstacles, dangers): done = {(start[0], start[1], 0): 0} frontier = deque([(start[0], start[1], 0)]) while frontier: i, j, stepmod4 = frontier.popleft() if (i, j) == goal: return done[(i, j, stepmod4)] for (di, dj) in [(-1, 0), (+1, 0), (0, -1), (0, +1)]: new_i = i + di new_j = j + dj new_stepmod4 = (stepmod4 + 1) % 4 if (new_i, new_j, new_stepmod4) in done: continue if new_i < 0 or new_i >= R or new_j < 0 or new_j >= C: continue if (new_i, new_j) in obstacles: continue if (new_i, new_j, new_stepmod4) in dangers: continue done[(new_i, new_j, new_stepmod4)] = done[(i, j, stepmod4)] + 1 frontier.append((new_i, new_j, new_stepmod4)) return None def do_case(case_num, grid): (R, C, start, goal, obstacles, dangers) = preprocess(grid) result = bfs(R, C, start, goal, obstacles, dangers) if result: print "Case #%d: %d" % (case_num, result) else: print "Case #%d: impossible" % case_num def main(): T = int(raw_input()) for case_num in range(1, T + 1): R, C = map(int, raw_input().split()) grid = [] for i in range(R): grid.append(raw_input().strip()) do_case(case_num, grid) if __name__ == "__main__": main()
The preprocess
function constructs sets of obstacles and dangerous (hit by laser beams) cells from the input.
The obstacles
set contains pairs of coordinates of walls and turrets.
The dangers
set contains tuples of coordinates and step numbers modulo 4 of every dangerous cell.
bfs
uses double-ended queue data structure from the collections
module of the Python standard library to perform
breadth-first search from the start to the goal, respecting obstacles
and dangers
.
During the contest, before writing the Python solution, I coded a solution using B-Prolog and its planning library. The B-Prolog program used iterative deepening depth-first search instead of breadth-first search, and would be too slow for some maximum-sized inputs. After the contest I tried the B-Prolog program on the input I was given by the contest server, and my program found the correct results in no time. The given input was by far not the hardest one: there were only 20 test cases instead of 100 promised in the problem statement, and all of the test cases were much smaller than allowed 100 × 100.
Overall, I really enjoyed the qualification round, and I'm looking forward to the Hacker Cup Online Round 1 held in a week.