It's a rainy day here, so I wrote a solution. I can send if you want, but maybe you will soon find it yourself?
Here are some tips:
your code doesn't seem to start at 10 in (2,2)
when trying a new value, you can add it to any empty space. the best place to look is one that has many neighbors because it allows you to quickly test and reject bad values.
suggested above, or another way of saying the same thing - my search was above the values. so I chose a place for the “next move” and tried every value there. on the contrary, there would be a search in places (I chose the "next value" and a search with this value in each place), but this is not so effective (see above).
when backtracking and retrying, the same layout always follows. for example, (2.2) is 10, then (2.3) can be 40, then you can find nothing suitable (2.4). so you step back and remove 40 and try a different number (2,3). but the second number you try (after 10 and something in (2.2)) is always in (2,3). if you are not so careful, you can end up testing many duplicate combinations. Sorry, not sure if this is very clear. basically - select the "path" that you fill in and stick to it when searching and backtracking. since this path was chosen to maximize the number of neighbors (the point above), I built it as I continued, but kept the cache of the paths that I used in the backtracking. it's easier to explain by showing the code ...
For the table, I used an array of arrays. when copying, I reused columns that were not changed. this should reduce memory usage (I don't know if this is important).
the search requires only 40x repetition (once for each value), so the stack is quite large.
A simple search in python, which in turn tries each value, returning with an error, works on my laptop for ~ 4 minutes (provided that you use the prompts above) (without printing, a slightly modified version takes only 8 seconds).
I found it useful to have a python function that, given the grid and position, returns a list (well, a generator, with yield ) of neighbors' coordinates. which made recording of other functions, such as those that check whether the movement is in order, easier.
anyway, if you need a code or solution (I changed my code to print everything, and there was only one), just ask and I will send a message. Of course, he may have a mistake: o)
Decision
I changed this a bit and now it prints out the solution (2,2) = 10 and then searches for all the solutions (which still work for me):
#!/usr/bin/python3 nx, ny = 4, 5 values = [1,2,3,4,5,6,7,8,9,10,12,18,20,21,24,27,30,35,36,40] # grid[x][y] so it is a list of columns (prints misleadingly!) grid = [[0 for _ in range(ny)] for _ in range(nx)] # cache these to avoid re-calculating xy_moves = {} debug = False def neighbours(grid, x, y): 'coordinates of vertical/horizontal neighbours' for (xx, yy) in [(x-1,y),(x+1,y),(x,y-1),(x,y+1)]: if xx > -1 and xx < nx and yy > -1 and yy < ny: yield xx, yy def filled_neighbours(grid, x, y): 'filter "neighbours" to give only filled cells' return filter(lambda xy: grid[xy[0]][xy[1]], neighbours(grid, x, y)) def count_neighbours(grid, x, y): 'use this to find most-constrained location' return sum(1 for _ in filled_neighbours(grid, x, y)) def next_xy(grid, depth): '''given a certain depth in the search, where should we move next? choose a place with lots of neighbours so that we have good constraints (and so can reject bad moves)''' if depth not in xy_moves: best, x, y = 0, nx // 2, ny // 2 # default to centre for xx in range(nx): for yy in range(ny): if not grid[xx][yy]: count = count_neighbours(grid, xx, yy) if count > best: best, x, y = count, xx, yy xy_moves[depth] = (x, y) if debug: print('next move for %d is %d,%d' % (depth, x, y)) return xy_moves[depth] def drop_value(value, values): 'remove value from the values' return [v for v in values if v != value] def copy_grid(grid, x, y, value): 'copy grid, replacing the value at x,y' return [[value if j == y else grid[i][j] for j in range(ny)] if x == i else grid[i] for i in range(nx)] def move_ok(grid, x, y, value): 'are all neighbours multiples?' for (xx, yy) in filled_neighbours(grid, x, y): g = grid[xx][yy] if (g > value and g % value) or (g < value and value % g): if debug: print('fail: %d at %d,%d in %s' % (value, x, y, grid)) return False return True def search(grid, values, depth=0): 'search over all values, backtracking on failure' if values: (x, y) = next_xy(grid, depth) for value in values: if move_ok(grid, x, y, value): if debug: print('add %d to %d,%d' % (value, x, y)) for result in search(copy_grid(grid, x, y, value), drop_value(value, values), depth+1): yield result else: yield grid # run the search, knowing that (2,2) (which is (1,1) for zero-indexing) # has the value 10. for result in search(copy_grid(grid, 1, 1, 10), drop_value(10, values)): print(result) # how many solutions in total? #xy_moves = {} # reset cache #for (n, solution) in enumerate(search(grid, values)): # print('%d: %s' % (n, solution))
starts by choosing a square where it will add the next number using next_xy() . he selects the location of as many existing numbers as possible so that he can test and reject the numbers efficiently (the position is saved in xy_moves , so it does not need to be re-detected if we go back). for each value, it checks to see if the value works at that position with move_ok . if so, it calculates a new grid (with added value) and a new list of values (with deletion of the used value) and recurses. recursion ends when there are no added values.
and here is the result (each internal list is a column):
> time ./grid.py [[4, 20, 5, 35, 7], [40, 10, 30, 1, 21], [8, 2, 6, 18, 3], [24, 12, 36, 9, 27]] real 0m5.909s
[removed incorrect comment about recursion and generators]
Update
he completed a global search - if you do not fix (2,2) at the beginning, there seem to be only 12 solutions (3 different solutions if you ignore simple symmetries).
update 2
final code, including finding all solutions without symmetrical duplicates