Dictionary be a Key in a Dictionary Python - python

I want an O(1) method of checking if I have been in a state. The problem is that a state is defined by the position of a few zoombinis on a map.
Zoombini = {(1,1): 0, (2,2):1, (3,3):3} {Position: Zoombini ID}
I am using Breadth-First Search and am pushing onto my Queue this dictionary of positions.
dirs = [goNorth, goSouth, goWest, goEast] ## List of functions
zoom = {}
boulders = {}
visited = {} ## {(zoom{}): [{0,1,2},{int}]}
## {Map: [color, distance] 0 = white, 1 = gray, 2 = black
n = len(abyss)
for i in xrange(n):
for j in xrange(n):
if (abyss[i][j] == 'X'):
boulders[(i,j)] = True
elif (isInt(abyss[i][j])):
zoom[(i,j)] = int(abyss[i][j]) ## invariant only 1 zomb can have this position
elif (abyss[i][j] == '*'):
exit = (i, j)
sQueue = Queue.Queue()
zombnum = 0
done = False
distance = 0
sQueue.put(zoom)
while not(sQueue.empty()):
currZomMap = sQueue.get()
for zom in currZomMap.iterkeys(): ## zoom {(i,j): 0}
if not(zom == exit):
z = currZomMap[zom]
for fx in dirs: ## list of functions that returns resulting coordinate of going in some direction
newPos = fx(zom)
newZomMap = currZomMap.copy()
del(newZomMap[zom]) ## Delete the old position
newZomMap[newPos] = z ## Insert new Position
if not(visited.has_key(newZomMap)):
sQueue.put(newZomMap)
My implementation isn't done but I need a better method of checking if I have already visited a state. I could make a function that creates an integer hash out of the dictionary but I don't think that I'd be able to efficiently. Time is also an issue. How can I go about this optimally?

Rather than constructing some fragile custom hash function, I'd probably just use a frozenset:
>>> Z = {(1,1): 0, (2,2):1, (3,3):3}
>>> hash(Z)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'
>>> frozenset(Z.items())
frozenset([((2, 2), 1), ((1, 1), 0), ((3, 3), 3)])
>>> hash(frozenset(Z.items()))
-4860320417062922210
The frozenset can be stored in sets and dicts without any problems. You could also use a tuple built from Z.items() but you'd have to ensure it was always stored in a canonical format (say by sorting it first.)

Python doesn't allow mutable keys so I ended up creating a function that hashes my dictionary.
edit--
def hashthatshit(dictionary):
result = 0
i =0
for key in dictionary.iterkeys():
x = key[0]
y = key[1]
result+=x*10**i+y*10**(i+1)+\
10**(i+2)*dictionary[key]
i+=3
return result
I used this which is specific to my implementation which is why I originally didn't include it.

Related

How to call a function on list to return incremental bits range

I am trying to write a function that returns from and to bits in [from:to] format.
I am not quite sure how exactly it can be done (recursively?). The expected output is in incremental range of bits. Here is the piece of code to start with,
cntlist = [5,1,4,3,1]
def find_size(cnt):
if cnt>1:
a = "[%s:%s]" % (cnt-1, cnt-cnt)
left = cnt-1
right = cnt-cnt
if cnt==1:
a = "[%s]" % (cnt)
left = a
right = a
return a, left, right
newlist = list(map(find_size, cntlist))
print(newlist)
Output:
[('[4:0]', 4, 0), ('[1]', '[1]', '[1]'), ('[3:0]', 3, 0), ('[2:0]', 2, 0), ('[1]', '[1]', '[1]')]
Expected output:
['[4:0]', '[5]', '[9:6]', '[12:10]', '[13]']
Note: If size is 1 in cntlist, the range will have only one element which will be +1 to previous range's left number.
IIUC, a simple loop should work:
def bitrange(cntlst):
out = []
total = 0
for i in cntlst:
prev = total
total += i
if i == 1:
out.append(f'[{total-1}]')
else:
out.append(f'[{total-1}:{prev}]')
return out
bitrange([5,1,4,3,1])
output:
['[4:0]', '[5]', '[9:6]', '[12:10]', '[13]']

python - unique set of ranges, merging when needed

