The task of finding the words in the grid can be done through the solutions provided in the other answers, or through tries, suffix trees or arrays.
As an example, based on the answer given by @Péter Leéh, this would be a modified version for finding all paths using python3:
grid = [
['d', 'r', 'd', 'o', 'r', 's'],
['o', 'b', 'i', 'g', 'n', 'c'],
['g', 'f', 'n', 'm', 't', 'a'],
['x', 's', 'i', 'a', 'n', 't']
]
words1 = [ "dog", "dogma", "cat" ]
# Building the dense grid
dense_grid = {}
for row, line in enumerate(grid):
for col, letter in enumerate(line):
dense_grid.setdefault(letter, []).append((row, col))
# Finding all paths for all words
def is_valid_move(p, q):
return ( p[0] == q[0] and p[1]+1 == q[1] ) or ( p[0]+1 == q[0] and p[1] == q[1] )
def find_all_paths(curr_pos, suffix, dense_grid=dense_grid):
if len(suffix) == 0:
return [[curr_pos]]
possible_suffix_paths = []
for pos in dense_grid[suffix[0]]:
if is_valid_move(curr_pos, pos):
possible_suffix_paths += find_all_paths(pos, suffix[1:])
# Since the list of positions is ordered, I can skip the rest
elif pos[0] - curr_pos[0] >= 2:
break
return [ [curr_pos] + p for p in possible_suffix_paths ]
words_paths = [
[ path for pos in dense_grid[word[0]] for path in find_all_paths(pos, word[1:]) ]
for word in words1
]
The final dense_grid is a dictionary from character to list of positions in the grid, being the positions represented by (row, column):
{
'd': [(0, 0), (0, 2)],
'r': [(0, 1), (0, 4)],
'o': [(0, 3), (1, 0)],
's': [(0, 5), (3, 1)],
'b': [(1, 1)],
'i': [(1, 2), (3, 2)],
'g': [(1, 3), (2, 0)],
'n': [(1, 4), (2, 2), (3, 4)],
'c': [(1, 5)],
'f': [(2, 1)],
'm': [(2, 3)],
't': [(2, 4), (3, 5)],
'a': [(2, 5), (3, 3)],
'x': [(3, 0)]
}
The final words_paths is a list containing for each word a list of all possible paths, being each path defined by a sequence (list) of positions in the grid:
[
[
[(0, 0), (1, 0), (2, 0)], # dog
[(0, 2), (0, 3), (1, 3)]
],
[
[(0, 2), (0, 3), (1, 3), (2, 3), (3, 3)] # dogma
],
[
[(1, 5), (2, 5), (3, 5)] # cat
]
]
After you have all the possible paths for all the words, you can find the words with unique characters by transforming the problem into a digraph maximum flow problem.
To do the transformation of this problem, for every word, you have to create a starting and ending node, henceforth called START_word and END_word. The START_word nodes are connected to all the first positions of the paths of the word, which will then be connected to the second positions, and so on. The last positions of all the paths of the word will then be connected to the END_word node.
The nodes of the positions are unique across the graph. Meaning that words sharing the same positions in the grid, will also share the same nodes.
Now that we have the graph representing all the possible paths for all the words, we just need to connect a SOURCE node to all the starting nodes, and connect all the ending nodes to a TARGET node. With the resulting graph, you can the solve the maximum flow problem, where every edge in the graph as a capacity of 1.
This would be the resulting graph that you get from the problem you defined in the question:

However, to make sure that there are no nodes where the minimum of the in degree and out degree is greater than 1, we also need to add choking nodes. Assuming that a node has this characteristic, we need to remove all the out edges, and connect the original node with a single choking node. To the choking node is then added the original node's out edges.
I tested this idea using the library networkx, and here is the code I used to test it:
import networkx as nx
# Connecting source node with starting nodes
edges = [ ("SOURCE", "START_"+word) for word in words1 ]
# Connecting ending nodes with target nodes
edges += [ ("END_"+word, "TARGET") for word in words1 ]
# Connecting characters between them and to the starting and ending nodes too
edges += list(set(
( s_node if isinstance(s_node, tuple) else s_node,
t_node if isinstance(t_node, tuple) else t_node )
for word, paths in zip(words1, words_paths)
for path in paths
for s_node, t_node in zip(["START_"+word] + path, path + ["END_"+word])
))
# Generating graph from the nodes and edges created
g = nx.DiGraph()
g.add_edges_from(edges, capacity=1)
# Adding choke nodes if required
node_edge_dict = {}
nodes_indeg_gt1 = [ node for node, in_deg in g.in_degree() if not isinstance(node, str) and in_deg > 1 ]
for s_node, t_node in g.out_edges(nodes_indeg_gt1):
node_edge_dict.setdefault(s_node, []).append(t_node)
for node, next_nodes in node_edge_dict.items():
if len(next_nodes) <= 1: continue
choke_node = node + (-1,)
g.add_edge(node, choke_node, capacity=1)
g.add_edges_from([ (choke_node, p) for p in next_nodes ], capacity=1)
g.remove_edges_from([ (node, p) for p in next_nodes ])
# Solving the maximum flow problem
num_words, max_flow_dict = nx.algorithms.flow.maximum_flow(g, "SOURCE", "TARGET")
# Extracting final paths for all the words
final_words_path = []
for word in words1:
word_path = []
start = "START_"+word
end = "END_"+word
node = start
while node != end:
node = next( n for n,f in max_flow_dict[node].items() if f == 1 )
if isinstance(node, str) or len(node) == 3: continue
word_path.append(node)
final_words_path.append(word_path)
print(final_words_path)
The output for the problem stated in the question is this:
[
[(0, 0), (1, 0), (2, 0)], # dog
[(0, 2), (0, 3), (1, 3), (2, 3), (3, 3)], # dogma
[(1, 5), (2, 5), (3, 5)] # cat
]