8
$\begingroup$

This is a problem that was presented to me through the google foobar challenge, but my time has since expired and they had decided that I did not complete the problem. I suspect a possible bug on their side, because I passed 9/10 test cases (which are hidden from me) but I'm unsure. I'd really like to know where or if I made a mistake, so I'll ask the problem here.

The problem is as follows: you are standing in a room with integer dimensions, where the left wall exists at $x=0$, and the right wall exists at $x=x_0$. Similarly, the bottom wall exists at $y=0$ and the top wall exists at $y=y_0$. Within this room, you and another individual (which we call a guard) stand on the integer lattice such that your coordinates are at $[x_{s}, y_{s}]$, and the guard stands at $[x_g, y_g]$, where

$$ 0<x_s<x_0 \\ 0<x_g<x_0 \\ 0<y_s<y_0 \\ 0<y_g<y_0 \\ y_s \neq y_g~\text{or}~x_s \neq x_g \\ $$

that is, neither you nor the guard is standing on a wall, and you occupy distinct positions in the room. If you have a bullet which can travel a total distance $d$, then determine the number of distinct directions in which you can fire the bullet such that it hits the other individual, travels a distance less than or equal to $d$, and it does not hit yourself in the process. This is written as a function:

solution(room_dimensions, your_position, guard_position, d)

What I had noticed in my solution was that the allowed directions can be written as a vector of the total distance traveled by the bullet in a specific dimension, where a negative sign indicates initial movement towards $x=0$ or $y=0$, and a positive sign indicates initial movement towards $x=x_0$ or $y=y_0$. Let's give an example of what this means.

Suppose the room has dimensions $[2, 3]$ and you stand at $[1, 2]$ and the guard stands at $[1, 1]$. Then, of course the bullet could go straight down: $[0, -1]$ is a valid bearing. However, we could bounce the bullet off of the left wall, and then have the bullet hit the guard: $[-2, -1]$. That $-2$ is valid because the total distance traveled is $2$: $1$ unit is traveled in moving from your position to the left wall, and an additional $1$ from moving back towards the guard. It is negative because we are moving towards the left wall $x=0$. This can be extended the any number of bounces off of each wall, and each dimension can be considered independently. So, for each dimension, I generate a list of the allowed distances (with sign indicating direction) which results in the bullet arriving at the same coordinate as the guard in that dimension.

If we generate the aforementioned lists (allowed_x, allowed_y) for the $x$ and $y$ dimensions, then $[X, Y]$ $X\in$ allowed_x, $Y\in$ allowed_y is a valid bearing which will take the bullet from you to the guard, with two created problems: 1) The bullet may first pass through you, and 2) Some of these bearings may point in the same direction and travel different distances.

If we determine all of the directions that result in you being hit by the bullet, we can see if a parallel direction exists in the list of directions which result in the guard being hit, and if it does and the path to you being hit is shorter, then that is not a valid direction as it requires the bullet to pass through you. This resolves 1).

2) Is resolved by checking to see if directions are parallel before counting them, and if they are, we record the shorter of the two distances. In this way, we only get distinct directions.

As I said, my code works for 9/10 tests, which are hidden from me. As a result, it is likely that the 1/10 case is specifically chosen to test an extreme which, at random, you would not encounter. I thoroughly tested my code, so I don't believe such an extreme exits, but I would be very relieved if someone could point out my oversight. Cheers!

I've attached my python 2.7.13 code below if anyone wants to get very involved.

from fractions import gcd 
from random import randint 

def get_1D_bearings(t, m, g, distance):
    '''
    Returns a list of the total distance travelled in 1D which results in the beam arriving at the guard's t-coordinate.

    Parameters:
        t(int): The length of this dimension.
        m(int): The t-coodinate of the shooter (me). 
        g(int): The t-coordinate of the guard.
        distance(int): The maximum allowed distance that the beam may travel. 
    '''
    i = 0 
    bearings = [] 
    path = g - m # Direct path from the shooter to the guard with no bounces. 
    if abs(path) <= distance:
        bearings.append(path)
    while True:
        # Initial bounce off of the positive wall and final bounce off of the positive wall. 
        path = (t - m) + (t-g) + 2*i*t 
        if abs(path) <= distance:
            bearings.append(path)
        # Initial bounce off of the negative wall and final bounce off of the negative wall. 
        path = - (m+g+2*i*t)
        if abs(path) <= distance:
            bearings.append(path)
        # Initial bounce off of the positive wall and final bounce off of the negative wall. 
        path = (t - m) + g + (2*i+1)*t 
        if abs(path) <= distance:
            bearings.append(path)
        # Initial bounce off of the negative wall and final bounce off of the positive wall.
        path = - ( m + (t - g) + (2*i+1)*t)
        if abs(path) <= distance:
            bearings.append(path)
        else:
            break 
        i += 1
    return bearings 