Is there a datastructure that will maintain a unique set of ranges, merging an contiguous or overlapping ranges that are added? I need to track which ranges have been processed, but this may occur in an arbitrary order. E.g.:
range_set = RangeSet() # doesn't exist that I know of, this is what I need help with
def process_data(start, end):
global range_set
range_set.add_range(start, end)
# ...
process_data(0, 10)
process_data(20, 30)
process_data(5, 15)
process_data(50, 60)
print(range_set.missing_ranges())
# [[16,19], [31, 49]]
print(range_set.ranges())
# [[0,15], [20,30], [50, 60]]
Notice that overlapping or contiguous ranges get merged together. What is the best way to do this? I looked at using the bisect module, but its use didn't seem terribly clear.
Another approach is based on sympy.sets.
>>> import sympy as sym
>>> a = sym.Interval(1, 2, left_open=False, right_open=False)
>>> b = sym.Interval(3, 4, left_open=False, right_open=False)
>>> domain = sym.Interval(0, 10, left_open=False, right_open=False)
>>> missing = domain - a - b
>>> missing
[0, 1) U (2, 3) U (4, 10]
>>> 2 in missing
False
>>> missing.complement(domain)
[1, 2] U [3, 4]
You could get some similar functionality with pythons built-in set data structure; supposing only integer values are valid for start and end.
>>> whole_domain = set(range(12))
>>> A = set(range(0,1))
>>> B = set(range(4,9))
>>> C = set(range(3,6)) # processed range(3,5) twice
>>> done = A | B | C
>>> print done
set([0, 3, 4, 5, 6, 7, 8])
>>> missing = whole_domain - done
>>> print missing
set([1, 2, 9, 10, 11])
This still lacks many 'range'-features but might be sufficient.
A simple query if a certain range was already processed could look like this:
>>> isprocessed = [foo in done for foo in set(range(2,6))]
>>> print isprocessed
[False, True, True, True]
I've only lightly tested it, but it sounds like you're looking for something like this. You'll need to add the methods to get the ranges and missing ranges yourself, but it should be very straighforward as RangeSet.ranges is a list of Range objects maintained in sorted order. For a more pleasant interface you could write a convenience method that converted it to a list of 2-tuples, for example.
EDIT: I've just modified it to use less-than-or-equal comparisons for merging. Note, however, that this won't merge "adjacent" entries (e.g. it won't merge (1, 5) and (6, 10)). To do this you'd need to simply modify the condition in Range.check_merge().
import bisect
class Range(object):
# Reduces memory usage, overkill unless you're using a lot of these.
__slots__ = ["start", "end"]
def __init__(self, start, end):
"""Initialise this range."""
self.start = start
self.end = end
def __cmp__(self, other):
"""Sort ranges by their initial item."""
return cmp(self.start, other.start)
def check_merge(self, other):
"""Merge in specified range and return True iff it overlaps."""
if other.start <= self.end and other.end >= self.start:
self.start = min(other.start, self.start)
self.end = max(other.end, self.end)
return True
return False
class RangeSet(object):
def __init__(self):
self.ranges = []
def add_range(self, start, end):
"""Merge or insert the specified range as appropriate."""
new_range = Range(start, end)
offset = bisect.bisect_left(self.ranges, new_range)
# Check if we can merge backwards.
if offset > 0 and self.ranges[offset - 1].check_merge(new_range):
new_range = self.ranges[offset - 1]
offset -= 1
else:
self.ranges.insert(offset, new_range)
# Scan for forward merges.
check_offset = offset + 1
while (check_offset < len(self.ranges) and
new_range.check_merge(self.ranges[offset+1])):
check_offset += 1
# Remove any entries that we've just merged.
if check_offset - offset > 1:
self.ranges[offset+1:check_offset] = []
You have hit on a good solution in your example use case. Rather than try to maintain a set of the ranges that have been used, keep track of the ranges that haven't been used. This makes the problem pretty easy.
class RangeSet:
def __init__(self, min, max):
self.__gaps = [(min, max)]
self.min = min
self.max = max
def add(self, lo, hi):
new_gaps = []
for g in self.__gaps:
for ng in (g[0],min(g[1],lo)),(max(g[0],hi),g[1]):
if ng[1] > ng[0]: new_gaps.append(ng)
self.__gaps = new_gaps
def missing_ranges(self):
return self.__gaps
def ranges(self):
i = iter([self.min] + [x for y in self.__gaps for x in y] + [self.max])
return [(x,y) for x,y in zip(i,i) if y > x]
The magic is in the add method, which checks each existing gap to see whether it is affected by the new range, and adjusts the list of gaps accordingly.
Note that the behaviour of the tuples used for ranges here is the same as Python's range objects, i.e. they are inclusive of the start value and exclusive of the stop value. This class will not behave in exactly the way you described in your question, where your ranges seem to be inclusive of both.
Have a look at portion (https://pypi.org/project/portion/). I'm the maintainer of this library, and it supports disjuction of continuous intervals out of the box. It automatically simplifies adjacent and overlapping intervals.
Consider the intervals provided in your example:
>>> import portion as P
>>> i = P.closed(0, 10) | P.closed(20, 30) | P.closed(5, 15) | P.closed(50, 60)
>>> # get "used ranges"
>>> i
[0,15] | [20,30] | [50,60]
>>> # get "missing ranges"
>>> i.enclosure - i
(15,20) | (30,50)
Similar to DavidT's answer – also based on sympy's sets, but using a list of any length and addition (union) in a single operation:
import sympy
intervals = [[1,4], [6,10], [3,5], [7,8]] # pairs of left,right
print(intervals)
symintervals = [sympy.Interval(i[0],i[1], left_open=False, right_open=False) for i in intervals]
print(symintervals)
merged = sympy.Union(*symintervals) # one operation; adding to an union one by one is much slower for a large number of intervals
print(merged)
for i in merged.args: # assumes that the "merged" result is an union, not a single interval
print(i.left, i.right) # getting bounds of merged intervals
Here's my solution:
def flatten(collection):
subset = set()
for elem in collection:
to_add = elem
to_remove = set()
for s in subset:
if s[0] <= to_add[0] <= s[1] or s[0] <= to_add[1] <= s[1] or (s[0] > to_add[0] and s[1] < to_add[1]):
to_remove.add(s)
to_add = (min(to_add[0], s[0]), max(to_add[1], s[1]))
subset -= to_remove
subset.add(to_add)
return subset
range_set = {(-12, 4), (3, 20), (21, 25), (25, 30), (-13, -11), (5, 10), (-13, 20)}
print(flatten(range_set))
# {(21, 30), (-13, 20)}

Python 3.3.2 - 'Grouping' System with Characters

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))

