Related
I am now using lists to represent the graph, which would be similar to previous question. I found out that the dict approach would be very long and complex, so decided to go with the list approach. But I am still facing a few roadblocks.
So for example, the graph:
is now represented as:
nodes = ["1", "2", "3", "4", "5"]
edges = [
[0, 2, 1, 2, 0],
[1, 0, 1, 0, 0],
[0, 2, 0, 0, 0],
[1, 0, 1, 0, 2],
[1, 2, 0, 0, 0],
]
Here, edge weights can only be 1 or 2 and 0 represents no edge from one node to other. The edges are directed, so every list in the matrix represents the edges coming toward the node.
Similar to the last question, I want all possible two-edge modifications on the graph. So, for example, if we add an edge from node "4" to "5" with weight of 1, and remove the edge with weight 1 coming from node "1" to "4", the new graph will look like:
edges = [
[0, 2, 1, 2, 0],
[1, 0, 1, 0, 0],
[0, 2, 0, 0, 0],
[0, 0, 1, 0, 2],
[1, 2, 0, 1, 0],
]
and this is one of the possible modifications.
I want to build a generator that can create all such modifications sequentially and pass it to me so that I can use them to test.
My code so far is like this:
def all_modification_generation(graph: list[list], iter_count: int = 0):
possible_weights = {-1, 0, 1}
node_len = len(graph)
for i in range(node_len**2):
ix_x = i // node_len
ix_y = i % node_len
if i == ix_y:
continue
for possible_pertubs in possible_weights - {graph[ix_x][ix_y]}:
graph[ix_x][ix_y] = possible_pertubs
if iter_count == 0:
all_modification_generation(graph=graph, iter_count=iter_count + 1)
else:
yield all_modification_generation(graph=graph)
My logic is, once I do one change, I can then loop over all other elements that come after it in the matrix. So this problem could be solved recursively. And once a node is explored, we do not need to take it into consideration for next loops, because it will just give us a duplicate result that we have already found. And because I need to check for 2 modifications, I am increasing iter_count after first iteration and then yielding the next time. I am skipping ix_x == ix_y cases because a self-looping edge does not make any sense in this context, so that change is not required to be recorded.
But even then, this does not output any result. What am I doing wrong? Any help is appreciated, thanks!
Edit: I think I have figured out a way to do the double modification without repetitive generation of modified matrices. Now the only problem is that there is quite a bit of code repetition and a 4-level nested for-loop.
I'm not sure how to call a generator recursively, but I feel that should be the way to go! Thanks J_H for pointing me to the right direction.
The working code is:
def all_modification_generation(graph: list[list]):
possible_weights = {-1, 0, 1}
node_len = len(graph)
for i in range(node_len**2):
ix_x1 = i // node_len
ix_y1 = i % node_len
if ix_x1 == ix_y1:
continue
for possible_pertubs in possible_weights - {graph[ix_x1][ix_y1]}:
cc1_graph = deepcopy(graph)
cc1_graph[ix_x1][ix_y1] = possible_pertubs
for j in range(i + 1, node_len**2):
ix_x2 = j // node_len
ix_y2 = j % node_len
if ix_x2 == ix_y2:
continue
for possible_perturbs2 in possible_weights - {cc1_graph[ix_x2][ix_y2]}:
cc2_graph = deepcopy(cc1_graph)
cc2_graph[ix_x2][ix_y2] = possible_perturbs2
yield cc2_graph
The quadratic looping is an interesting technique.
We do wind up with quite a few repeated
division results, from // node_len, but that's fine.
I had a "base + edits" datastructure in mind for this problem.
Converting array to list-of-lists would be straightforward.
After overhead, a 5-node graph consumes 25 bytes -- pretty compact.
Numpy offers good support for several styles of sparse
graphs, should that become of interest.
from typing import Generator, Optional
import numpy as np
class GraphEdit:
"""A digraph with many base edge weights plus a handful of edited weights."""
def __init__(self, edge: np.ndarray, edit: Optional[dict] = None):
a, b = edge.shape
assert a == b, f"Expected square matrix, got {a}x{b}"
self.edge = edge # We treat these as immutable weights.
self.edit = edit or {}
#property
def num_nodes(self):
return len(self.edge)
def __getitem__(self, item):
return self.edit.get(item, self.edge[item])
def __setitem__(self, item, value):
self.edit[item] = value
def as_array(g: GraphEdit) -> np.ndarray:
return np.array([[g[i, j] for j in range(g.num_nodes)] for i in range(g.num_nodes)])
def all_single_mods(g: GraphEdit) -> Generator[GraphEdit, None, None]:
"""Generates all possible single-edge modifications to the graph."""
orig_edit = g.edit.copy()
for i in range(g.num_nodes):
for j in range(g.num_nodes):
if i == j: # not an edge -- we don't support self-loops
continue
valid_weights = {0, 1, 2} - {g[i, j]}
for w in sorted(valid_weights):
yield GraphEdit(g.edge, {**orig_edit, (i, j): w})
def all_mods(g: GraphEdit, depth: int) -> Generator[GraphEdit, None, None]:
assert depth >= 1
if depth == 1:
yield from all_single_mods(g)
else:
for gm in all_single_mods(g):
yield from all_mods(gm, depth - 1)
def all_double_mods(g: GraphEdit) -> Generator[GraphEdit, None, None]:
"""Generates all possible double-edge modifications to the graph."""
yield from all_mods(g, 2)
Here's the associated test suite.
import unittest
from numpy.testing import assert_array_equal
import numpy as np
from .graph_edit import GraphEdit, all_double_mods, all_single_mods, as_array
class GraphEditTest(unittest.TestCase):
def setUp(self):
self.g = GraphEdit(
np.array(
[
[0, 2, 1, 2, 0],
[1, 0, 1, 0, 0],
[0, 2, 0, 0, 0],
[1, 0, 1, 0, 2],
[1, 2, 0, 0, 0],
],
dtype=np.uint8,
)
)
def test_graph_edit(self):
g = self.g
self.assertEqual(5, self.g.num_nodes)
self.assertEqual(2, g[0, 1])
g[0, 1] = 3
self.assertEqual(3, g[0, 1])
del g.edit[(0, 1)]
self.assertEqual(2, g[0, 1])
def test_non_square(self):
with self.assertRaises(AssertionError):
GraphEdit(np.array([[0, 0], [1, 1], [2, 2]]))
def test_all_single_mods(self):
g = GraphEdit(np.array([[0, 0], [1, 0]]))
self.assertEqual(4, len(list(all_single_mods(g))))
expected = [
np.array([[0, 1], [1, 0]]),
np.array([[0, 2], [1, 0]]),
np.array([[0, 0], [0, 0]]),
np.array([[0, 0], [2, 0]]),
]
for ex, actual in zip(
expected,
map(as_array, all_single_mods(g)),
):
assert_array_equal(ex, actual)
# Now verify that original graph is untouched.
assert_array_equal(
np.array([[0, 0], [1, 0]]),
as_array(g),
)
def test_all_double_mods(self):
g = GraphEdit(np.array([[0, 0], [1, 0]]))
self.assertEqual(16, len(list(all_double_mods(g))))
expected = [
np.array([[0, 0], [1, 0]]),
np.array([[0, 2], [1, 0]]),
np.array([[0, 1], [0, 0]]),
np.array([[0, 1], [2, 0]]),
np.array([[0, 0], [1, 0]]), # note the duplicate
np.array([[0, 1], [1, 0]]),
np.array([[0, 2], [0, 0]]), # and it continues on in this vein
]
for ex, actual in zip(
expected,
map(as_array, all_double_mods(g)),
):
assert_array_equal(ex, actual)
def test_many_mods(self):
self.assertEqual(40, len(list(all_single_mods(self.g))))
self.assertEqual(1_600, len(list(all_double_mods(self.g))))
self.assertEqual(1_600, len(list(all_mods(self.g, 2))))
self.assertEqual(64_000, len(list(all_mods(self.g, 3))))
self.assertEqual(2_560_000, len(list(all_mods(self.g, 4))))
One could quibble about the fact that
it produces duplicates, since inner and outer loops
know nothing of one another.
It feels like this algorithm wants to use an
itertools.combinations
approach, generating all modifications in lexicographic order.
Expected Output:
indenitiy_matrix(3)
[[1, 0, 0], [0, 1, 0], [0, 0, 1]]
Actual Output with Error:
indenitiy_matrix(3)
[[1, 1, 1], [1, 1, 1], [1, 1, 1]]
def identity_matrix(n):
list_template = [[]]
list_n = list_template*n
for sub_l in list_n:
sub_l.append(0)
for val in range(n):
# I have the feeling that the problem lies somewhere around here.
list_n[val][val]=1
return(list_n)
list_template*n does not create n copies, instead but all those n copies reference to only one copy. For example see this
a = [[0,0,0]]*2
# Now, lets change first element of the first sublist in `a`.
a[0][0] = 1
print (a)
# but since both the 2 sublists refer to same, both of them will be changed.
Output:
[[1, 0, 0], [1, 0, 0]]
Fix for your code
def identity_matrix(n):
list_n = [[0]*n for i in range(n)]
for val in range(n):
list_n[val][val]=1
return list_n
print (identity_matrix(5))
Output:
[[1, 0, 0, 0, 0],
[0, 1, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 0, 1]]
No, the problem lies here:
list_template = [[]]
list_n = list_template*n
After this, try doing:
list_n[0].append(1) # let's change the first element
The result:
[[1], [1], [1], [1], [1]]
is probably not what you expect.
Briefly, the problem is that after its construction, your list consists of multiple references to same list. A detailed explanation is at the link given by #saint-jaeger : List of lists changes reflected across sublists unexpectedly
Finally, the numpy library is your friend for creating identity matrices and other N-dimensional arrays.
I did a for loop using enumerate from values in a matrix and tried assigning a value to the items that are different than 0 while appending to a list elements that are equal to 0. The fact is that original matrix don't get updated.
Sample code:
matrix = [[0, 0, 0], [0, 1, 0], [1, 1, 1]]
current = []
for x, i in enumerate(matrix):
for y, j in enumerate(i):
if j == 0:
current.append((x, y))
else:
#matrix[x][y] = -1 # This works
j = -1 # This doesn't
Since this doesn't work, there is no utility in using enumerate for that case. So I changed the code to:
matrix = [[0, 0, 0], [0, 1, 0], [1, 1, 1]]
current = []
for x in range(len(matrix)):
for y in range(len(matrix[0])):
if matrix[x][y] == 0:
current.append((x, y))
else:
matrix[x][y] = -1
The code above IMO is much less readble and also pylint suggests against using that with:
C0200: Consider using enumerate instead of iterating with range and
len (consider-using-enumerate)
You can't just update 2d array in-place through assigning to local variable j = -1 (which is reinitialized on each loop iteration for y, j in enumerate(i)).
In your simple case you can update your matrix with the following simple traversal:
matrix = [[0, 0, 0], [0, 1, 0], [1, 1, 1]]
for i, row in enumerate(matrix):
for j, val in enumerate(row):
if val != 0: matrix[i][j] = -1
print(matrix) # [[0, 0, 0], [0, -1, 0], [-1, -1, -1]]
Though Numpy provides a more powerful way for updating matrices:
import numpy as np
matrix = np.array([[0, 0, 0], [0, 1, 0], [1, 1, 1]])
matrix = np.where(matrix == 0, matrix, -1)
print(matrix)
I am try to code a game which has a 3x3 grid (think noughts & crosses [US=Tic Tac Toe]).
Each cell has a weighting. When the player places a counter a score is calculated. I want my code to go through the 3x3 matrix and find the top score and return the co-ordinates of the cell where that top score is found (this works) - but if there are several cells with an equal top score I want to return a list containing each of the places where that score is found.
Here is a cut-down version of my code (with LOTS of print statements to try to work out why it's not working).
The intention is that the loop records a simple list ("pos") which has the row and column co-ordinates (e.g. [0][2]) and appends this to a running list of equal scores ("possibles")
If instead of trying to append a 2-entry list I put in a single random number, the overall list ("possibles") builds as expected, but appending the 2-entry list results in a duplicated list of the final position (see output).
I clearly have a logic problem, but I am new to Python. Can anyone show me where I have gone wrong?
def test():
val = 1
max_val = 0
possibles = [] # This is the list where I will store a list of equally weighted positions
pos = [] # This is simply a 2 number co-ordinate
pos.append("") # Get it ready for row & col references
pos.append("")
for row in range (0,3):
for col in range (0,3):
print("Testing row",row,"col",col)
print("Possibles so far",possibles)
print("Pos=",pos)
pos[0] = row
pos[1] = col
print("Now pos=",pos)
#possibles.append(randint(0,100)) # This works
possibles.append(pos) # This doesn't
print("List of equals",possibles)
test()
Output:
Testing row 0 col 0
Possibles so far []
Pos= ['', '']
Now pos= [0, 0]
Testing row 0 col 1
Possibles so far [[0, 0]]
Pos= [0, 0]
Now pos= [0, 1]
Testing row 0 col 2
Possibles so far [[0, 1], [0, 1]]
Pos= [0, 1]
Now pos= [0, 2]
Testing row 1 col 0
Possibles so far [[0, 2], [0, 2], [0, 2]]
Pos= [0, 2]
Now pos= [1, 0]
Testing row 1 col 1
Possibles so far [[1, 0], [1, 0], [1, 0], [1, 0]]
Pos= [1, 0]
Now pos= [1, 1]
Testing row 1 col 2
Possibles so far [[1, 1], [1, 1], [1, 1], [1, 1], [1, 1]]
Pos= [1, 1]
Now pos= [1, 2]
Testing row 2 col 0
Possibles so far [[1, 2], [1, 2], [1, 2], [1, 2], [1, 2], [1, 2]]
Pos= [1, 2]
Now pos= [2, 0]
Testing row 2 col 1
Possibles so far [[2, 0], [2, 0], [2, 0], [2, 0], [2, 0], [2, 0], [2, 0]]
Pos= [2, 0]
Now pos= [2, 1]
Testing row 2 col 2
Possibles so far [[2, 1], [2, 1], [2, 1], [2, 1], [2, 1], [2, 1], [2, 1], [2, 1]]
Pos= [2, 1]
Now pos= [2, 2]
List of equals [[2, 2], [2, 2], [2, 2], [2, 2], [2, 2], [2, 2], [2, 2], [2, 2], [2, 2]]
You append the same object pos over and over again to your list. If you change its value in the next loop, all its representations are changed as well. You can test this. Append at the end of your test function:
for item in possibles:
print(item, id(item))
See, all list items have the same id.
To avoid this, assign a new object in each loop:
def test():
possibles = []
for row in range (3):
for col in range (3):
pos = [row, col] #create a new list
print("Now pos=",pos)
possibles.append(pos) #and append this new element
print("List of equals",possibles)
test()
It looks similar, but instead of changing element [0] and [1] of an existing list pos, now in each loop a new list is created. If you check it with id(item) from above, all list elements of possibles have now a different id.
I'm trying to construct permutations of a list with the i-th bit flipped every time.
For example:
With input:
[1,0,0,0]
To get:
[0,0,0,0]
[1,1,0,0]
[1,0,1,0]
[1,0,0,1]
I wrote a method which given a list returns the same list with the bit at position p changed:
def flipBit(l,p):
l[p] = ~l[p]&1
return l
And I'm trying to apply it using a map(), but I can't even get a basic example working:
p=list(map(flipBit, [[1,0,0]]*3,range(3))))
This is what it returns:
[[0, 1, 1], [0, 1, 1], [0, 1, 1]]
while expecting:
[[0, 0, 0], [1, 1, 0], [1, 0, 1]]
What I'm I doing wrong? (if anyone can suggest an even shorter code for this maybe without using a flipbit method I'd appreciate it as I won't really use flipbit other than this purpose and I want to keep the code concise and clean)
The issue is that [[1,0,0]]*3 creates a list containing three references to the same sublist. When you change one sublist, they all change.
Here is one way to fix this:
>>> list(map(flipBit, [[1,0,0] for _ in range(3)], range(3)))
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
[[0, 0, 0], [1, 1, 0], [1, 0, 1]]
And here is a way to implement this functionality without using a helper function:
>>> l = [1, 0, 0]
>>> [l[:i] + [1-l[i]] + l[i+1:] for i in range(len(l))]
[[0, 0, 0], [1, 1, 0], [1, 0, 1]]
The code you posted is was not valid at all, but I presume you need this:
>>> p=list(map(lambda x: flipBit([1, 0, 0], x), range(3)))
>>> p
[[0, 0, 0], [1, 1, 0], [1, 0, 1]]
Basically, you map with a lambda function, that partially applies [1, 0, 0] as l, and then takes each element in range(3) and applies it to p.