def are_parallel(a, b):
    '''
    Returns if the bearings given by a and b are parallel vectors. 

    Parameters:
        a(array-like): A 2D-array of integers. 
        b(array-like): A 2D-array of integers. 
    '''
    x1, y1 = a
    x2, y2 = b 
    div1 = abs(gcd(x1, y1))
    div2 = abs(gcd(x2, y2) ) 
    if div1 == 0 or div2 ==0:
        if not div1 == 0 and div2 == 0:
            return False 
        elif (x1 == 0 and x2 == 0) and (y1 // abs(y1) == y2 // abs(y2)):
            return True 
        elif (y1 == 0 and y2 ==0) and (x1 // abs(x1) == x2 // abs(x2)):
            return True 
        else:
            return False 
    else:
        if x1 // div1 == x2 // div2 and y1 // div1 == y2 // div2:
            return True 
        else:
            return False 

class VectorsNotParallel(Exception):
    '''Raise this exception when handling vectors which are assumed to be parallel but are not.'''
    def __init__(self):
        pass 

def get_shorter_bearing(a, b):
    '''
    Returns the shorter vector of a and b. If they are not parallel, raises VectorsNotParallel.

    Parameters:
        a(array-like): A 2D-array of integers indicating a direction. 
        b(array-like): A 2D-array of integers indicating a direction. 
    '''
    if not are_parallel(a, b):
        raise VectorsNotParallel("These bearings point in different directions: " + str(a) + " and " + str(b))
    x1, y1 = a
    x2, y2 = b
    if x1 == 0:
        if abs(y1) < abs(y2):
            return a
        else:
            return b 
    if y1 == 0:
        if abs(x1) < abs(x2):
            return a
        else:
            return b           
    div1 = abs(gcd(x1, y1))
    div2 = abs(gcd(x2, y2) ) 
    if div1 < div2:
        return a 
    else:
        return b    

def get_all_bearings(dimensions, your_position, guard_position, distance):
    '''
    Combines the allowed distances from each of the two dimensions to generate a list of all of the 
    allowed directions that can be shot in which take a beam from your_position to guard_position 
    while not travelling further than the provided distance. Note that some of these directions include
    passing through your_position. 

    Parameters:
        dimensions(array-like): A 2D-array of integers indicating the size of the room.
        your_position(array-like): A 2D-array of integers indicating your position in the room.
        guard_position(array-like): A 2D-array of integers indicating the guard's position in the room.
        distance(int): An integer indicating the maximum distance the beam can travel. 

    Returns:
        bearings(array-like): An array of 2D-arrays indicating the bearings which move the beam from your_position
                              to guard_position.
    '''
    dx, dy=  dimensions
    sx, sy = your_position
    gx, gy = guard_position
    allowed_x = get_1D_bearings(dx, sx, gx, distance)
    allowed_y = get_1D_bearings(dy, sy, gy, distance) 
    bearings = [] 
    for x in allowed_x:
        for y in allowed_y:
            if x**2 + y**2 < 1 or x**2 + y**2 > distance **2:
                continue 
            res = [x, y]
            append = True  # Do we need to append to the list of bearings or just update an existing one
            for bearing in bearings:
                if are_parallel(res, bearing):
                    append = False 
                    res_2 = get_shorter_bearing(res, bearing)
                    bearing[0] = res_2[0]
                    bearing[1] = res_2[1] 
            if append:
                bearings.append(res) 
    return bearings 

def count_friendly_fires(friendly_bearings, guard_bearings):
    '''
    Returns the number of bearings which result in the guard being hit only after the beam 
    passes through the shooter (which is not allowed). 

    Parameters:
        friendly_bearings(array-like): An array of 2D arrays which indicate bearings that reach the shooter. 
        guard_bearings(array-like): An array of 2D arrays which indicate bearings that reach the guard. 
    '''
    count = 0 
    for f_bearing in friendly_bearings:
        for g_bearing in guard_bearings:
            if are_parallel(f_bearing, g_bearing):
                if get_shorter_bearing(f_bearing, g_bearing) == f_bearing:
                    print(f_bearing, g_bearing)
                    count += 1 
    return count 

def solution(dimensions, your_position, guard_position, distance):
    '''
    Returns the number of distinct directions that take a bullet from your_position to 
    guard_position within the allowed distance. 

    Parameters:
        dimensions(array-like): A 2D-array of integers indicating the size of the room.
        your_position(array-like): A 2D-array of integers indicating your position in the room.
        guard_position(array-like): A 2D-array of integers indicating the guard's position in the room.
        distance(int): An integer indicating the maximum distance the beam can travel. 
    '''
    guard_hitting_bearings = get_all_bearings(dimensions, your_position, guard_position, distance)
    self_hitting_bearings = get_all_bearings(dimensions, your_position, your_position, distance)
    count = count_friendly_fires(self_hitting_bearings, guard_hitting_bearings)
    return len(guard_hitting_bearings) - count 
$\endgroup$
9
  • 1
    $\begingroup$ I think you mean $x_s \neq x_g \lor y_s \neq y_g$. It is not necessary for both to be true in order to have different positions in the room. $\endgroup$
    – Paul
    Jun 1, 2020 at 2:42
  • 3
    $\begingroup$ Just a side remark: one alternative viewpoint, which is oft-used in polygonal billiards, is to "unfold the table": see e.g. these slides. In this way, if you treat the walls as mirrors, you in this room would "see" many copies of the guard in the reflections of the room (as well as yourself), which correspond to the points in the plane obtained by applying a sequence of reflections in the walls, to the original guard. This allows you to consider straight trajectories (i.e. counting points) rather than bounces $\endgroup$
    – Good Boy
    Jun 1, 2020 at 18:34
  • $\begingroup$ Also, is it clear what happens if you shoot the corner? $\endgroup$
    – Good Boy
    Jun 1, 2020 at 20:47
  • 1
    $\begingroup$ In your example, the bullet travels a distance $\sqrt 5$, not $2$, before it hits the guard. It travels two units horizontally and one unit vertically. I didn't check if your code makes this mistake, but it could be that the case you failed is because the the range of the bullet is just a bit short. $\endgroup$ Jun 2, 2020 at 2:54
  • 1
    $\begingroup$ @RossMillikan That's correct, but I do actually account for this. The condition is written in the code that $x^2+y^2\leq d^2$, because I'd rather not work with square roots in a problem with integers. I'll also add that I used specific examples and watched for errors like this: I printed the distances and bearings for numerous cases and they were all less than or equal to $d$: I specifically checked the case where a distance of exactly $d$ was covered. $\endgroup$
    – Kraigolas
    Jun 2, 2020 at 4:33

2 Answers 2

4
+100
$\begingroup$

I have inspected your code thoroughly and cannot find any issue; it seems entirely robust to me, at least in terms of correctness. However, I am aware that Foobar imposes time limits on problems; I suspect that your code failed that one test case because it was too slow.

To be precise, let us define $B$ as the maximum product of the sizes of allowed_x and allowed_y (in a vague sense, the maximum of the sizes of guard_hitting_bearings and self_hitting_bearings, although typically it will be much larger). We'll investigate the runtimes of each of the functions in your code, and provide a vague asymptotic bound.

Firstly, are_parallel is clearly $O(1)$, so we can just lump it in with the other constant time operations. The same goes for get_shorter_bearing.

Now, for get_all_bearings, we have an outer pair of loops that iterates through all $O(B)$ possibilities; and for each of the actually valid ones, it iterates through the entire list so far, for a total complexity of $O(B^2)$.

Similarly, for count_friendly_fires, we iterate over all pairs of bearings between friendly_bearings and guard_bearings, and thus again have a complexity of $O(B^2)$.

Overall, this code is $O(B^2)$ (plus some other small stuff we don't particularly care about), and we can significantly improve on that. Additionally, and importantly, this quadratic upper bound can be forced by a sufficiently bad input; for instance, setting your initial position to $(1, 1)$, the guard position to $(2, 2)$, and the room dimensions to $p \times p$ for some prime $p$ should force the sizes of guard_hitting_bearings and self_hitting_bearings to be at most a constant factor smaller than the product of the sizes of allowed_x and allowed_y. I suspect the last test case was probably something to this effect, designed to break solutions which were too slow (you probably passed the other test cases because they all had lots of parallel vectors, so the sizes reduced significantly). In any case, it is clear that improvements will have to be made to the two $O(B^2)$ functions listed above.

Looking at your code, a pattern emerges in the places that degenerate into $O(B^2)$: they all involve a naive parallel vector check, so optimising this is the key to making the code run faster. The key idea is as follows. Let $x \parallel y$ if and only if are_parallel(x, y). This is an equivalence relation ($x \parallel x$ for all $x$, and $x \parallel y$ and $y \parallel z$ implies $x \parallel z$). Now define $r(v)$ to be the vector with smallest magnitude such that $r(v) \parallel v$; it is easy to see that this vector is uniquely defined, and a function to compute $r(v)$ in $O(1)$ can be obtained by a simple modification of are_parallel. Now, since $r(v)$ is uniquely defined, we have the fact $$ x \parallel y \iff r(x) = r(y). $$ This is the key to optimising your solution. In particular, instead of arrays, we can use a dictionary indexed by $r(v)$.

Instead of the $O(B)$ inner loop in get_all_bearings to check if the new vector is parallel with anything already in bearing, we simply check the element at bearing[r(v)] (if such an element exists); this reduces the loop to a simple $O(1)$ dictionary lookup, and thus overall get_all_bearings is reduced to $O(B)$. Similarly, count_friendly_fires can iterate over all key-value pairs in friendly_bearings, and simply look up the element of guard_bearings parallel to the current key (if such an element exists); again the $O(B)$ inner loop is reduced to $O(1)$, making the function $O(B)$ overall. Thus, with this simple modification, your code can be made to run quite significantly faster.

Paul has some nice, readable code implementing this idea in another answer. His $r(v)$ is called get_direction, for reference.

$\endgroup$
3
  • $\begingroup$ I've just tested my code with the parameters: $([3, 3], [1, 1], [2, 2], 1e6)$ and sure enough, you're correct. The maximum distance was either 1e6 or it was 1e9, I'm unsure. I actually had a dictionary method, because I too noticed that this would take a long time to complete, but I abandoned it because the test I was failed was the first of eight hidden tests, and was the only test that has an instance of the bullet passing through the shooter (except the case in my question, which was an unhidden test with only a single self hit). Thus I opted for more clearly written code. $\endgroup$
    – Kraigolas
    Jun 5, 2020 at 21:03
  • 1
    $\begingroup$ ah, that's unfortunate. glad that the cause has been identified at least. $\endgroup$
    – hdighfan
    Jun 5, 2020 at 21:05
  • 1
    $\begingroup$ I made some small tweaks to my dictionary-style test when writing this new code, and didn't translate them over because I, naively, didn't think it mattered. In some cases, the code would exit in as quickly as half a second to say I'd failed a test, but for other challenges it could take as long as 30 seconds and I'd still pass: the time it takes to review the solution is no indicator of if your solution is taking too long. Great answer, thanks for your help! $\endgroup$
    – Kraigolas
    Jun 5, 2020 at 21:05
2
$\begingroup$

I made my own implementation based on your idea and added code to compare the results between your and my implementation. They are the same. So your code is probably fine.

Here is the code, tested on Python 3.7:

import math

################################### Original solution ##########################################

def get_1D_bearings(t, m, g, distance):
    '''
    Returns a list of the total distance travelled in 1D which results in the beam arriving at the guard's t-coordinate.

    Parameters:
        t(int): The length of this dimension.
        m(int): The t-coodinate of the shooter (me).
        g(int): The t-coordinate of the guard.
        distance(int): The maximum allowed distance that the beam may travel.
    '''
    i = 0
    bearings = []
    path = g - m # Direct path from the shooter to the guard with no bounces.
    if abs(path) <= distance:
        bearings.append(path)
    while True:
        # Initial bounce off of the positive wall and final bounce off of the positive wall.
        path = (t - m) + (t-g) + 2*i*t
        if abs(path) <= distance:
            bearings.append(path)
        # Initial bounce off of the negative wall and final bounce off of the negative wall.
        path = - (m+g+2*i*t)
        if abs(path) <= distance:
            bearings.append(path)
        # Initial bounce off of the positive wall and final bounce off of the negative wall.
        path = (t - m) + g + (2*i+1)*t
        if abs(path) <= distance:
            bearings.append(path)
        # Initial bounce off of the negative wall and final bounce off of the positive wall.
        path = - ( m + (t - g) + (2*i+1)*t)
        if abs(path) <= distance:
            bearings.append(path)
        else:
            break
        i += 1
    return bearings

def are_parallel(a, b):
    '''
    Returns if the bearings given by a and b are parallel vectors.

    Parameters:
        a(array-like): A 2D-array of integers.
        b(array-like): A 2D-array of integers.
    '''
    x1, y1 = a
    x2, y2 = b
    div1 = abs(math.gcd(x1, y1))
    div2 = abs(math.gcd(x2, y2) )
    if div1 == 0 or div2 ==0:
        if not div1 == 0 and div2 == 0:
            return False
        elif (x1 == 0 and x2 == 0) and (y1 // abs(y1) == y2 // abs(y2)):
            return True
        elif (y1 == 0 and y2 ==0) and (x1 // abs(x1) == x2 // abs(x2)):
            return True
        else:
            return False
    else:
        if x1 // div1 == x2 // div2 and y1 // div1 == y2 // div2:
            return True
        else:
            return False

class VectorsNotParallel(Exception):
    '''Raise this exception when handling vectors which are assumed to be parallel but are not.'''
    def __init__(self):
        pass

def get_shorter_bearing(a, b):
    '''
    Returns the shorter vector of a and b. If they are not parallel, raises VectorsNotParallel.

    Parameters:
        a(array-like): A 2D-array of integers indicating a direction.
        b(array-like): A 2D-array of integers indicating a direction.
    '''
    if not are_parallel(a, b):
        raise VectorsNotParallel("These bearings point in different directions: " + str(a) + " and " + str(b))
    x1, y1 = a
    x2, y2 = b
    if x1 == 0:
        if abs(y1) < abs(y2):
            return a
        else:
            return b
    if y1 == 0:
        if abs(x1) < abs(x2):
            return a
        else:
            return b
    div1 = abs(math.gcd(x1, y1))
    div2 = abs(math.gcd(x2, y2) )
    if div1 < div2:
        return a
    else:
        return b

def get_all_bearings(dimensions, your_position, guard_position, distance):
    '''
    Combines the allowed distances from each of the two dimensions to generate a list of all of the
    allowed directions that can be shot in which take a beam from your_position to guard_position
    while not travelling further than the provided distance. Note that some of these directions include
    passing through your_position.

    Parameters:
        dimensions(array-like): A 2D-array of integers indicating the size of the room.
        your_position(array-like): A 2D-array of integers indicating your position in the room.
        guard_position(array-like): A 2D-array of integers indicating the guard's position in the room.
        distance(int): An integer indicating the maximum distance the beam can travel.

    Returns:
        bearings(array-like): An array of 2D-arrays indicating the bearings which move the beam from your_position
                              to guard_position.
    '''
    dx, dy=  dimensions
    sx, sy = your_position
    gx, gy = guard_position
    allowed_x = get_1D_bearings(dx, sx, gx, distance)
    allowed_y = get_1D_bearings(dy, sy, gy, distance)
    bearings = []
    for x in allowed_x:
        for y in allowed_y:
            if x**2 + y**2 < 1 or x**2 + y**2 > distance **2:
                continue
            res = [x, y]
            append = True  # Do we need to append to the list of bearings or just update an existing one
            for bearing in bearings:
                if are_parallel(res, bearing):
                    append = False
                    res_2 = get_shorter_bearing(res, bearing)
                    bearing[0] = res_2[0]
                    bearing[1] = res_2[1]
            if append:
                bearings.append(res)
    return bearings

def count_friendly_fires(friendly_bearings, guard_bearings):
    '''
    Returns the number of bearings which result in the guard being hit only after the beam
    passes through the shooter (which is not allowed).

    Parameters:
        friendly_bearings(array-like): An array of 2D arrays which indicate bearings that reach the shooter.
        guard_bearings(array-like): An array of 2D arrays which indicate bearings that reach the guard.
    '''
    count = 0
    for f_bearing in friendly_bearings:
        for g_bearing in guard_bearings:
            if are_parallel(f_bearing, g_bearing):
                if get_shorter_bearing(f_bearing, g_bearing) == f_bearing:
                    #print(f_bearing, g_bearing)
                    count += 1
    return count

def solution(dimensions, your_position, guard_position, distance):
    '''
    Returns the number of distinct directions that take a bullet from your_position to
    guard_position within the allowed distance.

    Parameters:
        dimensions(array-like): A 2D-array of integers indicating the size of the room.
        your_position(array-like): A 2D-array of integers indicating your position in the room.
        guard_position(array-like): A 2D-array of integers indicating the guard's position in the room.
        distance(int): An integer indicating the maximum distance the beam can travel.
    '''
    guard_hitting_bearings = get_all_bearings(dimensions, your_position, guard_position, distance)
    self_hitting_bearings = get_all_bearings(dimensions, your_position, your_position, distance)
    count = count_friendly_fires(self_hitting_bearings, guard_hitting_bearings)
    return len(guard_hitting_bearings) - count


################################### My solution ##########################################

def get_range (a, b, max_distance):
    """
    Gets values of a + bk for integers k such that -max_distance <= a + bk <= max_distance.
    :param a: integer
    :param b: positive integer
    :param max_distance: non-negative integer
    """
    return list(range((a + max_distance) % b - max_distance, max_distance + 1, b))

def get_1d_list (size, source, to, max_distance):
    return get_range(to - source, 2*size, max_distance) + get_range(-source - to, 2*size, max_distance)

def get_direction (dx, dy):
    gcd = math.gcd(dx, dy)
    return dx // gcd, dy // gcd

def get_direction_distance_map (dx_list, dy_list, max_squared_distance):
    """Returns a map which has directions as key and squared distances as value"""
    map = {}
    for dx in dx_list:
        for dy in dy_list:
            sd = dx*dx + dy*dy
            if 0 < sd <= max_squared_distance:
                dir = get_direction(dx, dy)
                if sd < map.get(dir, max_squared_distance + 1):
                    map[dir] = sd
    return map

def my_solution (dimensions, your_position, guard_position, distance):
    self_x = get_1d_list(dimensions[0], your_position[0], your_position[0], distance)
    self_y = get_1d_list(dimensions[1], your_position[1], your_position[1], distance)
    guard_x = get_1d_list(dimensions[0], your_position[0], guard_position[0], distance)
    guard_y = get_1d_list(dimensions[1], your_position[1], guard_position[1], distance)
    map_self = get_direction_distance_map(self_x, self_y, distance*distance)
    map_guard = get_direction_distance_map(guard_x, guard_y, distance*distance)

    # Remove friendly fires
    for dir, sd_self in map_self.items():
        sd_guard = map_guard.get(dir)
        if sd_guard is not None and sd_self < sd_guard:
            del map_guard[dir]

    return len(map_guard)


################################### Test code ##########################################

for w in range(2, 8):
    print(f"Testing width {w}")
    for d in range(16):
        for h in range(2, 8):
            for xs in range(1, w):
                for ys in range(1, h):
                    for xg in range(1, w):
                        for yg in range(1, h):
                            if xs != xg or ys != yg:
                                s1 = solution([w, h], [xs, ys], [xg, yg], d)
                                s2 = my_solution([w, h], [xs, ys], [xg, yg], d)
                                if s1 != s2:
                                    print(w, h, xs, ys, xg, yg)
$\endgroup$
1
  • 1
    $\begingroup$ While I can't accept this as the best answer, I appreciate your effort in trying to help me solve the issue! Thank you! $\endgroup$
    – Kraigolas
    Jun 5, 2020 at 21:06

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .