This is the Tractor problem from the Kattis problems archive.
Problem Statement
Bessie the Cow has stolen Farmer John’s tractor and is running wild on the coordinate plane! She, however, is a terrible driver, and can only move according to the following rules:
Each of her movements is in the same direction as either the positive x-axis or the positive y-axis. Her \$\text{nth}\$ movement takes her \2ドル^{n−1}\$ units forward in her chosen direction. (On her first movement, \$n = 1\,ドル so she moves 1 unit.) Farmer John’s farm is on the coordinate plane, in the shape of a rectangle with corners at \$(0, 0), (A, 0), (0, B) \text{and} (A, B)\$. If Bessie starts at (0, 0), how many points inside the farm, including the boundary, could she reach?
Input Format
The input begins with an integer \$N (1 \le N \le 100)\$ on a line by itself, indicating the number of test cases that follow. Each of the following N lines contains two space separated integers A and B \$(1 \le A, B \le 10^8)\,ドル describing the upper-right corner of Farmer John’s farm.
Output Format
Output N lines, with the Nth line containing the number of points that Bessie could possibly reach in the Nth test case.
In the first test case of the sample, Bessie can reach the following six points: \$(0, 0), (0, 1), (1, 0), (1, 2), (2, 1) and (3, 0)\$.
Sample Input
2 2 3 7 7
Sample Output
6 15
Solution
def next_steps():
n = 1
while True:
yield n
n <<= 1
def moves(a, b):
current_positions = [(0, 0)]
moves = []
for step in next_steps():
next_positions = []
while current_positions:
x, y = current_positions.pop(0)
moves.append((x, y))
if x + step <= a:
next_positions.append((x+step, y))
if y + step <= b:
next_positions.append((x, y+step))
if not next_positions:
break
else:
current_positions = next_positions
return moves
if __name__ == '__main__':
for test_case in xrange(int(raw_input())):
a, b = map(int, raw_input().strip().split())
print len(moves(a, b))
It's working fine on smaller input but gets timed out on very large instances.
4 Answers 4
for
loop
Use a for
loop when iterating over a collection:
for (x, y) in current_positions:
moves.append((x, y))
for
has less power than while
(theoretically) and its simplicity makes it a better choice over while
.
Avoid premature optimization
Just multiply by two:
n *= 2
Some tests:
>>> import timeit
>>> timeit.timeit("10 << 1")
0.029866752000089036
>>> timeit.timeit("10 * 2")
0.03209113599950797
>>> 0.029866752000089036 / 0.03209113599950797
0.9306854079751793
<<
is just 7% faster than *2
And the majority of your time is spent branching if x + step <= a:
and playing with lists moves.append((x, y))
, so the final improvement would be tiny, not worth the loss in readability.
Optimizing the algorithm
Your algorithm has exponential time complexity:
print(step, len(current_positions))
Gives:
1 2
2 4
4 8
8 16
16 32
32 64
64 74
128 0
At each iteration, the number of current_positions
is doubling, and this can get out of hand really quickly.
Arguments name
a
and b
are not acceptable argument names. I suggest width
and height
.
Some plotting always gives ideas
Following @ErikR suggestion, I made a graph of the reachable points and noticed a serious regularity, still doing graphs is a nice skill to have, so I encourage you to reproduce this graph by yourself.
-
\$\begingroup\$ You don't have to explicitly have parentheses around
x, y
in thefor
loop. It can just be written like this:for x, y in current_positions:
. \$\endgroup\$Ethan Bierlein– Ethan Bierlein2015年10月07日 18:36:17 +00:00Commented Oct 7, 2015 at 18:36 -
\$\begingroup\$ What do you mean by
for loop has less power than while loop
. I often get confused when to chose one. My criteria is when you don't know how many times a loop should run then simply opt forwhile
which I surely overlooked here. \$\endgroup\$CodeYogi– CodeYogi2015年10月08日 05:06:20 +00:00Commented Oct 8, 2015 at 5:06 -
\$\begingroup\$ @CodeYogi in Python while may do anything, while only iterates over a n iterator \$\endgroup\$Caridorc– Caridorc2015年10月08日 14:02:53 +00:00Commented Oct 8, 2015 at 14:02
-
\$\begingroup\$ @Caridorc why in Python, AFAIK its true in other languages as well. \$\endgroup\$CodeYogi– CodeYogi2015年10月08日 15:20:34 +00:00Commented Oct 8, 2015 at 15:20
Note that for \$a = b = 10^6\$ the answer is 2000001 which is also the size of your moves
list. Since you are only interested in its length, just use the variable to store the length of the list - i.e. replace moves.append(...)
with moves += 1
. This will enable you to solve the problem for larger values of a and b.
But to solve the problem for really large values you need a better algorithm. One way to see this is to note that moves
can increase only by 1 on each iteration of the loop. That means for \$a = b = 10^6\$ your problem will be performing at least 2 million loop iterations. On my machine 200K iterations already takes 8 seconds, so that shows that counting by 1 isn't a feasible way to solve this problem.
For a hint to a better method, plot out the points for a = 10, b = 10 and see if you can discern a pattern.
-
\$\begingroup\$ Thanks for providing the base for my "better algorithm" answer, and also for giving the indication leading to me finding out that for
a=b=x
the number of legal places are2x+1
. \$\endgroup\$holroy– holroy2015年10月07日 22:12:27 +00:00Commented Oct 7, 2015 at 22:12 -
\$\begingroup\$ Sry, but a=b=10^8 :) \$\endgroup\$CodeYogi– CodeYogi2015年10月08日 05:04:12 +00:00Commented Oct 8, 2015 at 5:04
-
\$\begingroup\$ @CodeYogi - is your comment directed to me or to @holroy? \$\endgroup\$ErikR– ErikR2015年10月08日 05:52:58 +00:00Commented Oct 8, 2015 at 5:52
-
\$\begingroup\$ Its to you, you have given a=b=10^6 \$\endgroup\$CodeYogi– CodeYogi2015年10月08日 06:17:20 +00:00Commented Oct 8, 2015 at 6:17
-
\$\begingroup\$ I just used it as an example of why counting by 1 is an impractical method of solving the problem. Clearly if a = b = 10^6 takes too long, you won't be able to solve the problem for a = b = 10^8. Hope this clarifies things. \$\endgroup\$ErikR– ErikR2015年10月08日 06:27:56 +00:00Commented Oct 8, 2015 at 6:27
A better algorithm
Thanks to ErikR and Caridorc for providing the base and image used to produce this answer:
Graph over places Bessie the Cow covers
From image one sees a clear diagonal pattern which can be expressed using the following function based upon \$x\$ and \$n\$:
$$ f(x, n) = 2^n -1 - x $$
Added: This function denotes the \$y\$ coordinate of the dots in the image. Let \$x\$ range from \0ドル\$ through \$A\,ドル first for \$n=0\$ which gives the \$(0, 0)\$ point (and a lot of points outside the coordinate plane). For \$n=1\$ it gives the \$(1, 0)\$ and \$(0, 1)\,ドル and so it continues. To calculate for \$f(0, 3)\$ you need to find to use \2ドル^3\$ (aka
print 2**3
in python), which then gives: $$ f(0, 3) = 2^n - 1 - x = 8 -1 -0 = 7 $$Which in turn gives us the \$y\$ of the fourth diagonal in the diagram (counting diagonals from the lower left including the one consisting of just the point \$(0, 0)\,ドル that is the point \$(0, f(0, 3)) = (0, 7)\$
Further since this function has a constant drop of 1 pr increase in \$x\,ドル the difference between the \$x_{start}\$ and the \$x_{end}\$ plus 1
of any given diagonal indicates how many places/moves this diagonal contributes to the final amount of legal moves. Finally, the \$x_{start}\$ needs either to be on the left or top edge or it is uninteresting, and the \$x_{end}\$ needs to be at the bottom or right edge to have any interest.
Added: \$x_{start}\$ is the x coordinate of the leftmost point in any given diagonal, and the \$x_{end}\$ is the x coordinate of the rightmost in the same diagonal. For \$n\$ diagonals you'll have \$n\$ pairs of \$x_{start}\$ and \$x_{end}\,ドル in the image \5ドル\$ diagonals are shown including the \$(0, 0)\$ diagonal.
Formalisation of previous statements:
- Left edge – If \$f(0, n) < B\$ for current \$n\,ドル then \$x_{start}\$ is 0
- Top edge – If \$f(0, n) > B\$ for current \$n\,ドル then find the \$x\$ where \$f(x, n) == B\$. That is let \$x_{start} = 2^n - 1 - B\$
- If \$x_{start} > A\,ドル then it is uninteresting as the diagonal doesn't cross our coordinate plane, we can then use \$x_{start} = A + 1\$
- Bottom edge – Find \$f(x, n) = 0\$ for current \$n\$. That is \$x_{end}\ = 2^n - 1\$
- If \$x_{end} > A\$ then limit it to \$x_{end} = A\$
- Places contribution is only valid if \$x_{start} <= A\$ and \$x_{start} <= x_{end}\,ドル and if valid the contribution is: \$x_{end} - x_{start} + 1\$
Do the calcuation for \$n = 3\$
- Left edge – \$f(0, 3) = 2^3 - 1 - 0 = 7\,ドル which is lower than B, let \$x_{start} = 0\$
- Bottom edge – \$x_{end} = 2^3 -1 = 7\,ドル which is lower than A, let \$x_{end} = 7\$
- Valid contribution – \7ドル - 0 + 1 = 8\$
Do the calculation for \$n = 4\$
- Left edge – \$f(0, 4) = 2^4 - 1 - 0 = 15\,ドル which is above B
- Top edge – Let \$x_{start} = 2^4 - 1 - 10 = 5\,ドル which is lower than \$A\$ so it's valid
- Bottom edge – Let \$x_{end} = 2^4 - 1 = 15\,ドル which is larger than \$A\,ドル so limit it to \$x_{end} = 10\$
- Valid contribution – \10ドル - 5 + 1 = 6\$
Code refactor
When refactoring for coding an optimal calculation I add the following statements:
- When the \$x_{start}\$ leaves the left edge, it will never come back down again, and similarily when it passes \$A\$ it is for ever lost
- When the \$x_{end}\$ crosses the right edge, it can always be limited to \$A\$
This leads to the following code using generators for calculating the next diagonal x pairs:
def x_start_generator(a, b):
"""Generate start x-coordinate of the n'th diagonal"""
above_b = False # Indicates if an earlier x_start=0 has y > b
beyond_a = False # Indicates if an earlier x_start > a
two_pow_n = 1 # Iterates 2**n, instead of calculating it
while True:
# print('two_pow_n: {}, diag: {}, above_b: {}, beyond_a: {}'.format(
# two_pow_n, diagonal_function(0, two_pow_n), above_b, beyond_a))
# if x_start has been bigger than a, then it will continue to be bigger
# so we always return a+1
if beyond_a:
yield a+1
continue
# Verify if x_start is on left edge (that is 'not above_b')
if not above_b:
above_b = (two_pow_n - 1) > b
# If on left edge, return start_x = 0 ...
if not above_b:
yield 0
else:
# ... else find x_start on line at height b
x_start = two_pow_n - 1 - b
# Check if x_start is beyond the value of a, and store this
beyond_a = x_start > a
# If not beyond return x_start matching the top edge
if not beyond_a:
yield x_start
# Prepare for next n
two_pow_n *= 2
def x_end_generator(a, b):
"""Generate end x-coordinate of the n'th diagonal"""
beyond_a = False # Indicates if an earlier x_end > b
two_pow_n = 1
while True:
# If x_end has once been larger than a, limit it to a
if beyond_a:
yield a
continue
# Find end coordinate when diagonal crosses y=0
x_end = two_pow_n - 1
# Check if we've passed a, and store this
beyond_a = x_end > a
# If not passed, return x coordinate
if not beyond_a:
yield x_end
# Prepare for next iteration
two_pow_n *= 2
def find_legal_places(a, b, print_xtras=False):
"""Return number of legal places within (0, 0) -> (a, b)
Using the x_start_generator() and x_end_generator() find the start
and end coordinators of the n'th diagonal. If it's a valid diagonal
add the legal places from this diagonal to the total. When not any
more valid coordinates are found, return the total.
If wanted, print_xtras=True, you can get print outs of the coordinates
as we tag along, and a nice total.
"""
x_start_range = x_start_generator(a, b)
x_end_range = x_end_generator(a, b)
legal_places = 0
n = 0
while True:
x_start = next(x_start_range)
x_end = next(x_end_range)
if x_start < a and x_start <= x_end:
legal_places += x_end - x_start + 1
if print_xtras:
print('n: {: 4d}, x_start: {: 12d}, x_end: {: 12d}, legal places: {: 12d}'.format(
n, x_start, x_end, legal_places))
else:
break
n += 1
if print_xtras:
print(' For A={} and B={} there are {} legal places\n'.format(a, b, legal_places))
return legal_places
def main():
print 'Sample Output 1'
for (a, b) in [(2, 3), (7, 7)]:
print(find_legal_places(a, b))
print
find_legal_places(10, 10, True)
print('a=10**1000, b=10**1000, legal_places={}'.format(find_legal_places(10**1000, 10**1000)))
if __name__ == '__main__':
main()
When running my main()
with a few test cases of varying magnitude, it all completed in less then a second. Cases run are (a, b) in [(2, 3), (7, 7), (10, 10), (10**1000, 10**1000)
. The latter would according to ErikR require quite a lot of iterations...
The output of this run was as follows:
Sample Output 1
6
15
n: 0, x_start: 0, x_end: 0, legal places: 1
n: 1, x_start: 0, x_end: 1, legal places: 3
n: 2, x_start: 0, x_end: 3, legal places: 7
n: 3, x_start: 0, x_end: 7, legal places: 15
n: 4, x_start: 5, x_end: 10, legal places: 21
For A=10 and B=10 there are 21 legal places
a=10**1000, b=10**1000, legal_places=20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
That last number is 2
followed by 999 0
's and ending with a 1
, or put another way 2 * 10^1000 + 1
. It seems like when \$a = b = x\,ドル then there is \2ドル * x + 1\$ legal places1.
1I tested for all cases up through \10ドル^{1000}\$
-
\$\begingroup\$ It will be very nice if you can describe your function in simpler terms along with xstart and xend. AFAIK the function relates nth diagonal and x but I am getting confused. \$\endgroup\$CodeYogi– CodeYogi2015年10月08日 05:03:21 +00:00Commented Oct 8, 2015 at 5:03
-
\$\begingroup\$ I am talking about this
f(x,n)
. \$\endgroup\$CodeYogi– CodeYogi2015年10月08日 07:38:20 +00:00Commented Oct 8, 2015 at 7:38 -
\$\begingroup\$ @CodeYogi, Have added some more explanation, which hopefully helps you understand it better \$\endgroup\$holroy– holroy2015年10月08日 08:13:56 +00:00Commented Oct 8, 2015 at 8:13
Style and Code review
Here are some comments on how to make your current logic somewhat faster, even though not in the same leagues as you get when switching to a better algoritm. This is to help you the next time you find a similar problem.
- General style is good – You might get a few comments on using variable names like
a
,b
, ... but as they refer directly to your problem statement I find it acceptable - Include problem statement in docstrings – In the original file of my answer I've included the problem statement at the start of the file in a docstring, this allows me when I later revisit the file to understand what problem I was trying to solve. Even just adding a link to the problem is better than nothing, but in general I would suggest making a textual copy in docstrings
- Add docstrings to methods – Always document what it does and returns if is a slightly complex method
- Avoid code in the
__main__
block – Move code here to a well named function, and it is easier to understand what is happening. Themap(int, raw_input().strip().split())
is close to garbage when skimming it - Don't store any more data then needed – This is the major performance issues in your code. You store every legal move, when you only need the count of all moves. Storing
current_positions
andnext_positions
is good, storingmoves
not good at all - Use appropriate loop methods – Use
for position in current_positions
instead of a while loop. It is more intuitive, and that is what you are actually doing. This also skips the unnatural pop which modifies the list on every iteration, which is unneccessary
You should try refactoring your code according to these suggestions, and you'll see a massive increase in performance when going to the larger versions.
-
\$\begingroup\$ Very informative! hope I could include you in my next code reviews somehow. Still need to check your optimized solution. \$\endgroup\$CodeYogi– CodeYogi2015年10月08日 04:54:31 +00:00Commented Oct 8, 2015 at 4:54
Explore related questions
See similar questions with these tags.