Abreviations in a NFA, python

I'm trying to create a method were abreviations skips from one point to another.
I've created a NFA with the current edges
EDGES = [
(0, 'h', 1),
(1,'a',2),
(2,'z', 3),
(3,'a',4),
(4, 'r', 5),
(5, 'd', 6)
)]
Example of what I'm trying to accomplish
nrec("h-rd", nfa, 1) should return accept
nrec is the method that process the string for the NFA and checking wether it accepts or rejects.
def nrec(tape, nfa, trace=0):
"""Recognize in linear time similarly to transform NFA to DFA """
char = "-"
index = 0
states = [nfa.start]
while True:
if trace > 0: print " Tape:", tape[index:], " States:", states
if index == len(tape): # End of input reached
successtates = [s for s in states
if s in nfa.finals]
# If this is nonempty return True, otherwise False.
return len(successtates)> 0
elif len(states) == 0:
# Not reached end of string, but no states.
return False
elif char is tape[index]:
# the add on method to take in abreviations by sign: -
else:
# Calculate the new states.
states = set([e[2] for e in nfa.edges
if e[0] in states and
tape[index] == e[1]
])
# Move one step in the string
index += 1
I need to add a method that takes abreviations into the account. I'm not quite sure on how I can go skip from one state to another.
This is what is in the class NFA:
def __init__(self,start=None, finals=None, edges=None):
"""Read in an automaton from python shell"""
self.start = start
self.edges = edges
self.finals = finals
self.abrs = {}
I tought about using the abrs, but i constatly get's error when trying to define my own abrs such as
nfa = NFA(
start = 0,
finals = [6],
abrs = {0:4, 2:5},
edges=[
(0,'h', 1),
(1,'a', 2),
(2,'z', 3),
(3,'a', 4),
(4,'r', 5),
(5,'d', 6)
])
I recive error "TypeError: init() got an unexpected keyword argument 'abrs'"
Why I am reciveing that error?
for the modification i tought I'd do something like this
elif char is tape[index]:
#get the next char in tape tape[index+1] so
#for loop this.char with abrs states and then continue from that point.
smart choice or any better solutions?
The error is caused by __init__ not accepting the abrs keyword parameter as it is defined.
def __init__(self,start=None, finals=None, edges=None):
You'd need abrs=None (or another value) to make it a keyword argument or abrs to make it a required argument.

