I'm trying to write a code that generates a visibility graph from a set of points and walls (obstacles). My algorithms is not correct and fails on some cases where there is more than one wall intersecting an edge between two points.
Here's kind of a pseudo-python code for my algorithm :
Intersect(wall, P, Q):
returns True if wall segment intersects with PQ segment
Cross(wall, P, Q):
returns True if wall segment crosses PQ segment
for i in range(len(nodes)):
for j in range(i + 1, len(nodes)):
flag = True
for wall in walls:
if (Cross(wall, nodes[i].pos, nodes[j].pos)):
flag = False
if (flag):
nodes[i].adj.append(nodes[j])
nodes[j].adj.append(nodes[i])
How can I fix my algorithm?
Here's one of the tests where it fails:
Walls :
w1 -> (1, 0),(2, 1)
w2 -> (2, 1),(3, 2)
Nodes to be checked:
node1 -> (0, 2)
node2 -> (4, 0)
There shouldn't be an edge but my algorithm generates an edge because the edge does not Cross any wall (it intersects but not cross).
For clarification, Cross means that two segments intersect (share a point,) but they don't share any point that is either the start or end of any of the two segments.
When the view ray just grazes a wall like this, you need to keep track of whether the grazing was at the left edge of the wall or at the right edge, as seen from viewpoint P.
def LeftIntersect(wall, P, Q):
if Cross(wall, P, Q):
return False
if not Intersect(wall, P, Q):
return False
if magnitude(cross_product(PQ, wall_midpoint)) <= 0:
return False
return True
def RightIntersect(wall, P, Q):
if Cross(wall, P, Q):
return False
if not Intersect(wall, P, Q):
return False
if magnitude(cross_product(PQ, wall_midpoint)) >= 0:
return False
return True
for i in range(len(nodes)):
for j in range(i + 1, len(nodes)):
crossCount = 0
leftIntersectCount = 0
rightIntersectCount = 0
for wall in walls:
if (Cross(wall, nodes[i].pos, nodes[j].pos)):
crossCount += 1
if (LeftIntersect(wall, nodes[i].pos, nodes[j].pos)):
leftIntersectCount += 1
if (RightIntersect(wall, nodes[i].pos, nodes[j].pos)):
rightIntersectCount += 1
visible = True
if (crossCount > 0)
visible = False
if (leftIntersect > 0 && rightIntersect > 0)
visible = False
if (visible):
nodes[i].adj.append(nodes[j])
nodes[j].adj.append(nodes[i])
The first way that comes to mind for me is to check every pair of three from [node_a, node_b, wall_start, wall_end] and see if the third point lines along the segment between the other two. A quick and accurate way to do this is first create a vector between each of the points, and take two dot products to make sure the "middle" point really does lie in the middle. Additionally is necessary to also check the direction of the vectors to make sure they are parallel, which is equally fast. If both pass, then the third point lies along the segment between the other two.
In python
def intersect(a, b, c):
(ax, ay), (bx, by), (cx, cy) = a, b, c
bax, bay = bx-ax, by-ay
bcx, bcy = bx-cx, by-cy
acx, acy = ax-cx, ay-cy
if bax*bcx + bay*bcy < 0: return False
if bax*acx + bay*acy > 0: return False
return bax*bcy == bay*bcx
In practice, it might be better to check bax*bcy == bay*bcx first, since it is just as fast but probably more likely to fail (and break early) for non-intersecting points.
To then check if any two points intersects a given wall-
def wall_hit(node1, node2, wall_start, wall_end):
return intersect(node1, node2, wall_start) or \
intersect(node1, node2, wall_end) or \
intersect(wall_start, wall_end, node1) or \
intersect(wall_start, wall_end, node2)
Since most of the checks will effectively "short-circuit" after the first or second check in each intersect() call, and each wall_hit() will short-circuit if any of them do hit, I don't think this would be too costly to implement.
If you need to optimize it, you could always compute + reuse the bax, bay = bx-ax, by-ay; ... calculations by either inlining all the function calls and reordering, or by computing them in a separate function and then caching with the lru_cache decorator from functools. Additionally, if you go with the inlining approach, you can likely reorder the conditionals and bax, bay = ... calculations to lazy evaluate them so that you may not need to compute all the intermediate values to assert hit/no_hit.
Related
I was given a question during an interview and although my answer was accepted at the end they wanted a faster approach and I went blank..
Question :
Given an undirected graph, can you see if it's a tree? If so, return true and false otherwise.
A tree:
A - B
|
C - D
not a tree:
A
/ \
B - C
/
D
You'll be given two parameters: n for number of nodes, and a multidimensional array of edges like such: [[1, 2], [2, 3]], each pair representing the vertices connected by the edge.
Note:Expected space complexity : O(|V|)
The array edges can be empty
Here is My code: 105ms
def is_graph_tree(n, edges):
nodes = [None] * (n + 1)
for i in range(1, n+1):
nodes[i] = i
for i in range(len(edges)):
start_edge = edges[i][0]
dest_edge = edges[i][1]
if nodes[start_edge] != start_edge:
start_edge = nodes[start_edge]
if nodes[dest_edge] != dest_edge:
dest_edge = nodes[dest_edge]
if start_edge == dest_edge:
return False
nodes[start_edge] = dest_edge
return len(edges) <= n - 1
Here's one approach using a disjoint-set-union / union-find data structure:
def is_graph_tree(n, edges):
parent = list(range(n+1))
size = [1] * (n + 1)
for x, y in edges:
# find x (path splitting)
while parent[x] != x:
x, parent[x] = parent[x], parent[parent[x]]
# find y
while parent[y] != y:
y, parent[y] = parent[y], parent[parent[y]]
if x == y:
# Already connected
return False
# Union (by size)
if size[x] < size[y]:
x, y = y, x
parent[y] = x
size[x] += size[y]
return True
assert not is_graph_tree(4, [(1, 2), (2, 3), (3, 4), (4, 2)])
assert is_graph_tree(6, [(1, 2), (2, 3), (3, 4), (3, 5), (1, 6)])
The runtime is O(V + E*InverseAckermannFunction(V)), which better than O(V + E * log(log V)), so it's basically O(V + E).
Tim Roberts has posted a candidate solution, but this will work in the case of disconnected subtrees:
import queue
def is_graph_tree(n, edges):
# A tree with n nodes has n - 1 edges.
if len(edges) != n - 1:
return False
# Construct graph.
graph = [[] for _ in range(n)]
for first_vertex, second_vertex in edges:
graph[first_vertex].append(second_vertex)
graph[second_vertex].append(first_vertex)
# BFS to find edges that create cycles.
# The graph is undirected, so we can root the tree wherever we want.
visited = set()
q = queue.Queue()
q.put((0, None))
while not q.empty():
current_node, previous_node = q.get()
if current_node in visited:
return False
visited.add(current_node)
for neighbor in graph[current_node]:
if neighbor != previous_node:
q.put((neighbor, current_node))
# Only return true if the graph has only one connected component.
return len(visited) == n
This runs in O(n + len(edges)) time.
You could approach this from the perspective of tree leaves. Every leaf node in a tree will have exactly one edge connected to it. So, if you count the number of edges for each nodes, you can get the list of leaves (i.e. the ones with only one edge).
Then, take the linked node from these leaves and reduce their edge count by one (as if you were removing all the leaves from the tree. That will give you a new set of leaves corresponding to the parents of the original leaves. Repeat the process until you have no more leaves.
[EDIT] checking that the number of edges is N-1 eliminiates the need to do the multi-root check because there will be another discrepancy (e.g. double link, missing node) in the graph if there are multiple 'roots' or a disconnected subtree
If the graph is a tree, this process should eliminate all nodes from the node counts (i.e. they will all be flagged as leaves at some point).
Using the Counter class (from collections) will make this relatively easy to implement:
from collections import Counter
def isTree(N,E):
if N==1 and not E: return True # root only is a tree
if len(E) != N-1: return False # a tree has N-1 edges
counts = Counter(n for ab in E for n in ab) # edge counts per node
if len(counts) != N : return False # unlinked nodes
while True:
leaves = {n for n,c in counts.items() if c==1} # new leaves
if not leaves:break
for a,b in E: # subtract leaf counts
if counts[a]>1 and b in leaves: counts[a] -= 1
if counts[b]>1 and a in leaves: counts[b] -= 1
for n in leaves: counts[n] = -1 # flag leaves in counts
return all(c==-1 for c in counts.values()) # all must become leaves
output:
G = [[1,2],[1,3],[4,5],[4,6]]
print(isTree(6,G)) # False (disconnected sub-tree)
G = [[1,2],[1,3],[1,4],[2,3],[5,6]]
print(isTree(6,G)) # False (doubly linked node 3)
G = [[1,2],[2,6],[3,4],[5,1],[2,3]]
print(isTree(6,G)) # True
G = [[1,2],[2,3]]
print(isTree(3,G)) # True
G = [[1,2],[2,3],[3,4]]
print(isTree(4,G)) # True
G = [[1,2],[1,3],[2,5],[2,4]]
print(isTree(6,G)) # False (missing node)
Space complexity is O(N) because the counts dictionary has one entry per node(vertex) with an integer as value. Time complexity will be O(ExL) where E is the number of edges and L is the number of levels in the tree. The worts case time is O(E^2) for a tree where all parents have only one child node. However, since the initial condition is for E to be less than V, the worst case will actually be O(V^2)
Note that this algorithm makes no assumption on edge order or numerical relationships between node numbers. The root (last node to be made a leaf) found by this algorithm is not necessarily the only possible root given that, unless the nodes have an implicit cardinality relationship (or edges have an order), there could be ambiguous scenarios:
[1,2],[2,3],[2,4] could be:
1 2 3
|_2 OR |_1 OR |_2
|_3 |_3 |_1
|_4 |_4 |_4
If a cardinality relationship between node numbers or an order of edges can be relied upon, the algorithm could potentially be made more time efficient (because we could easily determine which node is the root and start from there).
[EDIT2] Alternative method using groups.
When the number of edges is N-1, if the graph is a tree, all nodes should be reachable from any other node. This means that, if we form groups of reachable nodes for each node and merge them together based on the edges, we should end up with a single group after going through all the edges.
Here is the modified function based on that approach:
def isTree(N,E):
if N==1 and not E: return True # root only is a tree
if len(E) != N-1: return False # a tree has N-1 edges
groups = {n:[n] for ab in E for n in ab} # each node in its own group
if len(groups) != N : return False # unlinked nodes
for a,b in E:
groups[a].extend(groups[b]) # merge groups
for n in groups[b]: groups[n] = groups[a] # update nodes' groups
return len(set(map(id,groups.values()))) == 1 # only one group when done
Given that we start out with fewer edges than nodes and that group merging will consume at most 2x a group size (so also < N), the space complexity will remain O(V). The time complexity will also be O(V^2) at for the worts case scenarios
You don't even need to know how many edges there are:
def is_graph_tree(n, edges):
seen = set()
for a,b in edges:
b = max(a,b)
if b in seen:
return False
seen.add(b)
return True
a = [[1,2],[2,3],[3,4]]
print(is_graph_tree(0,a))
b = [[1,2],[1,3],[2,3],[2,4]]
print(is_graph_tree(0,b))
Now, this WON'T catch the case of disconnected subtrees, but that wasn't in the problem description...
I was working on this specific LeetCode problem and I encountered a problem where I would be stuck recursing. The way I understand it, if an input type is mutable, the input should be pass by reference, so they should be referencing the same thing. Can someone explain how my method breaks? I really want to try solving this problem using recursion, but I don't understand how to do it using my method. My code first finds north, east,south,west, and then determines if they are valid. It then determines if among those directions if they have the same count as the original node.
Of those that have the same count as the original node, I need to recurse on those and repeat the process until all nodes have the value of newColor
https://leetcode.com/problems/flood-fill/
class Solution:
def floodFill(self, image: List[List[int]], sr: int, sc: int, newColor: int) -> List[List[int]]:
top = (sr-1, sc)
down = (sr+1, sc)
left = (sr, sc-1)
right = (sr, sc+1)
# Possible Directions
posDirec = [direc for direc in [top,down,left,right] if direc[0] >=0 and direc[1] >=0 and direc[0] < len(image) and direc[1] < len(image[0])]
# Neighbors that we can traverse
posNeigh = [e for e in posDirec if image[e[0]][e[1]] == image[sr][sc]]
image[sr][sc] = newColor
# print(image, '\n')
print(len(posNeigh), posNeigh, image)
if len(posNeigh) == 0:
pass
else:
for neigh in posNeigh: #top, down,left, right of only valids
self.floodFill(image, neigh[0], neigh[1], newColor)
return image
At the very end, my program should return the image. I want to return the image at the end, however, my code ends up stuck in recursion
Take a look at the following line:
# Neighbors that we can traverse
posNeigh = [e for e in posDirec if image[e[0]][e[1]] == image[sr][sc]]
This condition fails to account for the possibility that image[e[0]][e[1]] has already been filled in with newColor, resulting in an infinite loop between filled cells and a stack overflow.
If we change it to
posNeigh = [
e for e in posDirec
if image[e[0]][e[1]] == image[sr][sc]
and image[e[0]][e[1]] != newColor # <-- added
]
we can make sure we're not revisiting previously-filled areas.
Given that the list comprehensions have grown quite unwieldy, you might consider a rewrite:
def floodFill(self, image, sr, sc, new_color):
target_color = image[sr][sc]
image[sr][sc] = new_color
for y, x in ((sr + 1, sc), (sr, sc - 1), (sr, sc + 1), (sr - 1, sc)):
if y >= 0 and x >= 0 and y < len(image) and x < len(image[0]) and \
image[y][x] != new_color and image[y][x] == target_color:
self.floodFill(image, y, x, new_color)
return image
A mutable input does not pass by reference. The way I see it, solving it using recursion is not possible. Try an iterative solution.
I am trying to find a way to solve a maze. My teacher said I have to use BFS as a way to learn. So I made the algorithm itself, but I don't understand how to get the shortest path out of it. I have looked at others their codes and they said that backtracking is the way to do it. How does this backtracking work and what do you backtrack?
I will give my code just because I like some feedback to it and maybe I made some mistake:
def main(self, r, c):
running = True
self.queue.append((r, c))
while running:
if len(self.queue) > 0:
self.current = self.queue[0]
if self.maze[self.current[0] - 1][self.current[1]] == ' ' and not (self.current[0] - 1, self.current[1])\
in self.visited and not (self.current[0] - 1, self.current[1]) in self.queue:
self.queue.append((self.current[0] - 1, self.current[1]))
elif self.maze[self.current[0] - 1][self.current[1]] == 'G':
return self.path
if self.maze[self.current[0]][self.current[1] + 1] == ' ' and not (self.current[0], self.current[1] + 1) in self.visited\
and not (self.current[0], self.current[1] + 1) in self.queue:
self.queue.append((self.current[0], self.current[1] + 1))
elif self.maze[self.current[0]][self.current[1] + 1] == 'G':
return self.path
if self.maze[self.current[0] + 1][self.current[1]] == ' ' and not (self.current[0] + 1, self.current[1]) in self.visited\
and not (self.current[0] + 1, self.current[1]) in self.queue:
self.queue.append((self.current[0] + 1, self.current[1]))
elif self.maze[self.current[0] + 1][self.current[1]] == 'G':
return self.path
if self.maze[self.current[0]][self.current[1] - 1] == ' ' and not (self.current[0], self.current[1] - 1) in self.visited\
and not (self.current[0], self.current[1] - 1) in self.queue:
self.queue.append((self.current[0], self.current[1] - 1))
elif self.maze[self.current[0]][self.current[1] - 1] == 'G':
return self.path
self.visited.append((self.current[0], self.current[1]))
del self.queue[0]
self.path.append(self.queue[0])
As maze I use something like this:
############
# S #
##### ######
# #
######## ###
# #
## ##### ###
# G#
############
Which is stored in a matrix
What I eventually want is just the shortest path inside a list as output.
Since this is a coding assignment I'll leave the code to you and simply explain the general algorithm here.
You have a n by m grid. I am assuming this is is provided to you. You can store this in a two dimensional array.
Step 1) Create a new two dimensional array the same size as the grid and populate each entry with an invalid coordinate (up to you, maybe use None or another value you can use to indicate that a path to that coordinate has not yet been discovered). I will refer to this two dimensional array as your path matrix and the maze as your grid.
Step 2) Enqueue the starting coordinate and update the path matrix at that position (for example, update matrix[1,1] if coordinate (1,1) is your starting position).
Step 3) If not at the final coordinate, dequeue an element from the queue. For each possible direction from the dequeued coordinate, check if it is valid (no walls AND the coordinate does not exist in the matrix yet), and enqueue all valid coordinates.
Step 4) Repeat Step 3.
If there is a path to your final coordinate, you will not only find it with this algorithm but it will also be a shortest path. To backtrack, check your matrix at the location of your final coordinate. This should lead you to another coordinate. Continue this process and backtrack until you arrive at the starting coordinate. If you store this list of backtracked coordinates then you will have a path in reverse.
The main problem in your code is this line:
self.path.append(self.queue[0])
This will just keep adding to the path while you go in all possible directions in a BFS way. This path will end up getting all coordinates that you visit, which is not really a "path", because with BFS you continually switch to a different branch in the search, and so you end up collecting positions that are quite unrelated.
You need to build the path in a different way. A memory efficient way of doing this is to track where you come from when visiting a node. You can use the visited variable for that, but then make it a dictionary, which for each r,c pair stores the r,c pair from which the cell was visited. It is like building a linked list. From each newly visited cell you'll be able to find back where you came from, all the way back to the starting cell. So when you find the target, you can build the path from this linked list.
Some other less important problems in your code:
You don't check whether a coordinate is valid. If the grid is bounded completely by # characters, this is not really a problem, but if you would have a gap at the border, you'd get an exception
There is code repetition for each of the four directions. Try to avoid such repetition, and store recurrent expressions like self.current[1] - 1 in a variable, and create a loop over the four possible directions.
The variable running makes no sense: it never becomes False. Instead make your loop condition what currently is your next if condition. As long as the queue is not empty, continue. If the queue becomes empty then that means there is no path to the target.
You store every bit of information in self properties. You should only do that for information that is still relevant after the search. I would instead just create local variables for queue, visited, current, ...etc.
Here is how the code could look:
class Maze():
def __init__(self, str):
self.maze = str.splitlines()
def get_start(self):
row = next(i for i, line in enumerate(self.maze) if "S" in line)
col = self.maze[row].index("S")
return row, col
def main(self, r, c):
queue = [] # use a local variable, not a member
visited = {} # use a dict, key = coordinate-tuples, value = previous location
visited[(r, c)] = (-1, -1)
queue.append((r, c))
while len(queue) > 0: # don't use running as variable
# no need to use current; just reuse r and c:
r, c = queue.pop(0) # you can remove immediately from queue
if self.maze[r][c] == 'G':
# build path from walking backwards through the visited information
path = []
while r != -1:
path.append((r, c))
r, c = visited[(r, c)]
path.reverse()
return path
# avoid repetition of code: make a loop
for dx, dy in ((-1, 0), (0, -1), (1, 0), (0, 1)):
new_r = r + dy
new_c = c + dx
if (0 <= new_r < len(self.maze) and
0 <= new_c < len(self.maze[0]) and
not (new_r, new_c) in visited and
self.maze[new_r][new_c] != '#'):
visited[(new_r, new_c)] = (r, c)
queue.append((new_r, new_c))
maze = Maze("""############
# S #
##### ######
# #
######## ###
# #
## ##### ###
# G#
############""")
path = maze.main(*maze.get_start())
print(path)
See it run on repl.it
We will be working with graphs, and two players. In this connected graph, the winning condition is that the second player has no other paths to take. The catch is that once a path is taken by a player, it can't be taken again.
Let us assume the initial input is adjacency list (x,y) means x has path to y
The goal is to return a set of vertices that player 1 can choose such that it will always win.
For example, if I have [(1,2), (2,0), (0, 3), (3,2)] and player 1 starts, then we should return [1, 0, 3]. We cannot return 2:
2 --> player 1 starts here
(2,0) --> player 2 goes to 0
(0,3) --> player 1 goes to 3
(3,2) --> player 2 goes to 2
(2,0) --> player 1 cannot go here, already taken
already_visited = []
turn = 1
result = []
def findStarting(L):
global already_visited
global turn
global result
for x,y in L:
allowed = can_visit(L, y) # function tell me which I can visit safely
turn = (turn % 2) + 1 # increment the turn
already_visited.append((x,y)) # we visited this edge
res = findStarting([(x, y)]) # recursive call (search on this node for paths)
if (turn == 2): return True
def can_visit(L, y):
res = []
for a,b in L: if (a==y and (a,b) not in already_visited): res.append((a,b))
return res
I am having trouble with the recursive case. I think what I want to do is return True if we reach a point where turn is 2 and the player has no paths they can take, but I am not sure how to move ahead from here
Here is a simple recursive solution. It is not efficient, it's brute force search without any caching of intermediate states, so it can definitely be made faster, though I don't know if there is an efficient (i.e. non-exponential) solution to this problem.
def firstPlayerWins(g,v):
for i,e in enumerate(g):
if e[0]==v and not firstPlayerWins(g[:i]+g[i+1:],e[1]):
return True
return False
def winningVertices(g):
return [v for v in set(e[0] for e in g) if firstPlayerWins(g,v)]
winningVertices([(1,2), (2,0), (0, 3), (3,2)])
## [0, 2, 3]
This is my pathfinding function:
def get_distance(x1,y1,x2,y2):
neighbors = [(-1,0),(1,0),(0,-1),(0,1)]
old_nodes = [(square_pos[x1,y1],0)]
new_nodes = []
for i in range(50):
for node in old_nodes:
if node[0].x == x2 and node[0].y == y2:
return node[1]
for neighbor in neighbors:
try:
square = square_pos[node[0].x+neighbor[0],node[0].y+neighbor[1]]
if square.lightcycle == None:
new_nodes.append((square,node[1]))
except KeyError:
pass
old_nodes = []
old_nodes = list(new_nodes)
new_nodes = []
nodes = []
return 50
The problem is that the AI takes to long to respond( response time <= 100ms)
This is just a python way of doing https://en.wikipedia.org/wiki/Pathfinding#Sample_algorithm
You should replace your algorithm with A*-search with the Manhattan distance as a heuristic.
One reasonably fast solution is to implement the Dijkstra algorithm (that I have already implemented in that question):
Build the original map. It's a masked array where the walker cannot walk on masked element:
%pylab inline
map_size = (20,20)
MAP = np.ma.masked_array(np.zeros(map_size), np.random.choice([0,1], size=map_size))
matshow(MAP)
Below is the Dijkstra algorithm:
def dijkstra(V):
mask = V.mask
visit_mask = mask.copy() # mask visited cells
m = numpy.ones_like(V) * numpy.inf
connectivity = [(i,j) for i in [-1, 0, 1] for j in [-1, 0, 1] if (not (i == j == 0))]
cc = unravel_index(V.argmin(), m.shape) # current_cell
m[cc] = 0
P = {} # dictionary of predecessors
#while (~visit_mask).sum() > 0:
for _ in range(V.size):
#print cc
neighbors = [tuple(e) for e in asarray(cc) - connectivity
if e[0] > 0 and e[1] > 0 and e[0] < V.shape[0] and e[1] < V.shape[1]]
neighbors = [ e for e in neighbors if not visit_mask[e] ]
tentative_distance = [(V[e]-V[cc])**2 for e in neighbors]
for i,e in enumerate(neighbors):
d = tentative_distance[i] + m[cc]
if d < m[e]:
m[e] = d
P[e] = cc
visit_mask[cc] = True
m_mask = ma.masked_array(m, visit_mask)
cc = unravel_index(m_mask.argmin(), m.shape)
return m, P
def shortestPath(start, end, P):
Path = []
step = end
while 1:
Path.append(step)
if step == start: break
if P.has_key(step):
step = P[step]
else:
break
Path.reverse()
return asarray(Path)
And the result:
start = (2,8)
stop = (17,19)
D, P = dijkstra(MAP)
path = shortestPath(start, stop, P)
imshow(MAP, interpolation='nearest')
plot(path[:,1], path[:,0], 'ro-', linewidth=2.5)
Below some timing statistics:
%timeit dijkstra(MAP)
#10 loops, best of 3: 32.6 ms per loop
The biggest issue with your code is that you don't do anything to avoid the same coordinates being visited multiple times. This means that the number of nodes you visit is guaranteed to grow exponentially, since it can keep going back and forth over the first few nodes many times.
The best way to avoid duplication is to maintain a set of the coordinates we've added to the queue (though if your node values are hashable, you might be able to add them directly to the set instead of coordinate tuples). Since we're doing a breadth-first search, we'll always reach a given coordinate by (one of) the shortest path(s), so we never need to worry about finding a better route later on.
Try something like this:
def get_distance(x1,y1,x2,y2):
neighbors = [(-1,0),(1,0),(0,-1),(0,1)]
nodes = [(square_pos[x1,y1],0)]
seen = set([(x1, y1)])
for node, path_length in nodes:
if path_length == 50:
break
if node.x == x2 and node.y == y2:
return path_length
for nx, ny in neighbors:
try:
square = square_pos[node.x + nx, node.y + ny]
if square.lightcycle == None and (square.x, square.y) not in seen:
nodes.append((square, path_length + 1))
seen.add((square.x, square.y))
except KeyError:
pass
return 50
I've also simplified the loop a bit. Rather than switching out the list after each depth, you can just use one loop and add to its end as you're iterating over the earlier values. I still abort if a path hasn't been found with fewer than 50 steps (using the distance stored in the 2-tuple, rather than the number of passes of the outer loop). A further improvement might be to use a collections.dequeue for the queue, since you could efficiently pop from one end while appending to the other end. It probably won't make a huge difference, but might avoid a little bit of memory usage.
I also avoided most of the indexing by one and zero in favor of unpacking into separate variable names in the for loops. I think this is much easier to read, and it avoids confusion since the two different kinds of 2-tuples had had different meanings (one is a node, distance tuple, the other is x, y).