I've recently found myself solving a lot of 2D matrix search problems. A typical approach looks like the skeleton below, which searches for a word along 4-directionally connected paths in a 2D array:
data State = S { word :: String, coord :: Coord } deriving (Eq, Ord, Read, Show)
data Coord = C Int Int deriving (Eq, Ord, Read, Show)
dfsOnN :: Ord r => (a -> r) -> (a -> [a]) -> [a] -> [a]
coordMap :: [[a]] -> Map Coord a
cardinals :: Coord -> [Coord]
neighbors :: Map Coord Char -> State -> [State]
solution :: [[Char]] -> String -> Bool
solution board w = any found . search . states w $ board
where
search = dfsOnN coord (neighbors vals)
found = (== "") . word
vals = coordMap board
The DFS function with signature
dfsOnN :: Ord r => (a -> r) -> (a -> [a]) -> [a] -> [a]
takes a projection function, which sends states to representations suitable for a visited set, and a successors function, which computes neighbors in the graph.
My trouble is the successors function. As you can tell from the type signature of neighbors
, I usually map locations to values and perform lookups on neighboring coordinates to get successors. But this strikes me as a very roundabout way to do things. If we're always given our matrix as a list of lists, the list already "knows" what stuff is nearby, since it's sequential.
In many problems like these, the only reason we translate to coordinates/indices at all is to perform the index arithmetic to get neighbors. The coordinates themselves are completely superfluous to the problem. We also have to bother with lookups that can fail and do Maybe
handling if we use something like a Map Coord Char
, but this should also be unnecessary. The list of lists already "knows" where its bounds are, and we throw this information away when we translate to coordinates.
Question: Is there a concise way to implement a fast, total a -> [a]
neighbors function for 2D matrix problems that doesn't use coordinates?
Constraints:
- Concise means under 100 tokens including type signatures (rough size of my coordinate-full version)
- Solutions that appeal to reasonably popular/common/canonical libraries are welcomed. (So things like
base
,containers
,unordered-containers
,fgl
,heaps
, etc.) - Total means no partial functions like
!!
,head
,maximum
, etc. - Fast means
O(log n)
.
Why the specific constraints? Simply because I already know how to solve it without these constraints. I'm interested in finding a principled approach to these sorts of problems which will work well for competitive programming when a large custom template library is not allowed.
Ideas towards a solution: I wonder if something like a 2D zipper could work.
Note: I asked a question whose title is similar here, but these are quite unrelated, as a value of type [[NonEmpty a]]
or similar is not "compatible" with a DFS function like the one above. It appears as though there is no nice way to grab a value from the list of neighbors of a cell and then in O(1) be able to grab its neighbors in turn, because this would require us to index back into the list in the correct location, but we've thrown all information about location away in this representation.