Posting more utility code
see github: https://gist.github.com/1233012#file_new.cpp
This is basically an much better approach to generating all possible permutations based on much simpler approach (therefore I had no real reason to post it before: As it stands right now, it doesn't do anything more than the python code).
I decided to share it anyways, as you might be able to get some profit out of this as the basis for an eventual solution.
Pro:
- much faster
- smarter algorithm (leverages STL and maths :))
- instruction optimization
- storage optimization
- generic problem model
- model and algorithmic ideas can be used as basis for proper algorithm
- basis for a good OpenMP parallelization (n-way, for n rows) designed-in (but not fleshed out)
Contra:
- The code is much more efficient at the cost of flexibility: adapting the code to build in logic about the constraints and cost heuristics would be much easier with the more step-by-step Python approach
All in all I feel that my C++ code could be a big win IFF it turns out that Simulated Annealing is appropriate given the cost function(s); The approach taken in the code would give
- a highly efficient storage model
- a highly efficient way to generate random / closely related new grid configurations
- convenient display functions
Mandatory (abritrary...) benchmark data point (comparison to the python version:)
a b c d e
f g h i j
k l m n o
p q r s t
Result: 207360000
real 0m13.016s
user 0m13.000s
sys 0m0.010s
Here is what we got up till now:
From the description I glean the suggestion that you have a basic graph like 
a path has to be constructed that visits all nodes in the grid (Hamiltonian cycle).
The extra constraint is that subsequent nodes have to be taken from the next rank (a-d, e-h, i-l being the three ranks; once a node from the last rank was visited, the path has to continue with any unvisited node from the first rank
The edges are weighted, in that they have a cost associated. However, the weight function is not traditional for graph algorithms in that the cost depends on the full path, not just the end-points of each edge.
In the light of this I believe we are in the realm of 'Full Cover' problems (requiring A* algorithm, most famous from Knuths Dancing Links paper).
Specifically Without further information (equivalence of paths, specific properties of the cost function) the best known algorithm to get the 'cheapest' hamiltonian path that satisfies the constraints will be to
- generate all possible such paths
- calculate the actual cost function for each
- choose the minimum cost path
Which is why I have set off and wrote a really dumb brute force generator that generates all the unique paths possible in a generic grid of NxM.
The End Of The Universe
Output for the 3×4 sample grid is 4!3 = 13824 possible paths... Extrapolating that to 6×48 columns, leads to 6!48 = 1.4×10137 possibilities. It is very clear that without further optimization the problem is untractible (NP Hard or something -- I never remember quite the subtle definitions).
The explosion of runtime is deafening:
- 3×4 (measured) takes about 0.175s
- 4×5 (measured) took about 6m5s (running without output and under PyPy 1.6 on a fast machine)
- 5×6 would take roughly 10 years and 9+ months...
At 48x6 we would be looking at... what... 8.3x10107 years (read that closely)
Anyways, here is the python code (all preset for 2×3 grid)
#!/usr/bin/python
ROWS = 2
COLS = 3
## different cell representations
def cell(r,c):
## exercise for the reader: _gues_ which of the following is the fastest
## ...
## then profile it :)
index = COLS*(r) + c
# return [ r,c ]
# return ( r,c )
# return index
# return "(%i,%i)" % (r,c)
def baseN(num,b,numerals="abcdefghijklmnopqrstuvwxyz"):
return ((num == 0) and numerals[0]) or (baseN(num // b, b, numerals).lstrip(numerals[0]) + numerals[num % b])
return baseN(index, 26)
ORIGIN = cell(0,0)
def debug(t): pass; #print t
def dump(grid): print("\n".join(map(str, grid)))
def print_path(path):
## Note: to 'normalize' to start at (1,1) node:
# while ORIGIN != path[0]: path = path[1:] + path[:1]
print " -> ".join(map(str, path))
def bruteforce_hamiltonians(grid, whenfound):
def inner(grid, whenfound, partial):
cols = len(grid[-1]) # number of columns remaining in last rank
if cols<1:
# assert 1 == len(set([ len(r) for r in grid ])) # for debug only
whenfound(partial) # disable when benchmarking
pass
else:
#debug(" ------ cols: %i ------- " % cols)
for i,rank in enumerate(grid):
if len(rank)<cols: continue
#debug("debug: %i, %s (partial: %s%s)" % (i,rank, "... " if len(partial)>3 else "", partial[-3:]))
for ci,cell in enumerate(rank):
partial.append(cell)
grid[i] = rank[:ci]+rank[ci+1:] # modify grid in-place, keeps rank
inner(grid, whenfound, partial)
grid[i] = rank # restore in-place
partial.pop()
break
pass
# start of recursion
inner(grid, whenfound, [])
grid = [ [ cell(c,r) for r in range(COLS) ] for c in range(ROWS) ]
dump(grid)
bruteforce_hamiltonians(grid, print_path)