Find the paths!

Python 3, 238 237 200 199 192 181 bytes

def f(a,i=0):F=lambda i,n,c:29>i>=0!=" "!=a[i]==c!=n and(a.__setitem__(i,n)or-~sum(F(i+j,n,c)for j in[-1,1,-6,6]));j=i+i//5;F(j,[a[j],"x"][2<F(j,1,a[j])],1);i>23or f(a,i+1);return a

Defines a function f(a) that takes the input as an array of characters and returns the same array modified. (Arrays of characters are acceptable as strings by default.)

Ungolfed with explanation

The modified code is recursive, but works the same.

# The main function; fills all continuous nonempty areas of size >= 3 in array
# with x's. Both modifies and returns array.
def findpaths(array):
    # Fills a continuous area of curr_char in array with new_char, starting
    # from index. Returns the number of cells affected.
    def floodfill(index, new_char, curr_char):
        if (0 <= index < 29                   # Check that the position is in bounds
                and (index + 1) % 6 != 0      # Don't fill newlines
                and array[index] != " "       # Don't fill empty cells
                and array[index] == curr_char # Don't fill over other characters
                and curr_char != new_char):   # Don't fill already filled-in cells
            array[index] = new_char # Fill current position
            return (1 # Add neighboring cells' results, plus 1 for this cell
                    + floodfill(index + 1, new_char, curr_char)  # Next char
                    + floodfill(index - 1, new_char, curr_char)  # Previous char
                    + floodfill(index + 6, new_char, curr_char)  # Next line
                    + floodfill(index - 6, new_char, curr_char)) # Previous line
        return 0 # Nothing was filled. The golfed solution returns False here,
                 # but that's coerced to 0 when adding.

    for i in range(25): # Loop through the 25 cells
        i += i // 5 # Accommodate for newlines in input
        curr_char = array[i] # Get the cell's contents
        # Fill the area from the cell with dummies
        area_size = floodfill(i, 1, curr_char)
        # Convert dummies to "x" if area was large enough, back to original otherwise
        fill_char = "x" if 2 < area_size else curr_char
        floodfill(i, fill_char, 1)
    return array

Ruby, 304 bytes

def b(s,i)
  @v=[]
  b2(s,i,s[i])
end
def b2(s,i,c)
  if(0...s.size)===i&&s[i]==c&&!@v[i]
    @v[i]=s[i]='x'
    [1,-1,6,-6].each{|j|b2(s,i+j,c)}
  end
  s
end
def f(s)
  z = s.dup
  ps = ->(i){b(z.dup,i).scan('x').size}
  (0...s.size).each{|i|b(s, i)if ![' ',"\n"].include?(s[i])&&ps.call(i)>2}
  s
end

example usage:

puts f(File.read("map.txt"))

the code reuses the 'blot' method to calculate the path length.

variables/methods:

  • f(s): function to convert map string, returns new map with 'x's
  • ps(i): path size from map index i (where x = i % 6, y = i / 6)
  • s: input string, map lines separated by "\n"
  • z: copy of input string
  • b(s,i): 'blot' function: writes 'x' from map index i over paths
  • @v: 'visited' array

Attempt at more detailed explanation:

make a copy of the input string, which we use for finding the length of the path from any given point in the map.

z = s.dup

define a 'ps' (path length) anonymous function (lambda) which takes the map index i as an argument. it returns the length of the path from that point. it does this by calling the 'b' (blot) method to insert x's on a copy of the original map and then counting the number of x's in the returned string.

  ps = ->(i){b(z.dup,i).scan('x').size}

the following part iterates over each character in the map (index i, character s[i]). it calls the 'b' (blot) function on map position i if the path length from position i is greater than 2, and if it's not a space or newline character.

  (0...s.size).each { |i|
     b(s, i) if ![' ',"\n"].include?(s[i]) && ps.call(i) > 2
  }

the b (blot) function takes the map string and an index as argument. it initialises @v (visited array) and calls the b2 helper function.

def b(s,i)
  @v=[]
  b2(s,i,s[i])
end

the b2 function takes the map string, a map position (i), and a character in the current path (c). it calls itself recursively to replace connected sections of digits with the 'x' character. it returns the input string (this is so the ps function can call scan() on the return value).

this if statement is checking that the map position (i) given is within the bounds of the string (0...s.size) and that the character at s[i] is the same as the starting character. also @v[i] is checked to avoid infinite recursion.

if(0...s.size) === i && s[i] == c && !@v[i]

this is the bit that replaces the character at index (i) with the 'x' character. it also marks that index as visited.

@v[i] = s[i] = 'x'

this is where b2 calls itself recursively searching for the path. i+1 is one character to the right, i-1 is one character to the left, i+6 is one row down (5 digits + 1 newline = 6 characters), i-6 is one row up.

[1,-1,6,-6].each { |j| b2(s, i+j, c) }