Sergii Dymchenko

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.

Comments

comments powered by Disqus