I have a fun little problem.
I need to count the amount of 'groups' of characters in a file. Say the file is...
..##.#..#
##..####.
.........
###.###..
##...#...
The code will then count the amount of groups of #'s. For example, the above would be 3. It includes diagonals. Here is my code so far:
build = []
height = 0
with open('file.txt') as i:
build.append(i)
height += 1
length = len(build[0])
dirs = {'up':(-1, 0), 'down':(1, 0), 'left':(0, -1), 'right':(0, 1), 'upleft':(-1, -1), 'upright':(-1, 1), 'downleft':(1, -1), 'downright':(1, 1)}
def find_patches(grid, length):
queue = []
queue.append((0, 0))
patches = 0
while queue:
current = queue.pop(0)
line, cell = path[-1]
if ## This is where I am at. I was making a pathfinding system.
Here’s a naive solution I came up with. Originally I just wanted to loop through all the elements once an check for each, if I can put it into an existing group. That didn’t work however as some groups are only combined later (e.g. the first # in the second row would not belong to the big group until the second # in that row is processed). So I started working on a merge algorithm and then figured I could just do that from the beginning.
So how this works now is that I put every # into its own group. Then I keep looking at combinations of two groups and check if they are close enough to each other that they belong to the same group. If that’s the case, I merge them and restart the check. If I completely looked at all possible combinations and could not merge any more, I know that I’m done.
from itertools import combinations, product
def canMerge (g, h):
for i, j in g:
for x, y in h:
if abs(i - x) <= 1 and abs(j - y) <= 1:
return True
return False
def findGroups (field):
# initialize one-element groups
groups = [[(i, j)] for i, j in product(range(len(field)), range(len(field[0]))) if field[i][j] == '#']
# keep joining until no more joins can be executed
merged = True
while merged:
merged = False
for g, h in combinations(groups, 2):
if canMerge(g, h):
g.extend(h)
groups.remove(h)
merged = True
break
return groups
# intialize field
field = '''\
..##.#..#
##..####.
.........
###.###..
##...#...'''.splitlines()
groups = findGroups(field)
print(len(groups)) # 3
I'm not exactly sure what your code is trying to do. Your with statement opens a file, but all you do is append the file object to a list before the with ends and it gets closed (without its contents ever being read). I suspect his is not what you intend, but I'm not sure what you were aiming for.
If I understand your problem correctly, you are trying to count the connected components of a graph. In this case, the graph's vertices are the '#' characters, and the edges are wherever such characters are adjacent to each other in any direction (horizontally, vertically or diagonally).
There are pretty simple algorithms for solving that problem. One is to use a disjoint set data structure (also known as a "union-find" structure, since union and find are the two operations it supports) to connect groups of '#' characters together as they're read in from the file.
Here's a fairly minimal disjoint set I wrote to answer another question a while ago:
class UnionFind:
def __init__(self):
self.rank = {}
self.parent = {}
def find(self, element):
if element not in self.parent: # leader elements are not in `parent` dict
return element
leader = self.find(self.parent[element]) # search recursively
self.parent[element] = leader # compress path by saving leader as parent
return leader
def union(self, leader1, leader2):
rank1 = self.rank.get(leader1,1)
rank2 = self.rank.get(leader2,1)
if rank1 > rank2: # union by rank
self.parent[leader2] = leader1
elif rank2 > rank1:
self.parent[leader1] = leader2
else: # ranks are equal
self.parent[leader2] = leader1 # favor leader1 arbitrarily
self.rank[leader1] = rank1+1 # increment rank
And here's how you can use it for your problem, using x, y tuples for the nodes:
nodes = set()
groups = UnionFind()
with open('file.txt') as f:
for y, line in enumerate(f): # iterate over lines
for x, char in enumerate(line): # and characters within a line
if char == '#':
nodes.add((x, y)) # maintain a set of node coordinates
# check for neighbors that have already been read
neighbors = [(x-1, y-1), # up-left
(x, y-1), # up
(x+1, y-1), # up-right
(x-1, y)] # left
for neighbor in neighbors:
if neighbor in nodes:
my_group = groups.find((x, y))
neighbor_group = groups.find(neighbor)
if my_group != neighbor_group:
groups.union(my_group, neighbor_group)
# finally, count the number of unique groups
number_of_groups = len(set(groups.find(n) for n in nodes))
Related
first of all let me pretext this by the fact that this is for an university homework, so I only want hints and not solutions.
The problem consists of finding the path from s to t that has the smallest maximum amount of snow on one of the edges, while choosing shortest distance for tie breaking. (i. e. if multiple edges with the same snow amount are considered, we take the one with the shortest length). For any two vertices, there is at most one edge that connects them.
n - number of vertices
m - number of edges
s - source
t - target
a - list of edge beginnings
b - list of edge ends
w - list of lengths corresponding to the edges
c - list of amounts of snow corresponding to the edges
I would really appreciate the help as I've been racking my head over this for a long time.
I tried this.
import heapq
# # intersections, # roads, start list, end list, len list, snow list
def snowy_road(n, m, s, t, a, b, w, c):
# Initialize distances to all nodes as infinite except for the source node, which has distance 0
distancesc = [float("inf")] * n
distancesc[s-1] = 0
distancesw = [float("inf")] * n
distancesw[s-1] = 0
# Create a set to store the nodes that have been visited
visited = set()
# Create a priority queue to store the nodes that still need to be processed
# We will use the distance to the source node as the priority
# (i.e. the next node to process will be the one with the smallest distance to the source)
pq = []
pq.append((0, 0, s-1))
while pq:
# Pop the node with the smallest distance from the priority queue
distc, distw, u = heapq.heappop(pq)
# Skip the node if it has already been visited
if u in visited:
continue
# Mark the node as visited
visited.add(u)
# Update the distances to all adjacent nodes
for i in range(m):
if a[i] == u+1:
v = b[i]-1
elif b[i] == u+1:
v = a[i]-1
else:
continue
altc = c[i]
if distc != float("inf"):
altc = max(distc, altc)
altw = distw + w[i]
if altc < distancesc[v]:
distancesc[v] = altc
distancesw[v] = altw
heapq.heappush(pq, (altc, altw, v))
elif altc == distancesc[v] and altw < distancesw[v]:
distancesw[v] = altw
heapq.heappush(pq, (altc, altw, v))
# Return the distance to the target node
return distancesc[t-1], distancesw[t-1]
If anyone is wondering, I solved this using two consequent Dijkstras. The first one finds the value of the shortest bottleneck path, the second one takes this value into account and doest a shortest path normal Dijkstra on the graph using w, while considering only edges that have a snow coverage <= than the bottleneck we found. Thank you to everyone that responded!
I am creating a word search solver and need a way to rotate the word search, which is in a list, so the left corner is the 'top' and the bottom right is at the 'bottom'
I have this:
Puzzle = ["FUNCTIONRRIRAI",
"RAIOONFRCCPWON",
"PTCSNOBEUITOLO",
"BNCACIANTOSLIH",
"RBYOLILYNREFBT",
"HYYNOGESTIBRIY",
"AATTSIONCMCENP",
"UORTENRRCBFVAU",
"CEBEECVWIERORI",
"PROCESSORTOPYF",
"OHCOMPUTERHSOS",
"YCYPRESREOSMRW",
"OATHBRMVTHHCTR",
"PGORWOOUIPSCHP"]
I need it in the formation of:
Puzzle = ["F","RU","PAN","BTIC",...]
so it appears that the word search has been rotated 45 degrees
any suggestions/help would be appreciated
Code for find_horizontal and words to find:
def load_words_to_find(file_name):
word_list = []
file = open(file_name, "r")
for line in file.readlines():
word_list.append(line)
word_list = list(map(lambda s: s.strip(), word_list))
return word_list
def find_horizontal(Puzzle, Words, ReplaceWith, Found):
# Parameters :- List:Puzzle, List:Words, Character:ReplaceWith, List:Found
# Return :- List:Outpuz, List:Found
# Find all words which are horizontally in place (left to right and right to left), return the puzzle and list of found words
rev = ''
Outpuz = Puzzle
for line in Puzzle:
rev = line[::-1]
for word in Words:
if word in line:
Found.append(word)
Puzzle[Puzzle.index(line)] = line.replace(word, ReplaceWith * len(word))
if word in rev:
Found.append(word)
Puzzle[Puzzle.index(line)] = line.replace(word[::-1], ReplaceWith * len(word))
else:
pass
print("Found: ", Found)
print(Outpuz)
return Outpuz, Found
find_horizontal(Puzzle, load_words_to_find("words.txt"), ".", [])
Kind of silly, but you could insert string iterators to the front of a list, and then join and yield the next character from each iterator.
rows = [
"FUNCTIONRRIRAI",
"RAIOONFRCCPWON",
"PTCSNOBEUITOLO",
"BNCACIANTOSLIH",
"RBYOLILYNREFBT",
"HYYNOGESTIBRIY",
"AATTSIONCMCENP",
"UORTENRRCBFVAU",
"CEBEECVWIERORI",
"PROCESSORTOPYF",
"OHCOMPUTERHSOS",
"YCYPRESREOSMRW",
"OATHBRMVTHHCTR",
"PGORWOOUIPSCHP"
]
def get_next_diagonal(rows):
iters = []
for row in rows:
iters.insert(0, iter(row))
yield "".join(next(it, "") for it in iters)
while iters[0].__length_hint__():
yield "".join(next(it, "") for it in iters)
for diagonal in get_next_diagonal(rows):
print(diagonal)
Output:
F
RU
PAN
BTIC
RNCOT
HBCSOI
AYYANNO
UAYOCOFN
COTNLIBRR
PERTOIAECR
ORBTSGLNUCI
YHOEEIEYTIPR
OCCCENOSNOTWA
PAYOECRNTRSOOI
GTPMSVRCIELLN
OHRPSWCMBFIO
RBEUOIBCRBH
WRSTREFEIT
OMRETRVNY
OVEROOAP
UTOHPRU
IHSSYI
PHMOF
SCRS
CTW
HR
P
Apologies for the lack of efficiency, but here is my quick solution.
We view the grid as a set of coordinates. This solution is based off the fact that every item in the output you want will have X and Y coordinates than add up to a certain number. For example, the first item "F" has a location of (0,0), which sum to 0. The second two items "RU" are located ad (1,0) and (0,1), which both add up to 1. The third line "PAN" has letters located at (0,2), (1,1) and (2,0), and so on. Tracking this number with "i" in my solution, it needs to be big enough to scan down and across the lists, so this is twice the size of the length of the array. So we scan the list for items that are located at x and y coordinates where x+y==i.
Code:
Puzzle = ["FUNCTIONRRIRAI",
"RAIOONFRCCPWON",
"PTCSNOBEUITOLO",
"BNCACIANTOSLIH",
"RBYOLILYNREFBT",
"HYYNOGESTIBRIY",
"AATTSIONCMCENP",
"UORTENRRCBFVAU",
"CEBEECVWIERORI",
"PROCESSORTOPYF",
"OHCOMPUTERHSOS",
"YCYPRESREOSMRW",
"OATHBRMVTHHCTR",
"PGORWOOUIPSCHP"]
output = []
i = 0
while i < len(Puzzle)*2:
single_string = ""
for y in range(0,len(Puzzle)):
for x in range(0,len(Puzzle[0])):
if (x + y) == i:
single_string += Puzzle[x][y]
if single_string != "":
output.append(single_string)
i += 1
print(output)
Outputs
['F',
'RU',
'PAN',
'BTIC',
'RNCOT',
'HBCSOI',
'AYYANNO',
'UAYOCOFN',
'COTNLIBRR',
'PERTOIAECR',
'ORBTSGLNUCI',
'YHOEEIEYTIPR',
'OCCCENOSNOTWA',
'PAYOECRNTRSOOI',
'GTPMSVRCIELLN',
'OHRPSWCMBFIO',
'RBEUOIBCRBH',
'WRSTREFEIT',
'OMRETRVNY',
'OVEROOAP',
'UTOHPRU',
'IHSSYI',
'PHMOF',
'SCRS',
'CTW',
'HR',
'P']
Hope this helps. Happy to clarify anything if needed
I find it easier and clearer to just generate the indexes in two loops, down the rows starting at the first column and then along the columns starting at the last row:
puzzle = ['FUNCTIONRRIRAI',
'RAIOONFRCCPWON',
'PTCSNOBEUITOLO',
'BNCACIANTOSLIH',
'RBYOLILYNREFBT',
'HYYNOGESTIBRIY',
'AATTSIONCMCENP',
'UORTENRRCBFVAU',
'CEBEECVWIERORI',
'PROCESSORTOPYF',
'OHCOMPUTERHSOS',
'YCYPRESREOSMRW',
'OATHBRMVTHHCTR',
'PGORWOOUIPSCHP']
nrows = len(puzzle)
ncols = len(puzzle[0])
output = []
for ir in range(nrows):
row = []
ic = 0
jr = ir
while jr >= 0:
row.append(puzzle[jr][ic])
ic += 1
jr -= 1
output.append(''.join(row))
for ic in range(1, ncols):
row = []
ir = nrows - 1
jc = ic
while jc < ncols:
row.append(puzzle[ir][jc])
ir -= 1
jc += 1
output.append(''.join(row))
for row in output:
print(row)
Output:
F
RU
PAN
BTIC
RNCOT
HBCSOI
AYYANNO
UAYOCOFN
COTNLIBRR
PERTOIAECR
ORBTSGLNUCI
YHOEEIEYTIPR
OCCCENOSNOTWA
PAYOECRNTRSOOI
GTPMSVRCIELLN
OHRPSWCMBFIO
RBEUOIBCRBH
WRSTREFEIT
OMRETRVNY
OVEROOAP
UTOHPRU
IHSSYI
PHMOF
SCRS
CTW
HR
P
Variation on a theme - generate diagonal indices.
If you trace your finger down the left edge, those are the starting point for the top-half diagonals and tracing your finger across the bottom edge are starting points for the bottom-half diagonals.
Top Half:
From each diagonal's starting point the first dimension's indices range from the starting point to zero (negative steps) and the second dimension's indices range from zero to a maximum of the length of the second dimension (positive steps).
Bottom Half:
From each diagonal's starting point the first dimension's indices range from the length of the first dimension minus one to a minimum of zero (negative steps) and the second dimension's indices range from the starting point to a the length of the second dimension (positive steps).
For both sets of diagonals you can take advantage of the fact that zip will stop when the shortest iterable is exhausted.
def cw(dims,ragged=False):
'''Generate indices for diagonals based on dims.
dims --> tuple: (nrows,ncolumns)
Currently only implemented for all rows have same number of columns
Diagonals as if the rectangle was rotated CW 45 degrees
bottom-left to upper-right
'''
if ragged: raise NotImplementedError
nrows,ncolumns = dims
# top half
index1 = range(0,ncolumns)
for i in range(nrows):
yield zip(range(i,-1,-1),index1)
# bottom half
index0 = range(nrows-1,-1,-1)
for i in range(1,nrows):
yield zip(index0,range(i,ncolumns))
new = []
for diagonal in cw((len(Puzzle),len(Puzzle[0]))):
new.append(''.join(Puzzle[j][k] for (j,k) in diagonal))
And a CCW rotation solution
def ccw(dims,ragged=False):
'''Generate indices for diagonals based on dims.
dims --> tuple: (nrows,ncolumns)
Currently only implemented for all rows have same number of columns
Diagonals as if the rectangle was rotated CCW 45 degrees
top-left to bottom-right
'''
if ragged: raise NotImplementedError
nrows,ncolumns = dims
# top half
index0 = range(0,nrows)
for i in range(ncolumns,-1,-1):
yield zip(index0,range(i,ncolumns))
# bottom half
index1 = range(0,ncolumns)
for i in range(1,nrows):
yield zip(range(i,nrows),index1,)
Given a list of paths(with start and end coordinates) that are only horizontal and vertical, how can I find all the rectangles that they form?
Details:
The end points of the rectangle must be end-points of their constituent edges. i.e rectangles formed from the intersections of lines are not needed.
The x,y of start and end, represented as complex numbers of each line is given.
The output should be four lines representing each edge of the rectangle
This is my naive implementation which is too slow. Are there any other approaches?
def find_rectangles(paths):
vertical_paths = filter(lambda path: is_vertical(path), paths)
horizontal_paths = filter(lambda path: is_horizontal(path), paths)
vertical_paths.sort(key=lambda path: path.start.imag, reverse=True)
horizontal_paths.sort(key=lambda path: path.start.real)
h_pairs = []
for i in range(0, len(horizontal_paths) - 1):
for j in range(1, len(horizontal_paths)):
if horizontal_paths[i].start.real == horizontal_paths[j].start.real and horizontal_paths[i].end.real == horizontal_paths[j].end.real:
h_pairs.append((horizontal_paths[i], horizontal_paths[j]))
v_pairs = []
for i in range(0, len(vertical_paths) - 1):
for j in range(1, len(vertical_paths)):
if vertical_paths[i].start.imag == vertical_paths[j].start.imag and vertical_paths[i].end.imag == vertical_paths[j].end.imag:
v_pairs.append((vertical_paths[i], vertical_paths[j]))
rects = []
for h1, h2 in h_pairs:
for v1, v2 in v_pairs:
if h1.start == v1.start and v1.end == h2.start and h1.end == v2.start and h2.end == v2.end:
rects.append(Rect(h1.start, h1.end, h2.end, h2.start))
return rects
Edit:(Improvements from all suggestions)
Main difference is I am storing all horizontal edges' end-points in a set so look up for them is O(1):
def find_rectangles(paths):
vertical_paths = [path for path in paths if is_vertical(path)]
horizontal_paths_set = set([(path.start, path.end) for path in paths if is_horizontal(path)])
vertical_paths.sort(key=lambda pair: path.start.imag, reverse=True)
v_pairs = [pair for pair in list(itertools.combinations(vertical_paths, 2)) if pair[0].start.imag == pair[1].start.imag and pair[0].end.imag == pair[1].end.imag]
rects = []
for v1,v2 in v_pairs:
h1 = (v1.start, v2.start)
h2 = (v1.end, v2.end)
if(h1 in horizontal_paths_set and h2 in horizontal_paths_set):
rects.append(Rect(h1[0], h1[1], h2[1], h2[0 ]))
return rects
My new code runs much much faster, but it still is on the order of O(n2). Any suggestion to improve upon it is welcome.
You can drop the search for v_pairs to begin with. You only need to know if the potential rectangle (a horizontal pair) can be closed.
def find_rectangles(paths):
vertical_paths = filter(lambda path: is_vertical(path), paths)
horizontal_paths = filter(lambda path: is_horizontal(path), paths)
vertical_paths.sort(key=lambda path: path.start.imag, reverse=True)
horizontal_paths.sort(key=lambda path: path.start.real)
potential_rectangles = []
for i,h1 in enumerate(horizontal_paths[:-1]):
for h2 in horizontal_paths[i+1:]:
if ((h1.start.real == h2.start.real)
and (h1.end.real == h2.end.real)):
potential_rectangles.append((h1,h2,None,None))
rectangles = []
for v in vertical_paths:
for i,(h1,h2,v1,v2) in enumerate(potential_rectangles):
if v1 is None and v.start == h1.start and v.end == h2.start:
potential_rectangles[i][2] = v
if v2 is not None:
rectangles.append(potential_rectangles.pop(i))
break
if v2 is None and v.start == h1.end and v.end == h2.end:
potential_rectangles[i][3] = v
if v1 is not None:
rectangles.append(potential_rectangles.pop(i))
break
return rectangles
There certainly is a lot of potential to speed up the selection, dependent on the data received. For instance, sorting paths by length. Can you give more details?
After Edit
Bisect is a lot faster than 'is in', but it requires an ordered list of scalar values.
I'm currently doing a project, and in the code I have, I'm trying to get trees .*. and mountains .^. to spawn in groups around the first tree or mountain which is spawned randomly, however, I can't figure out how to get the trees and mountains to spawn in groups around a single randomly generated point. Any help?
grid = []
def draw_board():
row = 0
for i in range(0,625):
if grid[i] == 1:
print("..."),
elif grid[i] == 2:
print("..."),
elif grid[i] == 3:
print(".*."),
elif grid[i] == 4:
print(".^."),
elif grid[i] == 5:
print("[T]"),
else:
print("ERR"),
row = row + 1
if row == 25:
print ("\n")
row = 0
return
There's a number of ways you can do it.
Firstly, you can just simulate the groups directly, i.e. pick a range on the grid and fill it with a specific figure.
def generate_grid(size):
grid = [0] * size
right = 0
while right < size:
left = right
repeat = min(random.randint(1, 5), size - right) # *
right = left + repeat
grid[left:right] = [random.choice(figures)] * repeat
return grid
Note that the group size need not to be uniformly distributed, you can use any convenient distribution, e.g. Poisson.
Secondly, you can use a Markov Chain. In this case group lengths will implicitly follow a Geometric distribution. Here's the code:
def transition_matrix(A):
"""Ensures that each row of transition matrix sums to 1."""
copy = []
for i, row in enumerate(A):
total = sum(row)
copy.append([item / total for item in row])
return copy
def generate_grid(size):
# Transition matrix ``A`` defines the probability of
# changing from figure i to figure j for each pair
# of figures i and j. The grouping effect can be
# obtained by setting diagonal entries A[i][i] to
# larger values.
#
# You need to specify this manually.
A = transition_matrix([[5, 1],
[1, 5]]) # Assuming 2 figures.
grid = [random.choice(figures)]
for i in range(1, size):
current = grid[-1]
next = choice(figures, A[current])
grid.append(next)
return grid
Where the choice function is explained in this StackOverflow answer.
EDIT: This was a while ago and I've since got it working, if you'd like to see the code it's included at github.com/LewisGaul/minegaulerQt.
I'm trying to write a program to calculate probabilities for the game minesweeper, and have had some difficulty working out how best to structure it. While it may seem quite simple at first with the example below, I would like to know the best way to allow for more complex configurations. Note I am not looking for help with how to calculate probabilities - I know the method, I just need to implement it!
To make it clear what I'm trying to calculate, I will work through a simple example which can be done by hand. Consider a minesweeper configuration
# # # #
# 1 2 #
# # # #
where # represents an unclicked cell. The 1 tells us there is exactly 1 mine in the leftmost 7 unclicked cells, the 2 tells us there are exactly 2 in the rightmost 7. To calculate the probability of each individual cell containing a mine, we need to determine all the different cases (only 2 in this simple case):
1 mine in leftmost 3 cells, 2 mines in rightmost 3 cells (total of 3 mines, 3x3=9 combinations).
1 mine in center 4 cells, 1 mine in rightmost 3 cells (total of 2 mines, 4x3=12 combinations).
Given the probability of a mine being in a random cell is about 0.2, it is (in a random selection of cells) about 4 times more likely there is a total of 2 mines rather than a total of 3, so the total number of mines in a configuration matters, as well as the number of combinations of each configuration. So in this case the probability of case 1 is 9/(9+4x12)=0.158, and the probability of there being a mine in a given leftmost cell is therefore about 0.158/3=0.05, as those cells are effectively equivalent (they share exactly the same revealed neighbours).
I have created a GUI with Tkinter which allows me to easily enter configurations such as the one in the example, which stores the grid as a numpy array. I then made a NumberGroup class which isolates each of the clicked/numbered cells, storing the number and a set of the coordinates of its unclicked neighbours. These can be subtracted to get equivalence groups... Although this would not be as straightforward if there were three or more numbers instead of just two. But I am unsure how to go from here to getting the different configurations. I toyed with making a Configuration class, but am not hugely familiar with how different classes should work together. See working code below (numpy required).
Note: I am aware I could have attempted to use a brute force approach, but if possible I would like to avoid that, keeping the equivalent groups separate (in the above example there are 3 equivalence groups, the leftmost 3, the middle 4, the rightmost 3). I would like to hear your thoughts on this.
import numpy as np
grid = np.array(
[[0, 0, 0, 0],
[0, 2, 1, 0],
[0, 0, 0, 0]]
)
dims = (3, 4) #Dimensions of the grid
class NumberGroup(object):
def __init__(self, mines, coords, dims=None):
"""Takes a number of mines, and a set of coordinates."""
if dims:
self.dims = dims
self.mines = mines
self.coords = coords
def __repr__(self):
return "<Group of {} cells with {} mines>".format(
len(self.coords), self.mines)
def __str__(self):
if hasattr(self, 'dims'):
dims = self.dims
else:
dims = (max([c[0] for c in self.coords]) + 1,
max([c[1] for c in self.coords]) + 1)
grid = np.zeros(dims, int)
for coord in self.coords:
grid[coord] = 1
return str(grid).replace('0', '.').replace('1', '#')
def __sub__(self, other):
if type(other) is NumberGroup:
return self.coords - other.coords
elif type(other) is set:
return self.coords - other.coords
else:
raise TypeError("Can only subtract a group or a set from another.")
def get_neighbours(coord, dims):
x, y = coord
row = [u for u in range(x-1, x+2) if u in range(dims[0])]
col = [v for v in range(y-1, y+2) if v in range(dims[1])]
return {(u, v) for u in row for v in col}
groups = []
all_coords = [(i, j) for i in range(dims[0])
for j in range(dims[1])]
for coord, nr in [(c, grid[c]) for c in all_coords if grid[c] > 0]:
empty_neighbours = {c for c in get_neighbours(coord, dims)
if grid[c] == 0}
if nr > len(empty_neighbours):
print "Error: number {} in cell {} is too high.".format(nr, coord)
break
groups.append(NumberGroup(nr, empty_neighbours, dims))
print groups
for g in groups:
print g
print groups[0] - groups[1]
UPDATE:
I have added a couple of other classes and restructured a bit (see below for working code), and it is now capable of creating and displaying the equivalence groups, which is a step in the right direction. However I still need to work out how to iterate through all the possible mine-configurations, by assigning a number of mines to each group in a way that creates a valid configuration. Any help is appreciated.
For example,
# # # #
# 2 1 #
# # # #
There are three equivalence groups G1: the left 3, G2: the middle 4, G3: the right 3. I want the code to loop through, assigning groups with mines in the following way:
G1=2 (max the first group) => G2=0 => G3=1 (this is all configs with G1=2)
G1=1 (decrease by one) => G2=1 => G3=0 (this is all with G1=1)
G1=0 => G2=2 INVALID
So we arrive at both configurations. This needs to work for more complicated setups!
import numpy as np
def get_neighbours(coord, dims):
x, y = coord
row = [u for u in range(x-1, x+2) if u in range(dims[0])]
col = [v for v in range(y-1, y+2) if v in range(dims[1])]
return {(u, v) for u in row for v in col}
class NrConfig(object):
def __init__(self, grid):
self.grid = grid
self.dims = grid.shape # Dimensions of grid
self.all_coords = [(i, j) for i in range(self.dims[0])
for j in range(self.dims[1])]
self.numbers = dict()
self.groups = []
self.configs = []
self.get_numbers()
self.get_groups()
self.get_configs()
def __str__(self):
return str(self.grid).replace('0', '.')
def get_numbers(self):
for coord, nr in [(c, self.grid[c]) for c in self.all_coords
if self.grid[c] > 0]:
empty_neighbours = {c for c in get_neighbours(
coord, self.dims) if self.grid[c] == 0}
if nr > len(empty_neighbours):
print "Error: number {} in cell {} is too high.".format(
nr, coord)
return
self.numbers[coord] = Number(nr, coord, empty_neighbours,
self.dims)
def get_groups(self):
coord_neighbours = dict()
for coord in [c for c in self.all_coords if self.grid[c] == 0]:
# Must be a set so that order doesn't matter!
coord_neighbours[coord] = {self.numbers[c] for c in
get_neighbours(coord, self.dims) if c in self.numbers}
while coord_neighbours:
coord, neighbours = coord_neighbours.popitem()
equiv_coords = [coord] + [c for c, ns in coord_neighbours.items()
if ns == neighbours]
for c in equiv_coords:
if c in coord_neighbours:
del(coord_neighbours[c])
self.groups.append(EquivGroup(equiv_coords, neighbours, self.dims))
def get_configs(self):
pass # WHAT GOES HERE?!
class Number(object):
"""Contains information about the group of cells around a number."""
def __init__(self, nr, coord, neighbours, dims):
"""Takes a number of mines, and a set of coordinates."""
self.nr = nr
self.coord = coord
# A list of the available neighbouring cells' coords.
self.neighbours = neighbours
self.dims = dims
def __repr__(self):
return "<Number {} with {} empty neighbours>".format(
int(self), len(self.neighbours))
def __str__(self):
grid = np.zeros(self.dims, int)
grid[self.coord] = int(self)
for coord in self.neighbours:
grid[coord] = 9
return str(grid).replace('0', '.').replace('9', '#')
def __int__(self):
return self.nr
class EquivGroup(object):
"""A group of cells which are effectively equivalent."""
def __init__(self, coords, nrs, dims):
self.coords = coords
# A list of the neighbouring Number objects.
self.nr_neighbours = nrs
self.dims = dims
if self.nr_neighbours:
self.max_mines = min(len(self.coords),
max(map(int, self.nr_neighbours)))
else:
self.max_mines = len(coords)
def __repr__(self):
return "<Equivalence group containing {} cells>".format(
len(self.coords))
def __str__(self):
grid = np.zeros(self.dims, int)
for coord in self.coords:
grid[coord] = 9
for number in self.nr_neighbours:
grid[number.coord] = int(number)
return str(grid).replace('0', '.').replace('9', '#')
grid = np.array(
[[0, 0, 0, 0],
[0, 2, 1, 0],
[0, 0, 0, 0]]
)
config = NrConfig(grid)
print config
print "Number groups:"
for n in config.numbers.values():
print n
print "Equivalence groups:"
for g in config.groups:
print g
If you don't want to brute-force it, you could model the process as a decision tree. Suppose we start with your example:
####
#21#
####
If we want to start placing mines in a valid configuration, we at this point essentially have eight choices. Since it doesn't really matter which square we pick within an equivalence group, we can narrow that down to three choices. The tree branches. Let's go down one branch:
*###
#11#
####
I placed a mine in G1, indicated by the asterisk. Also, I've updated the numbers (just one number in this case) associated with this equivalence group, to indicate that these numbered squares can now border one fewer mines.
This hasn't reduced our freedom of choice for the following step, we can still place a mine in any of the equivalence groups. Let's place another one in G1:
*XX#
*01#
XXX#
Another asterisk marks the new mine, and the numbered square has again been lowered by one. It has now reached zero, meaning it cannot border any more mines. That means that for our next choice of mine placement, all the equivalence groups dependent upon this numbered square are ruled out. Xs mark squares where we can now not place any mine. We can only make one choice now:
*XX*
*00X
XXXX
Here the branch ends and you've found a valid configuration. By running along all the branches in this tree in this manner, you should find all of them. Here we found your first configuration. Of course, there's more than one way to get there. If we had started by placing a mine in G3, we would have been forced to place the other two in G1. That branch leads to the same configuration, so you should check for duplicates. I don't see a way to avoid this redundancy right now.
The second configuration is found by either starting with G2, or placing one mine in G1 and then the second in G2. In either case you again end up at a branch end:
**XX
X00X
XXXX
Invalid configurations like your example with zero mines in G1 do not pop up. There are no valid choices along the tree that lead you there. Here is the whole tree of valid choices.
Choice 1: 1 | 2 | 3
Choice 2: 1 2 3 | 1 | 1
Choice 3: 3 1 | |1
Valid configurations are the branch ends at which no further choice is possible, i.e.
113
12
131
21
311
which obviously fall into two equivalent classes if we disregard the order of the numbers.