tuple, not list
# In queue i added elements [current location, Direction,Turn Counts,visited dots in list]
queue.append([[0,0],'R',0,[]])
queue.append([[0,0],'D',0,[]])
Prefer to write the origin location as (0, 0)
rather than [0, 0]
.
Why?
In python we use a tuple
to model a C struct,
a fixed-length collection of "different" things.
We use a list
to model an arbitrarily long collection of "same" thing.
We're storing two integers here.
But are they the "same"?
No, certainly not, we impose an interpretation on \$x\$ coordinates
which is very different from how we interpret a \$y\$ coordinate.
Similarly, instead of
[[0, 0], 'R', 0, []]
, prefer this 4-tuple:
((0, 0), 'R', 0, [])
.
Notice that the final visited
is perfect: it is a
growing list of "same" thing, of visited coordinate pairs.
Using cryptic indexes 0
.. 3
is inconvenient.
Prefer to define a
namedtuple
having four named elements.
Sometimes an immutable tuple is not suitable,
and we'll instead choose a mutable
@dataclass.
\$(Δx, Δy)\$ representation
The {L, R, U, D} str
representation does not come
from the Problem Statement, and it is not convenient
in the implementation.
Prefer a 2-tuple of deltas.
- L: (-1, 0)
- R: (0, 1)
- U: (0, -1)
- D: (0, 1)
Now you can uniformly add a delta to a location
without four special cases.
The OP code as written would have benefited from an
enum
that mapped those four values to delta tuples.
Even a dict
lookup mapping would have been preferrable.
helpers
The move()
and check()
helpers are entirely appropriate;
thank you for adding them.
def move(a, b):
Yikes! That is just a terrible signature.
And doesn't come with a """docstring""" to
help the Gentle reader out of this mess.
The comment turns out to be insufficient,
plus it should be promoted to a docstring.
What is a
? What is b
?
Hard to say.
Plus we're treated to lots of cryptic subscripts,
instead of e.g. .x
or .y
coordinate. Yech!
The check()
signature suffers the same syndrome.
quadratic time complexity
if ... and [a[0][0]-1,a[0][1]] not in a[3]:
...
if ... and [a[0][0]+1,a[0][1]] not in a[3]:
...
if ... and [a[0][0], a[0][1]+1] not in a[3]:
...
if ... and [a[0][0], a[0][1]-1] not in a[3]:
return True
Wow, that's a lot of linear scans of an increasingly long list
.
You do this often enough that it would be worth caching
a set
version of the same datastructure,
to reduce the in
complexity from \$O(n)\$ linear
to \$O(1)\$ constant.
quadratic space complexity
Up = copy.deepcopy(p)
Down = copy.deepcopy(p)
Right = copy.deepcopy(p)
Left = copy.deepcopy(p)
Wow, that's a lot of duplicate visited
coordinates,
by the time we get near the exit square.
Prefer to represent "current state" with this mapping:
(entry_direction, location) --> cost
.
That is, we entered this location
square
in one of four ways, and cumulative price of
constructing prior squares plus this square is cost
.
Now it suffices to create four (\$O(1)\$ constant space!) copies
of this square, to enqueue four distinct deltas to explore.
You might find it convenient to have a
defaultdict
report such cost
figures.
For an as yet unexplored location,
report some very large \$\infty\$ cost, perhaps math.float('inf')
.
We can read out a solution from any location,
including the end location,
by having DFS scan four adjacent cost
values
and carry on from there.
The entry direction is easily turned into a reverse delta
pointing at the next square, typically closer to the origin.
pruning
My code consumes lots of running time
Yeah, that's what comes of exhaustively exploring the space.
An A*
consistent admissible heuristic here would be Manhattan distance.
That is, pretend there's zero barriers between here and the goal.
We'll need to make either zero, or typically one turn.
Add up the turn cost, the \$x\$, and the \$y\$ distances
to get the best possible "cost to goal", and use that
to prioritize which paths to explore first.
Or, since this problem is simple enough, use
Dijkstra.