Python: TypeError: Unhashable Type: List

I have a code that basically tests which circles in a random distribution of circles in a box touch - touching circles are added to a dictionary as clusters. I keep getting a TypeError when I run this code:
leftedge = 0
rightedge = 1
result = []
color = {}
parent = {}
clusters = {}
number = 0
def bfs(vertices, neighbours, source) :
global number
number +=1
clusters[number] = set()
color[source] = 'g'
q = []
q.append(source)
while q != [] :
v = q.pop(0)
for v2 in neighbours[v] :
if color[v2] == 'w' :
color[v2] = 'g'
parent[v2] = v
q.append(v2)
color[v] = 'b'
clusters[number].add(v)
def createclusters(vertices, neighbours) :
for v in vertices :
color[v] = 'w'
parent[v] = -1
while 'w' in color.values() :
for v in color.keys() :
if color[v] == 'w' :
bfs(vertices, neighbours, v)
def overlap(c1,c2,r) :
if ((c1[0]-c2[0])**2 +(c1[0]-c2[0])**2)**0.5 > 2*radius :
return 0
return 1
def findclusters(array, radius) :
d={}
for c1 in array :
d[c1]=[]
for c2 in array :
if overlap(c1, c2, radius) :
d[c1].append(c2)
createclusters(array,d)
for cluster in clusters.values() :
l = [i[0] for i in cluster]
left = right = False
x = max(l)
if x + radius > rightedge :
right = True
x = min(l)
if x - radius < leftedge :
left = True
result.append((cluster,left,right))
import numpy.random as nr
array = nr.uniform(size=(10,2)).tolist
radius = 0.1
findclusters(array, radius)
print(clusters)
print(result)
When I try and run it, I get this error:
TypeError Traceback (most recent call last)
/Users/annikamonari/<ipython-input-316-be6c65f2ce89> in <module>()
----> 1 findclusters(array,0.1)
/Users/annikamonari/<ipython-input-309-32f214b46080> in findclusters(array, radius)
2 d={}
3 for c1 in array:
----> 4 d[c1]=[]
5 for c2 in array:
6 if overlap(c1,c2,radius):
TypeError: unhashable type: 'list'
For the life of me, can't figure out why. Can anyone figure it out?
Thank you!
First, you probably need to call .tolist() rather than leaving off the parens.
Also, array is a 2-dimensional array, which means that when you do...
for c1 in array:
d[c1]=[]
...c1 is a list. Lists, being mutable, are not a hashable type (because if they were, the hash could change at any time based on the contents changing, and hash-based data structures aren't designed to handle that) and thus can't be used as a dictionary key.
If you intended to use a sequence of values as a dictionary key, you need to make them non-mutable (and thus hashable) first. The easiest way to do this is by converting to a tuple:
for c1 in array:
d[tuple(c1)]=[]
However, from reading your code, it seems more like you might be wanting to just iterate over the indices of the first array, which means you probably want something like...
for index, values in enumerate(array):
d[index] = []
for c2 in values:
# ...
or similar.
As the program expects array to be a list of 2d hashable types (2d tuples), its best if you convert array to that form, before calling any function on it.
temp = nr.uniform(size=(10,2)).tolist()
array = [tuple(i) for i in temp]
This should create the input in the required format.
You don't actually call the tolist method in your code, you just pass the function itself.
Add the () to call it:
array = nr.uniform(size=(10,2)).tolist()
Notice that c2 in your code is a list of two numbers, it's not a single number.

Categories