0

Im finding the distance between two points, given departure = [x,y] and destination = [x,y]. With x or y, one is always a float and the other an int, so its always on a line. You have to stay on the gridlines to get to the destination point, and there is no set incrementation. I havent seen any other posts on finding distance on a grid that deal with the mix of ints and floats so here I am.

This is my code:

def perfectCity(departure, destination):
    return abs(destination[0]-departure[0]) + abs(destination[1]-departure[1]) 

An example would be departure = [0.4, 1] and destination = [0.9, 3], it should equal 2.7, but I get 2.5

For example, you go from [0.4, 1] to [1, 1] to [1, 3] to [0.9, 3] for a total difference of 2.7. It's like calculating the Manhattan distance, but instead of starting and ending at lattice points, you might start and/or end half-way down a block.

Jared Goguen
  • 8,772
  • 2
  • 18
  • 36
JonnyDoeInWisco
  • 233
  • 1
  • 4
  • 12
  • Floats almost always have a bit of round-off error. Finite decimals expansions in base 10 are not always finite decimal expansions in base 2 – John Coleman Apr 13 '16 at 00:43
  • 2
    Possible duplicate of [Is floating point math broken?](http://stackoverflow.com/questions/588004/is-floating-point-math-broken) – John Coleman Apr 13 '16 at 00:44
  • I dont think its a duplicate, because you would have to know its a floating point error, in the other thread its explicit. – JonnyDoeInWisco Apr 13 '16 at 00:48
  • There can only be one thread where the solution is floating point errors? – JonnyDoeInWisco Apr 13 '16 at 01:27
  • It is a judgment call if two questions are sufficiently similar to be duplicates. How do you get 2.7 for your example? – John Coleman Apr 13 '16 at 01:41
  • Its a coding challenge, thats what the site says it should be. If you start out on a grid at [0.4, 1] and are going to [0.9, 3], you would go 0.6 to the right, up 2, and to the left 0.1 – JonnyDoeInWisco Apr 13 '16 at 01:46
  • 1
    Then -- that isn't Manhattan distance (which is definitely 2.5 rather than 2.7). There is something that you aren't telling us (perhaps that motion is restricted to gridlines which are at multiples of 1.0?). In this case the discrepancy between 2.5 and 2.7 has nothing to do with round-off error but is instead a clash between what the problem is asking for and what you are trying to compute. I'll retract my close vote, but you should edit your question to better communicate the actual problem. – John Coleman Apr 13 '16 at 02:02
  • The restrictions on motion have not been adequately spelled out. For example -- what is the distance between (0.8, 0.8) and (0.9, 0.9). Is it 0.2 or 0.4? You can't simply forbid off-grid travel if the points aren't on the grid. – John Coleman Apr 13 '16 at 02:21
  • Perhaps the "at least one is always a float" statement is supposed to be that "at least one is always an integer" and the "move along gridlines" rule means that you must maintain that invariant while moving? – Blckknght Apr 13 '16 at 02:25
  • Yeah I worded that wrong because I assumed they would throw curveballs in the tests, but looking at it now that wouldnt make sense to have two floats. – JonnyDoeInWisco Apr 13 '16 at 02:29
  • @JohnColeman To clarify: He is trying to calculate the Manhattan distance when you start halfway down a block. For example, you go from [0.4, 1] to [1, 1] to [1, 3] to [0.9, 3] for a total difference of 2.7. – Jared Goguen Apr 13 '16 at 02:58
  • @JaredGoguen I see that now. The original question used the phrase "at least" where it meant "exactly" so it was somewhat confusing. – John Coleman Apr 13 '16 at 03:01

3 Answers3

2

When you look at the different combinations, it seems like the naive Manhattan distance will work, except when your path takes on a "C" shape (as given in the example). This will happen if and only if your two float points are both x-coordinates or y-coordinates and have the same integer part.

An attempt might look like:

def minimum_distance(p1, p2):

    i1, i2 = int(p1[0]), int(p2[0])

    # if both x-coordinates are floats and they have the same integer part
    if i1 != p1[0] and i2 != p2[0] and i1 == i2:

        # find the decimal parts
        d1, d2 = p1[0] - i1, p2[0] - i2

        # find the smaller "C"
        x = min(d1 + d2, (1-d1) + (1-d2))

        # add the y distance to the "C" distance
        return abs(p1[1] - p2[1]) + x

    # repeat with the "y-coordinates are floats" case
    i1, i2 = int(p1[1]), int(p2[1])
    if i1 != p1[1] and i2 != p2[1] and i1 == i2:
        d1, d2 = p1[1] - i1, p2[1] - i2
        y = min(d1 + d2, (1-d1) + (1-d2))
        return abs(p1[0] - p2[0]) + y

    # simple case, return the Manhattan distance
    return abs(p1[0] - p2[0]) + abs(p1[1] - p2[1])


print(minimum_distance([0.4, 1], [0.9, 3]))
# 2.7
Jared Goguen
  • 8,772
  • 2
  • 18
  • 36
2

From each house take a short-range taxicab to a corner. You have two ways of doing so. Then -- take a long-range taxicab between the resulting corners. There are 2x2 = 4 possibilities, depending on the corners traveled to. Take the min:

from math import ceil,floor

#as a helper function, vanilla taxicab:

def t(p,q):
    x,y = p
    z,w = q
    return abs(x-z)+abs(y-w)

#find the two corners closest to a point:

def corners(p):
    x,y = p
    if isinstance(x,float):
        return [(floor(x),y),(ceil(x),y)]
    else:
        return [(x,floor(y)), (x,ceil(y))]

#combine these:   

def dist(p,q):
    return min(t(p,c) + t(c,d) + t(d,q)  for c in corners(p) for d in corners(q))

For example,

>>> dist((.4,1),(.9,3))
2.7
John Coleman
  • 51,337
  • 7
  • 54
  • 119
  • Very clean. It's not very efficient, so if it has to be used repeatedly, it might be worth optimizing. – Jared Goguen Apr 13 '16 at 03:47
  • Timing could be interesting. It seems like no matter what you are going to need at least 3 cases, so I decided to just find all cases and let the min operator pick which one. Built-in operators are written in C, so it isn't clear that this route is slower than explicitly deciding which case to use. I suspect that my approach is quicker than yours in the hard cases but is definitely slower than yours in the easy case where it reduces to simple taxicab, so it is hard to say what the net result is. – John Coleman Apr 13 '16 at 04:01
  • From some simple tests, my function appears to be 4x faster for the hard cases and even quicker for the easy cases. This seems natural since my function only calculates the Manhattan distance once rather than four times. While three cases need to be considered, they can be quickly eliminated without actually performing the "heavy" computations. – Jared Goguen Apr 13 '16 at 04:24
  • Also, I'm not sure the justification that "built-in operators are written in C" is appropriate. For example, for two values, it's twice as quick to use `a if a < b else b` than `min(a,b)`. Simple arithmetic is also pushed back to the C layer *and* it avoids a relatively expensive function call. – Jared Goguen Apr 13 '16 at 04:34
  • @JaredGoguen You are right. With Python it is easy to write elegant code which is not particularly efficient. Arguably it is a feature of Python rather than a bug since (compared to say C) the goal is more to optimize developer time than CPU time. Still, efficiency often matters and the function call overheads in my code would make it a poor choice if the function needed to be applied to a large collection of points. – John Coleman Apr 13 '16 at 11:01
0

It's not the mix of int and float, probably you are experiencing floating point error. floats are not exact, they are approximate, and can therefore be "off" by small amounts.

You could use Decimal objects provided by the decimal module to work accurately with floating point values. Here's an example:

>>> 1.1 + 2.2        # will produce small error
3.3000000000000003
>>> from decimal import Decimal
>>> Decimal('1.1') + Decimal('2.2')
Decimal('3.3')

The final result will still be "off", but using Decimal will help to avoid cumulative rounding errors:

>>> 3.3 - (1.1 + 2.2)
-4.440892098500626e-16
>>> Decimal(3.3) - (Decimal(1.1) + Decimal(2.2))
Decimal('-4.440892098500250464677810669E-16')
>>> Decimal('3.3') - (Decimal('1.1') + Decimal('2.2'))
Decimal('0.0')
mhawke
  • 84,695
  • 9
  • 117
  • 138
  • Its off by more than one or two points, sometimes its off by say 0.2 - 0.6 – JonnyDoeInWisco Apr 13 '16 at 00:57
  • @JonnyDoeInWisco: yes, the error can accumulate with each floating point operation. Using `Decimal` objects should reduce the cumulative errors. – mhawke Apr 13 '16 at 01:02
  • @JonnyDoeInWisco I would be surprised if the error is that large for input of typical size. Perhaps you could edit your question and show an example of any error that you think can't be explained by rather minor round-off error. – John Coleman Apr 13 '16 at 01:19
  • The down-vote was unfair since this was a good answer to the original question (not edited beyond recognition), so +1 – John Coleman Apr 13 '16 at 03:32