Related
I was solving Trapping Rain Water Problem from Gfg.
My Approach: For any index I will find the maximum element on the right array and maximum element on the left array.
Then for that respective position I will find the water trapped there and store it in Water[] by using the formula:
water[i]=min(maxL[i],MaxR[i])-array[i] .(see code for more clarity)
At the end i will return the sum of all the elements in Water[].
Code:
class Solution:
def trappingWater(self, arr,n):
maxL=[] #for storing the maximum on left
maxR=[] #for storing the maximum on right
water=[0]*n
for i in range(len(arr)):
if len(maxL)==0:
mx=arr[i]
elif arr[i]>mx:
mx=arr[i]
maxL.append(mx)
for i in range(len(arr)-1,-1,-1):
if len(maxR)==0:
mx=arr[i]
elif arr[i]>mx:
mx=arr[i]
maxR.append(mx)
maxR=maxR[::-1]
for i in range(n):
water[i]=min(maxL[i],maxR[i])-arr[i]
return sum(water)
But as this is a question from stack topic, I want to know how to implement stack in this problem to solve this.
Your approach is fine.
You can use a stack to collect blocks as long as they decrease in height, as they might serve as a left-sided support to hold water.
Contrary to your solution, the water volume is then added by horizontal "spans" between two (potentially remote) blocks. For this to work, it is necessary to also put the x-coordinate of the block on the stack.
An entry gets removed from the stack when its height is not greater than the height of the block we are currently visiting. In that case some water volume needs to be added to the total: pop the lower block from the stack and calculate the volume of water above that block, between the left-support given by the block that is now on the top of the stack (if any) and the current block.
Here is how that looks in script:
class Solution:
def trappingWater(self, arr, n):
stack = []
water = 0
for x, y in enumerate(arr):
while len(stack) > 0 and stack[-1][1] <= y:
x1, y1 = stack.pop() # we will look for water above this block
if stack:
x2, y2 = stack[-1] # this is the left-sided support for holding water
water += (min(y2, y) - y1) * (x - x2 - 1)
stack.append((x, y))
return water
I have an matrix with values in each cell (minimum value=1), where the maximum value is 'max'.
At a time, I modify each cell value by the highest value of its neighboring cells i.e. all 8 neighbors, and this occurs for the whole matrix, simultaneously. I want to find after what minimum number of iterations after which value of all cells will be max.
One brute force method of doing this is by padding the matrix by zeros, and
for i in range (1,x_max+1):
for j in range(1,y_max+1):
maximum = 0
for k in range(-1,2):
for l in range(-1,2):
if matrix[i+k][j+l]>maximum:
maximum = matrix[i+k][j+l]
matrix[i][j] = maximum
But is there an intelligent and faster way of doing this?
Thanks in advance.
I think this can be solved by BFS(Breadth first Search).
Start BFS simulatneously with all the matrix cells with 'max' value.
dis[][] == infinite // min. distance of cell from nearest cell with 'max' value, initially infinite for all
Q // Queue
M[][] // matrix
for all i,j // travers the matrix, enqueue all cells with 'max'
if M[i][j] == 'max'
dis[i][j] = 0 , Q.push( cell(i,j) )
while !Q.empty:
cell Current = Q.front
for all neighbours Cell(p,q) of Current:
if dis[p][q] == infinite
dis[p][q] = dis[Current.row][Current.column] + 1
Q.push( cell(p,q))
Q.pop()
The cell with max(dis[i][j]) for all i,j will be the no. of iterations needed.
Use an array with a "border".
Testing the edge conditions is tedious and can be avoided by making the array 1-bigger around the edge, each element with the value of INT_MIN.
Additionally, consider 8 tests, rather than a double nested loop
// Data is in matrix[1...N][1...M], yet is size matrix[N+2][M+2]
for (i=1; i <= N; i++) {
for (j=1; j <= M; j++) {
maximum = matrix[i-1][j-l];
if (matrix[i-1][j+0] > maximum) maximum = matrix[i-1][j+0];
if (matrix[i-1][j+1] > maximum) maximum = matrix[i-1][j+1];
if (matrix[i+0][j-1] > maximum) maximum = matrix[i+0][j-1];
if (matrix[i+0][j+0] > maximum) maximum = matrix[i+0][j+0];
if (matrix[i+0][j+1] > maximum) maximum = matrix[i+0][j+1];
if (matrix[i+1][j-1] > maximum) maximum = matrix[i+1][j-1];
if (matrix[i+1][j+0] > maximum) maximum = matrix[i+1][j+0];
if (matrix[i+1][j+1] > maximum) maximum = matrix[i+1][j+1];
newmatrix[i][j] = maximum
All existing answers require examining every cell in the matrix. If you don't already know what the locations of the maximum value are, this is unavoidable, and in that case, Amit Kumar's BFS algorithm has optimal time complexity: O(wh), if the matrix has width w and height h.
OTOH, perhaps you already know the locations of the k maximum values, and k is relatively small. In that case, the following algorithm will find the answer in just O(k^2*(log(k)+log(max(w, h)))) time, which is much faster when either w or h is large. It doesn't actually look at any matrix entries; instead, it runs a binary search to look for candidate stopping times (that is, answers). For each candidate stopping time it builds the set of rectangles that would be occupied by max by that time, and checks whether any matrix cell remains uncovered by a rectangle.
To explain the idea, we first need some terms. Call the top row of a rectangle a "starting vertical event", and the row below its bottom edge an "ending vertical event". A "basic interval" is the interval of rows spanned by any pair of vertical events that does not have a third vertical event anywhere between them (the event pairs defining these intervals can be from the same or different rectangles). Notice that with k rectangles, there can never be more than 2k+1 basic intervals -- there is no dependence here on h.
The basic idea is to walk left-to-right through the columns of the matrix that correspond to horizontal events: columns in which either a new rectangle "starts" (the left vertical edge of a rectangle), or an existing rectangle "finishes" (the column to the right of the right vertical edge of a rectangle), keeping track of how many rectangles are currently covering every basic interval. If we ever detect a basic interval covered by 0 rectangles, we can stop: we have found a column containing one or more cells that are not yet covered at time t. If we get to the right edge of the matrix without this happening, then all cells are covered at time t.
Here is pseudocode for a function that checks whether any matrix cell remains uncovered by time t, given a length-k array peak, where (peak[i].x, peak[i].y) is the location of the i-th max-containing cell in the original matrix, in increasing order of x co-ordinate (so the leftmost max-containing cell is at (peak[1].x, peak[1].y)).
Function IsMatrixCovered(t, peak[]) {
# Discover all vertical events and basic intervals
Let vertEvents[] be an empty array of integers.
For i from 1 to k:
top = max(1, peak[i].y - t)
bot = min(h, peak[i].y + t)
Append top to vertEvents[]
Append bot+1 to vertEvents[]
Sort vertEvents in increasing order, and remove duplicates.
x = 1
Let horizEvents[] be an empty array of { col, type, top, bot } structures.
For i from 1 to k:
# Calculate the (clipped) rectangle that peak[i] will cover at time t:
lft = max(1, peak[i].x - t)
rgt = min(w, peak[i].x + t)
top = max(1, peak[i].y - t)
bot = min(h, peak[i].y + t)
# Convert vertical positions to vertical event indices
top = LookupIndexUsingBinarySearch(top, vertEvents[])
bot = LookupIndexUsingBinarySearch(bot+1, vertEvents[])
# Record horizontal events
Append (lft, START, top, bot) to horizEvents[]
Append (rgt+1, STOP, top, bot) to horizEvents[]
Sort horizEvents in increasing order by its first 2 fields, with START considered < STOP.
# Walk through all horizontal events, from left to right.
Let basicIntervals[] be an array of size(vertEvents[]) integers, initially all 0.
nOccupiedBasicIntervalsFirstCol = 0
For i from 1 to size(horizEvents[]):
If horizEvents[i].type = START:
d = 1
Else (if it is STOP):
d = -1
If horizEvents[i].col <= w:
For j from horizEvents[i].top to horizEvents[i].bot:
If horizEvents[i].col = 1 and basicIntervals[j] = 0:
++nOccupiedBasicIntervalsFirstCol # Must be START
basicIntervals[j] += d
If basicIntervals[j] = 0:
return FALSE
If nOccupiedBasicIntervalsFirstCol < size(basicIntervals):
return FALSE # Could have checked earlier, but the code is simpler this way
return TRUE
}
The above function can simply be called inside a binary search on t, that looks for the smallest value of t for which the function returns TRUE.
A further factor of k/log(k) could be removed by exploiting the fact that the set of basic intervals affected by any rectangle starting or ending is always an interval, through the use of Fenwick trees.
I am trying to find an efficient solution for finding overlapping of n rectangles where rectangles are stored in two separate lists. We are looking for all rectangles in listA that overlap with rectangles in listB (and vice versa). Comparing one element from the first list to second list could take immensely large amount of time. I am looking for an efficient solution.
I have two list of rectangles
rect = Rectangle(10, 12, 56, 15)
rect2 = Rectangle(0, 0,1, 15)
rect3 = Rectangle (10, 12, 56, 15)
listA = [rect, rect2]
listB = [rect3]
which is created from the class:
import numpy as np
import itertools as it
class Rectangle(object):
def __init__(self, left, right, bottom, top):
self.left = left
self.bottom = right
self.right = bottom
self.top = top
def overlap(r1, r2):
hoverlaps = True
voverlaps = True
if (r1.left > r2.right) or (r1.right < r2.left):
hoverlaps = False
if (r1.top < r2.bottom) or (r1.bottom > r2.top):
voverlaps = False
return hoverlaps and voverlaps
I need to compare rectangle in listA to listB the code goes like this which is highly inefficient - comparing one by one
for a in it.combinations(listB):
for b in it.combinations(listA):
if a.overlap(b):
Any better efficient method to deal with the problem?
First off: As with many a problem from computational geometry, specifying the parameters for order-of-growth analysis needs care: calling the lengths of the lists m and n, the worst case in just those parameters is Ω(m×n), as all areas might overlap (in this regard, the algorithm from the question is asymptotically optimal). It is usual to include the size of the output: t = f(m, n, o) (Output-sensitive algorithm).
Trivially, f ∈ Ω(m+n+o) for the problem presented.
Line Sweep is a paradigm to reduce geometrical problems by one dimension - in its original form, from 2D to 1D, plane to line.
Imagine all the rectangles in the plane, different colours for the lists.
Now sweep a line across this plane - left to right, conventionally, and infinitesimally further to the right "for low y-coordinates" (handle coordinates in increasing x-order, increasing y-order for equal x).
For all of this sweep (or scan), per colour keep one set representing the "y-intervals" of all rectangles at the current x-coordinate, starting empty. (In a data structure supporting insertion, deletion, and enumerating all intervals that overlap a query interval: see below.)
Meeting the left side of a rectangle, add the segment to the data structure for its colour. Report overlapping intervals/rectangles in any other colour.
At a right side, remove the segment.
Depending on the definition of "overlapping", handle left sides before right sides - or the other way round.
There are many data structures supporting insertion and deletion of intervals, and finding all intervals that overlap a query interval. Currently, I think Augmented Search-Trees may be easiest to understand, implement, test, analyse…
Using this, enumerating all o intersecting pairs of axis-aligned rectangles (a, b) from listA and listB should be possible in O((m+n)log(m+n)+o) time and O(m+n) space. For sizeable problem instances, avoid data structures needing more than linear space ((original) Segment Trees, for one example pertaining to interval overlap).
Another paradigm in algorithm design is Divide&Conquer: with a computational geometry problem, choose one dimension in which the problem can be divided into independent parts, and a coordinate such that the sub-problems for "coordinates below" and "coordinates above" are close in expected run-time. Quite possibly, another (and different) sub-problem "including the coordinate" needs to be solved. This tends to be beneficial when a) the run-time for solving sub-problems is "super-log-linear", and b) there is a cheap (linear) way to construct the overall solution from the solutions for the sub-problems.
This lends itself to concurrent problem solving, and can be used with any other approach for sub-problems, including line sweep.
There will be many ways to tweak each approach, starting with disregarding input items that can't possibly contribute to the output. To "fairly" compare implementations of algorithms of like order of growth, don't aim for a fair "level of tweakedness": try to invest fair amounts of time for tweaking.
A couple of potential minor efficiency improvements. First, fix your overlap() function, it potentially does calculations it needn't:
def overlap(r1, r2):
if r1.left > r2.right or r1.right < r2.left:
return False
if r1.top < r2.bottom or r1.bottom > r2.top:
return False
return True
Second, calculate the contaning rectangle for one of the lists and use it to screen the other list -- any rectangle that doesn't overlap the container doesn't need to be tested against all the rectangles that contributed to it:
def containing_rectangle(rectangles):
return Rectangle(min(rectangles, key=lambda r: r.left).left,
max(rectangles, key=lambda r: r.right).right,
min(rectangles, key=lambda r: r.bottom).bottom,
max(rectangles, key=lambda r: r.top).top
)
c = containing_rectangle(listA)
for b in listB:
if b.overlap(c):
for a in listA:
if b.overlap(a):
In my testing with hundreds of random rectangles, this avoided comparisons on the order of single digit percentages (e.g. 2% or 3%) and occasionally increased the number of comparisons. However, presumably your data isn't random and might fare better with this type of screening.
Depending on the nature of your data, you could break this up into a container rectangle check for each batch of 10K rectangles out of 50K or what ever slice gives you maximum efficiency. Possibly presorting the rectangles (e.g. by their centers) before assigning them to container batches.
We can break up and batch both lists with container rectangles:
listAA = [listA[x:x + 10] for x in range(0, len(listA), 10)]
for i, arrays in enumerate(listAA):
listAA[i] = [containing_rectangle(arrays)] + arrays
listBB = [listB[x:x + 10] for x in range(0, len(listB), 10)]
for i, arrays in enumerate(listBB):
listBB[i] = [containing_rectangle(arrays)] + arrays
for bb in listBB:
for aa in listAA:
if bb[0].overlap(aa[0]):
for b in bb[1:]:
if b.overlap(aa[0]):
for a in aa[1:]:
if b.overlap(a):
With my random data, this decreased the comparisons on the order of 15% to 20%, even counting the container rectangle comparisons. The batching of rectangles above is arbitrary and you can likely do better.
The exception you're getting comes from the last line of the code you show. The expression list[rect] is not valid, since list is a class, and the [] syntax in that context is trying to index it. You probably want just [rect] (which creates a new list containing the single item rect).
There are several other basic issues, with your code. For instance, your Rect.__init__ method doesn't set a left attribute, which you seem to expect in your collision testing method. You've also used different capitalization for r1 and r2 in different parts of the overlap method (Python doesn't consider r1 to be the same as R1).
Those issues don't really have anything to do with testing more than two rectangles, which your question asks about. The simplest way to do that (and I strongly advise sticking to simple algorithms if you're having basic issues like the ones mentioned above), is to simply compare each rectangle with each other rectangle using the existing pairwise test. You can use itertools.combinations to easily get all pairs of items from an iterable (like a list):
list_of_rects = [rect1, rect2, rect3, rect4] # assume these are defined elsewhere
for a, b in itertools.combinations(list_of_rects, 2):
if a.overlap(b):
# do whatever you want to do when two rectangles overlap here
This implementation using numpy is about 35-40 times faster according to a test I did. For 2 lists each with 10000 random rectangles this method took 2.5 secs and the method in the question took ~90 sec. In terms of complexity it's still O(N^2) like the method in the question.
import numpy as np
rects1=[
[0,10,0,10],
[0,100,0,100],
]
rects2=[
[20,50,20,50],
[200,500,200,500],
[0,12,0,12]
]
data=np.asarray(rects2)
def find_overlaps(rect,data):
data=data[data[::,0]<rect[1]]
data=data[data[::,1]>rect[0]]
data=data[data[::,2]<rect[3]]
data=data[data[::,3]>rect[2]]
return data
for rect in rects1:
overlaps = find_overlaps(rect,data)
for overlap in overlaps:
pass#do something here
Obviously, if your list (at least listB) is sorted by r2.xmin, you can search for r1.xmax in listB and stop testing overlap of r1 in this listB (the rest will be to the right). This will be O(n·log(n)).
A sorted vector has faster access than a sorted list.
I'm supposing that the rectangles edges are oriented same as axis.
Also fix your overlap() function as cdlane explained.
If you know the upper and lower limits for coordinates, you can narrow the search by partitioning the coordinate space into squares e.g. 100x100.
Make one "set" per coordinate square.
Go through all squares, putting them in the "set" of any square they overlap.
See also Tiled Rendering which uses partitions to speed up graphical operations.
// Stores rectangles which overlap (x, y)..(x+w-1, y+h-1)
public class RectangleSet
{
private List<Rectangle> _overlaps;
public RectangleSet(int x, int y, int w, int h);
}
// Partitions the coordinate space into squares
public class CoordinateArea
{
private const int SquareSize = 100;
public List<RectangleSet> Squares = new List<RectangleSet>();
public CoordinateArea(int xmin, int ymin, int xmax, int ymax)
{
for (int x = xmin; x <= xmax; x += SquareSize)
for (int y = ymin; y <= ymax; y += SquareSize)
{
Squares.Add(new RectangleSet(x, y, SquareSize, SquareSize);
}
}
// Adds a list of rectangles to the coordinate space
public void AddRectangles(IEnumerable<Rectangle> list)
{
foreach (Rectangle r in list)
{
foreach (RectangleSet set in Squares)
{
if (r.Overlaps(set))
set.Add(r);
}
}
}
}
Now you have a much smaller set of rectangles for comparison, which should speed things up nicely.
CoordinateArea A = new CoordinateArea(-500, -500, +1000, +1000);
CoordinateArea B = new CoordinateArea(-500, -500, +1000, +1000); // same limits for A, B
A.AddRectangles(listA);
B.AddRectangles(listB);
for (int i = 0; i < listA.Squares.Count; i++)
{
RectangleSet setA = A[i];
RectangleSet setB = B[i];
// *** small number of rectangles, which you can now check thoroghly for overlaps ***
}
I think you have to setup an additional data structure (spatial index) in order to have fast access to nearby rectangles that potentially overlap in order to reduce the time complexity from quadratic to linearithmic.
See also:
https://en.wikipedia.org/wiki/Spatial_database
Spatial Index for Rectangles With Fast Insert
find overlapping rectangles algorithm
Here is what I use to calculate overlap areas of many candidate rectangles (with candidate_coords [[l, t, r, b], ...]) with a target one (target_coords [l, t, r, b]):
comb_tensor = np.zeros((2, candidate_coords.shape[0], 4))
comb_tensor[0, :] = target_coords
comb_tensor[1] = candidate_coords
dx = np.amin(comb_tensor[:, :, 2].T, axis=1) - np.amax(comb_tensor[:, :, 0].T, axis=1)
dy = np.amin(comb_tensor[:, :, 3].T, axis=1) - np.amax(comb_tensor[:, :, 1].T, axis=1)
dx[dx < 0] = 0
dy[dy < 0] = 0
overlap_areas = dx * dy
This should be fairly efficient especially if there are many candidate rectangles as all is done using numpy functions operating on ndarrays. You can either do a loop calculating the overlap areas or perhaps add one more dimension to comb_tensor.
I think the below code will be useful.
print("Identifying Overlap between n number of rectangle")
#List to be used in set and get_coordinate_checked_list
coordinate_checked_list = []
def get_coordinate_checked_list():
#returns the overlapping coordinates of rectangles
"""
:return: list of overlapping coordinates
"""
return coordinate_checked_list
def set_coordinate_checked_list(coordinates):
#appends the overlapping coordinates of rectangles
"""
:param coordinates: list of overlapping coordinates to be appended in coordinate_checked_list
:return:
"""
coordinate_checked_list.append(coordinates)
def overlap_checked_for(coordinates):
# to find rectangle overlap is already checked, if checked "True" will be returned else coordinates will be added
# to coordinate_checked_list and return "False"
"""
:param coordinates: coordinates of two rectangles
:return: True if already checked, else False
"""
if coordinates in get_coordinate_checked_list():
return True
else:
set_coordinate_checked_list(coordinates)
return False
def __isRectangleOverlap(R1, R2):
#checks if two rectangles overlap
"""
:param R1: Rectangle1 with cordinates [x0,y0,x1,y1]
:param R2: Rectangle1 with cordinates [x0,y0,x1,y1]
:return: True if rectangles overlaps else False
"""
if (R1[0] >= R2[2]) or (R1[2] <= R2[0]) or (R1[3] <= R2[1]) or (R1[1] >= R2[3]):
return False
else:
print("Rectangle1 {} overlaps with Rectangle2 {}".format(R1,R2))
return True
def __splitByHeightandWidth(rectangles):
# Gets the list of rectangle, divide the paged with respect to height and width and position
# the rectangle in suitable section say left_up,left_down,right_up,right_down and returns the list of rectangle
# grouped with respect to section
"""
:param rectangles: list of rectangle coordinates each designed as designed as [x0,y0,x1,y1]
:return:list of rectangle grouped with respect to section, suspect list which holds the rectangles
positioned in more than one section
"""
lu_Rect = []
ll_Rect = []
ru_Rect = []
rl_Rect = []
sus_list = []
min_h = 0
max_h = 0
min_w = 0
max_w = 0
value_assigned = False
for rectangle in rectangles:
if not value_assigned:
min_h = rectangle[1]
max_h = rectangle[3]
min_w = rectangle[0]
max_w = rectangle[2]
value_assigned = True
if rectangle[1] < min_h:
min_h = rectangle[1]
if rectangle[3] > max_h:
max_h = rectangle[3]
if rectangle[0] < min_w:
min_w = rectangle[0]
if rectangle[2] > max_w:
max_w = rectangle[2]
for rectangle in rectangles:
if rectangle[3] <= (max_h - min_h) / 2:
if rectangle[2] <= (max_w - min_w) / 2:
ll_Rect.append(rectangle)
elif rectangle[0] >= (max_w - min_w) / 2:
rl_Rect.append(rectangle)
else:
# if rectangle[0] < (max_w - min_w) / 2 and rectangle[2] > (max_w - min_w) / 2:
ll_Rect.append(rectangle)
rl_Rect.append(rectangle)
sus_list.append(rectangle)
if rectangle[1] >= (max_h - min_h) / 2:
if rectangle[2] <= (max_w - min_w) / 2:
lu_Rect.append(rectangle)
elif rectangle[0] >= (max_w - min_w) / 2:
ru_Rect.append(rectangle)
else:
# if rectangle[0] < (max_w - min_w) / 2 and rectangle[2] > (max_w - min_w) / 2:
lu_Rect.append(rectangle)
ru_Rect.append(rectangle)
sus_list.append(rectangle)
if rectangle[1] < (max_h - min_h) / 2 and rectangle[3] > (max_h - min_h) / 2:
if rectangle[0] < (max_w - min_w) / 2 and rectangle[2] > (max_w - min_w) / 2:
lu_Rect.append(rectangle)
ll_Rect.append(rectangle)
ru_Rect.append(rectangle)
rl_Rect.append(rectangle)
sus_list.append(rectangle)
elif rectangle[2] <= (max_w - min_w) / 2:
lu_Rect.append(rectangle)
ll_Rect.append(rectangle)
sus_list.append(rectangle)
else:
# if rectangle[0] >= (max_w - min_w) / 2:
ru_Rect.append(rectangle)
rl_Rect.append(rectangle)
sus_list.append(rectangle)
return [lu_Rect, ll_Rect, ru_Rect, rl_Rect], sus_list
def find_overlap(rectangles):
#Find all possible overlap between the list of rectangles
"""
:param rectangles: list of rectangle grouped with respect to section
:return:
"""
split_Rectangles , sus_list = __splitByHeightandWidth(rectangles)
for section in split_Rectangles:
for rect in range(len(section)-1):
for i in range(len(section)-1):
if section[0] and section[i+1] in sus_list:
if not overlap_checked_for([section[0],section[i+1]]):
__isRectangleOverlap(section[0],section[i+1])
else:
__isRectangleOverlap(section[0],section[i+1])
section.pop(0)
arr =[[0,0,2,2],[0,0,2,7],[0,2,10,3],[3,0,4,1],[6,1,8,8],[0,7,2,8],[4,5,5,6],[4,6,10,7],[9,3,10,5],[5,3,6,4],[4,3,6,5],[4,3,5`enter code here`,6]]
find_overlap(arr)
For a simple solution that improves on pure brute force if the rectangles are relatively sparse:
sort all Y ordinates in a single list, and for every ordinate store the index of the rectangle, the originating list and a flag to distinguish bottom and top;
scan the list from bottom to top, maintaining two "active lists", one per rectangle set;
when you meet a bottom, insert the rectangle index in its active list and compare to all rectangles in the other list to detect overlaps on X;
when you meet a top, remove the rectangle index from its active list.
Assuming simple linear lists, the updates and searches will take time linear in the size of the active lists. So instead of M x N comparisons, you will perform M x n + m x N comparisons, where m and n denote the average list sizes. (If the rectangles do not overlap within their set, one can expect an average list length not exceeding √M and √N.)
I am creating a program that involves a rectangle class, as well as a point and canvas class. One of the smaller functions asked of us is a common point function which determines if the canvas at hand has a common point throughout its array of rectangles. I am getting a list index error but i cannot see why.
Here is the common point function:
def common_point(self):
'''(Rectangle) -> Boolean
Returns True if rectangles in canvas all ahare a common point'''
common = False
for i in range(len(self.data)):
for j in range(len(self.data),-1,-1):
if self.data[i].intersects(self.data[j]) == True:
common = True
else:
return False
return common
and here is the intersect function it is calling:
def intersects(self,other):
'''(Rectangle, Rectangle) -> Boolean
Returns True if the two Rectangles intersect'''
return not(self.p2.y < other.p1.y or self.p1.y > other.p2.y or self.p2.x < other.p1.x or self.p1.x > other.p2.x)
any help as to why would be greatly appreciated
range(len(self.data),-1,-1) returns a list with length of len(self.data) + 1. This will always result in an index error when iterating using that range.
I suspect what you want is:
for j in range(len(self.data) - 1, -1, -1):
Vexed is a popular puzzle game, with many versions available (some of them GPL free software). It is very suitable for small screen devices; versions are available for Android, iOS, etc. I discovered it on the PalmOS platform.
Just for fun, I'd like to write a solver that will solve Vexed levels.
Vexed is a block-sliding puzzle game. Here are the rules in a nutshell:
0) Each level is a grid of squares, bounded by an impassible border. In any level there will be some solid squares, which are impassible. There are some number of blocks of various colors; these could be resting on the bottom border, resting on solid squares, or resting on other blocks (of a different color). Most levels are 8x8 or smaller.
1) The only action you can take is to slide a block to the left or to the right. Each square traveled by a block counts as one move.
2) There is gravity. If, after you slide a block, it is no longer resting on a solid square or another block, it will fall until it comes to rest on another block, a solid square, or the bottom border. Note that you cannot ever lift it up again.
3) Any time two or more blocks of the same color touch, they disappear. Note that chains are possible: if a supporting block disappears, blocks that rested upon it will fall, which could lead to more blocks of the same color touching and thus disappearing.
4) The goal is to make all blocks disappear in the minimum number of moves. Each level has a "par score" which tells you the minimum number of moves. (In the original PalmOS game, the "par score" wasn't necessarily the minimum, but in the Android version I play these days it is the minimum.)
Here is the SourceForge project with the source for the PalmOS version of the game:
http://sourceforge.net/projects/vexed/
I'm an experienced software developer, but I haven't done really any work in AI sort of stuff (pathfinding, problem-solving, etc.) So I'm looking for advice to get me pointed in the right direction.
At the moment, I can see two basic strategies for me to pursue:
0) Just write a brute-force solver, probably in C for the speed, that cranks through every possible solution for every game and returns a list of all solutions, best one first. Would this be a reasonable approach, or would the total number of possible moves make this too slow? I don't think any levels exist larger than 10x10.
1) Learn some AI-ish algorithms, and apply them in a clever way to solve the problem, probably using Python.
Note that the source for PalmOS Vexed includes a solver. According to the author, "The solver uses A* with pruning heuristics to find solutions."
http://www.scottlu.com/Content/Vexed.html
So, one strategy I could pursue would be to study the A* algorithm and then study the C++ code for the existing solver and try to learn from that.
I'm going to tag this with Python and C tags, but if you think I should be using something else, make your sales pitch and I'll consider it!
Here is ASCII art of a level from "Variety 25 Pack"; level 48, "Dark Lord". I am able to solve most levels but this one has, well, vexed me. Par score for this level is 25 moves, but I have not yet solved it at all!
__________
|## g####|
|## # b##|
|## # p##|
|#g ###|
|bp ###|
|p# p g |
==========
In this picture, the borders are underscores, vertical bars, and equals characters. Filled-in squares are '#'. Open spaces are space characters. Colored blocks are 'g' (green), 'b' (blue) and 'p' (purple).
By the way, I'll likely make the input file format to the solver be ASCII art of the levels, just like this but without the fussy line border characters.
Thanks for any advice!
EDIT:
I have accepted an answer. Thank you to the people who gave me answers.
This is a semi-brute-force solver. It isn't using A* but it is cutting short unprofitable branches of the tree.
It reads in a simple text file with the level data. A letter is a block, a '_' (underscore) is an open space, and a '#' is a filled-in space.
#!/usr/bin/env python
#
# Solve levels from the game Vexed.
from collections import Counter
import sys
level_blocks = set(chr(x) for x in range(ord('a'), ord('z')+1))
level_other = set(['_', '#'])
level_valid = set().union(level_blocks, level_other)
def prn_err(s='\n'):
sys.stderr.write(s)
sys.stderr.flush()
def validate_llc(llc):
if len(llc) == 0:
raise ValueError, "need at least one row of level data"
w = len(llc[0])
if w < 2:
raise ValueError, "level data not wide enough"
for i, row in enumerate(llc):
if len(row) != w:
s = "level data: row %d is different length than row 0"
raise ValueError, s % i
for j, ch in enumerate(row):
if ch not in level_valid:
s = "char '%c' at (%d, %d) is invalid" % (ch, i, j)
raise ValueError, s
class Info(object):
pass
info = Info()
info.width = 0
info.height = 0
info.spaces = set()
info.boom_blocks = set()
info.best_solution = 9999999999
info.title = "unknown"
class Level(object):
"""
Hold the state of a level at a particular move. self.parent points
to the previous state, from a previous move, so the solver builds a
tree representing the moves being considered. When you reach a solution
(a state where there are no more blocks) you can walk up the tree
back to the root, and you have the chain of moves that leads to that
solution."""
def __init__(self, x):
if isinstance(x, Level):
self.blocks = dict(x.blocks)
self.colors = dict(x.colors)
self.parent = x
self.s_move = ''
self.rank = x.rank + 1
else:
if isinstance(x, basestring):
# allow to init from a one-line "data" string
# example: "___;___;r_r"
x = x.split(';')
# build llc: list of rows, each row a list of characters
llc = [[ch for ch in row.strip()] for row in x]
llc.reverse()
info.width = len(llc[0])
info.height = len(llc)
validate_llc(llc)
# Use llc data to figure out the level, and build self.blocks
# and info.spaces. self.blocks is a dict mapping a coordinate
# tuple to a block color; info.spaces is just a set of
# coordinate tuples.
self.blocks = {}
for y in range(info.height):
for x in range(info.width):
loc = (x, y)
c = llc[y][x]
if c == '_':
# it's a space
info.spaces.add(loc)
elif c in level_blocks:
# it's a block (and the block is in a space)
self.blocks[loc] = c
info.spaces.add(loc)
else:
# must be a solid square
assert(c == '#')
# colors: map each block color onto a count of blocks.
self.colors = Counter(self.blocks.values())
# parent: points to the level instance that holds the state
# previous to the state of this level instance.
self.parent = None
# s_move: a string used when printing out the moves of a solution
self.s_move = 'initial state:'
# rank: 0 == initial state, +1 for each move
self.rank = 0
self.validate()
print "Solving:", info.title
print
sys.stdout.flush()
if self._update():
print "level wasn't stable! after updating:\n%s\n" % str(self)
def lone_color(self):
return any(count == 1 for count in self.colors.values())
def is_solved(self):
return sum(self.colors.values()) == 0
def validate(self):
if info.height == 0:
raise ValueError, "need at least one row of level data"
if info.width < 2:
raise ValueError, "level data not wide enough"
if self.lone_color():
raise ValueError, "cannot have just one of any block color"
for x, y in info.spaces:
if not 0 <= x < info.width or not 0 <= y < info.height:
raise ValueError, "Bad space coordinate: " + str(loc)
for x, y in self.blocks:
if not 0 <= x < info.width or not 0 <= y < info.height:
raise ValueError, "Bad block coordinate: " + str(loc)
if any(count < 0 for count in self.colors.values()):
raise ValueError, "cannot have negative color count!"
colors = Counter(self.blocks.values())
for k0 in [key for key in self.colors if self.colors[key] == 0]:
del(self.colors[k0]) # remove all keys whose value is 0
if colors != self.colors:
raise ValueError, "self.colors invalid!\n" + str(self.colors)
def _look(self, loc):
"""
return color at location 'loc', or '_' if empty, or '#' for a solid sqaure.
A bad loc does not raise an error; it just returns '#'.
"""
if loc in self.blocks:
return self.blocks[loc]
elif loc in info.spaces:
return '_'
else:
return '#'
def _lookxy(self, x, y):
loc = x, y
return self._look(loc)
def _board_mesg(self, mesg, loc):
x, y = loc
return "%s %c(%d,%d)" % (mesg, self._look(loc), x, y)
def _blocked(self, x, y):
return self._lookxy(x, y) != '_'
def _s_row(self, y):
return ''.join(self._lookxy(x, y) for x in xrange(info.width))
def data(self, ch_join=';'):
return ch_join.join(self._s_row(y)
for y in xrange(info.height - 1, -1, -1))
# make repr() actually print a representation
def __repr__(self):
return type(self).__name__ + "(%s)" % self.data()
# make str() work
def __str__(self):
return self.data('\n')
def _move_block(self, loc_new, loc_old):
self.blocks[loc_new] = self.blocks[loc_old]
del(self.blocks[loc_old])
def _explode_block(self, loc):
if loc in info.boom_blocks:
return
info.boom_blocks.add(loc)
color = self.blocks[loc]
self.colors[color] -= 1
def _try_move(self, loc, d):
x, y = loc
if not d in ('<', '>'):
raise ValueError, "d value '%c' invalid, must be '<' or '>'" % d
if d == '<':
x_m = (x - 1)
else:
x_m = (x + 1)
y_m = y
loc_m = (x_m, y_m)
if self._blocked(x_m, y_m):
return None # blocked, so can't move there
# Not blocked. Let's try the move!
# Make a duplicate level...
m = Level(self)
# ...try the move, and see if anything falls or explodes...
m._move_block(loc_m, loc)
m._update()
if m.lone_color():
# Whoops, we have only one block of some color. That means
# no solution can be found by considering this board.
return None
# finish the update
m.s_move = self._board_mesg("move:", loc) + ' ' + d
m.parent = self
return m
def _falls(self, loc):
x, y = loc
# blocks fall if they can, and only explode when at rest.
# gravity loop: block falls until it comes to rest
if self._blocked(x, y - 1):
return False # it is already at rest
while not self._blocked(x, y - 1):
# block is free to fall so fall one step
y -= 1
loc_m = (x, y)
self._move_block(loc_m, loc)
return True # block fell to new location
def _explodes(self, loc):
x, y = loc
exploded = False
color = self._look(loc)
# look left, right, up, and down for blocks of same color
for e_loc in [(x-1, y), (x+1, y), (x, y-1)]:
if e_loc in self.blocks and self.blocks[e_loc] == color:
self._explode_block(e_loc)
exploded = True
if exploded:
self._explode_block(loc)
return exploded
def _update(self):
c = 0
while True:
# TRICKY: sum() works on functions that return a bool!
# As you might expect, True sums as 1 and False as 0.
f = sum(self._falls(loc) for loc in self.blocks)
e = sum(self._explodes(loc) for loc in self.blocks)
for loc in info.boom_blocks:
del(self.blocks[loc])
info.boom_blocks.clear()
c += f + e
if (f + e) == 0:
# no blocks fell or exploded; board is stable, update is done
break
return c
def print_moves(self):
lst = [self]
a = self
while a.parent:
a = a.parent
lst.append(a)
lst.reverse()
for i, a in enumerate(lst):
if i:
print "Move %d of %d" % (i, len(lst) - 1)
print a.s_move
print a
print
def solve(self):
c = 0
seen = set()
solutions = []
seen.add(self.data())
q = []
if self.is_solved():
solutions.append(self)
else:
q.append(self)
while q:
a = q.pop(0)
# Show dots while solver is 'thinking' to give a progress
# indicator. Dots are written to stderr so they will not be
# captured if you redirect stdout to save the solution.
c += 1
if c % 100 == 0:
prn_err('.')
if a.rank > info.best_solution:
# We cannot beat or even match the best solution.
# No need to think any more about this possibility.
# Just prune this whole branch of the solution tree!
continue
for loc in a.blocks:
for d in ('<', '>'):
m = a._try_move(loc, d)
if not m or m.data() in seen:
continue
if m.is_solved():
if info.best_solution > a.rank:
print "\nnew best solution: %d moves" % m.rank
info.best_solution = a.rank
else:
print "\nfound another solution: %d moves" % m.rank
solutions.append(m)
else:
seen.add(m.data())
q.append(m)
print
print "Considered %d different board configurations." % c
print
solutions.sort(key=lambda a: a.rank)
for n, a in enumerate(solutions):
print "solution %d): %d moves" % (n, a.rank)
a.print_moves()
if not solutions:
print "no solutions found!"
def load_vex_file(fname):
with open(fname, "rt") as f:
s = f.next().strip()
if s != "Vexed level":
raise ValueError, "%s: not a Vexed level file" % fname
s = f.next().strip()
if not s.startswith("title:"):
raise ValueError, "%s: missing title" % fname
info.title = s[6:].lstrip() # remove "title:"
for s in f:
if s.strip() == "--":
break
return Level(f)
if __name__ == "__main__":
if len(sys.argv) == 1:
print "Usage vexed_solver <vexed_level_file.vex>"
sys.exit(1)
fname = sys.argv[1]
level = load_vex_file(fname)
level.solve()
Here is an example level file:
Vexed level
title: 25-48, "Dark Lord"
--
##_f####
##_#_c##
##_#_p##
#f___###
cp___###
p#_p_f__
On my computer, it solves "Dark Lord" in almost exactly 10 seconds, considering 14252 different board configurations. I wrote in Python 2.x instead of Python 3, because I want to try this with PyPy and see how fast it becomes.
Next I should work on applying A* to this. I guess I can make a metric like "better to move an orange block toward another orange block than away" and try to work that in. But I do want all the solutions to pop out, so maybe I'm done already. (If there are three solutions that are all the minimum number of moves, I want to see all three.)
I welcome comments on this Python program. I had fun writing it!
EDIT: I did try this with PyPy but I never updated this until now. On the computer I used with PyPy, the solver could solve the "Dark Lord" level in 10 seconds using CPython; that dropped to 4 seconds with PyPy. The cool part is that I could see the speedup as the JIT kicked in: this program prints dots as it is working, and under PyPy I can see the dots start out slower and then just accelerate. PyPy is nifty.
Studying Wikipedia may be better than studying the actual source code. A* is written out pretty clearly there. But that feels like cheating, doesn't it?
As all good ideas, A* is actually pretty obvious in retrospective. It's fun trying to work it through, and there are a few nice insights along the way. Here's how you get to it:
Write the brute-force solver. You'll need much of what you write in the more advanced versions: a game state, and a description of getting from one state to another. You'll also end up removing duplicate states. You should have a queue of some sort for states to be considered, a set of states you've already done, and structure to hold the best solution(s) found so far. And a method that takes a state from the queue and generates a state's „neighbor“ states (ones reachable from it). That's the basic structure of classical AI algorithms. Note that you're technically „generating“ or „exploring“ a huge graph here.
After that, add a simple pruning algorithm: if a state has only one block of some color left, there's no need to consider it further. See if you can come up with other pruning algorithms (i.e. ones that mark a state as „unsolvable“). A good pruning algorithm will eliminate lots of pointless states, thus justifying the time it takes to run the pruning itself.
Then, introduce a heuristic score: rank each state with a number that tells you how „good“ the state looks – about how much more solving will it take. Make your queue a priority queue. This will allow you to consider the „best looking“ states first, so the program should come up with a solution faster. But, the first solution found may not actually be the best, so to be sure that you find the best one, you still need to run the whole program.
Store the minimum cost (number of moves) that you took to get to each state. Remember to update it if you find a better path. Take the states with the lowest sum of their cost and their heuristic score first; those will more likely lead to a better solution.
And here comes A*. You need to modify your heuristic function so that it doesn't overestimate the distance to the goal, i.e. it can be lower than the number of moves you will actually need, but not higher. Then, note that if you found a solution, its heuristic score will be 0. And, any state where the sum of its cost and heuristic is more than the cost of a solution can't lead to a better solution. So, you can prune that state. But since you're taking the states in order, once you hit that threshold you can just stop and return, since all other states in the queue would be pruned as well.
All that's left now is perfecting your heuristic: it can never overestimate, but the better estimate it gives the less time A* will take. The better the heuristic, the better your results. Take care that the heuristic doesn't take so much time to complete – you wouldn't want, say, generating the solution by brute force, even though it would give the perfect answer :)
Wikipedia has some more discussion and possible improvements, if you get this far. But the best improvements you can make at this point will likely come from improving the heuristic function.
Maybe translate it into a classical planning problem (using PDDL syntax). Then you can try out some planners that are freely available.
E.g. try Fast Forward.