#!/usr/bin/env python # Slitherlink # Phil Bordelon import os import sys DEBUG = os.getenv("DEBUG", False) # Two defines for switching between state during the reading process. LINK = 1 GRID = 2 # A define for the ? character that means 'dunno.' UNKNOWN = -1 # A very boring Container class, to make .foo easy to use. class Container(object): pass def readPuzzle(): puzzle = Container() puzzle.links = [] puzzle.grid = {} # Read in the height and width. height, width = [int(x) for x in sys.stdin.readline().strip().split()] puzzle.height = height puzzle.width = width # The representation of a puzzle toggles between lines that consist of # links and nodes and lines that consist of links and values. We will # use a very basic state machine that switches between those two # states to handle each "type" of input. # The first row in the puzzle is links and nodes. row_state = LINK # Loop through all N*2+1 rows of the grid. for i in range(height * 2 + 1): line = sys.stdin.readline().strip() if row_state == LINK: # Use another state machine to go between the 'grid' (the # signs we # don't care about) and the links. char_state = GRID for j in range(width * 2 + 1): if char_state == GRID: # Skip this character; switch to the other state for the next. char_state = LINK elif char_state == LINK: if line[j] == "-": puzzle.links.append((i, j)) # Switch back to the 'skip' state for the next character. char_state = GRID # Switch state for the next line to values-and-links. row_state = GRID elif row_state == GRID: # Once again, we need state, so we reuse 'link' and 'grid'. char_state = LINK for j in range(width * 2 + 1): character = line[j] if char_state == LINK: if character == "|": # Add the link to the list. puzzle.links.append((i, j)) # Switch to the 'value' state for the next character. char_state = GRID elif char_state == GRID: # It should be one of 0, 1, 2, 3, or ?. Handle these cases. if character.isdigit(): if int(character) < 0 or int(character) > 3: sys.exit("ERROR: Invalid number %s in row '%s'!" % (character, line)) puzzle.grid[(i, j)] = int(character) else: puzzle.grid[(i, j)] = UNKNOWN # Switch to the 'link' state for the next character. char_state = LINK # Switch state for the next line to nodes-and-links. row_state = LINK return puzzle def linkListAtSpot(location): # The list of possible links at a location. Although technically # you can't go further left or right or up or down than the limits # of the board size, because of our representation we can cheat and # not special-case those values, as they won't show up in the link # list. I like lazy. (Of course, I spent lots of space here /explaining/ # it ...) Interestingly, this actually works for both nodes and # number squares, due to the way that the board is laid out: # # | - # -#- is equivalent to |?| ... from a certain point of view. # | - loc_x, loc_y = location return [(loc_x - 1, loc_y), (loc_x + 1, loc_y), (loc_x, loc_y - 1), (loc_x, loc_y + 1)] def nodeListAtLink(link): # If the link is vertical, it will have an even Y component; if it is # horizontal, it will have an even X component. We use that information # to determine where the nodes that connect to a particular link are. link_x, link_y = link if link_x % 2 == 0: return [(link_x, link_y - 1), (link_x, link_y + 1)] else: return [(link_x - 1, link_y), (link_x + 1, link_y)] def oneAndOnlyOneLoop(puzzle): # As the name implies, this function verifies that there is one and only # one complete loop in the puzzle. # First, the easiest test: All valid loops have at least four segments. if len(puzzle.links) < 4: return False # Get a copy of the list of all links, as we're going to be removing # elements from it as we make sure there's only one loop. link_list = puzzle.links[:] # Pick an arbitrary link; remove it from the list. curr_link = link_list[0] link_list.remove(curr_link) # Find both of its ends. With the link removed from the list, our # goal is to return to the other end having consumed every link. Think # of the loop as a (hopefully) continuous chain; we're removing one link # and nosing our way down the chain in the hopes that we return to the # "other side" of the removed link. curr_node, final_node = nodeListAtLink(curr_link) # Put this link in the list of used links. used_links = [curr_link] # Now, time to loop through all the links, marking them off as we encounter # them. done = False while not done: # Find all unused links at the current node. potential_links_here = linkListAtSpot(curr_node) links_here = [x for x in link_list if x in potential_links_here] # There had damn well only be one, otherwise there are too many # links at this node (as we've already tossed away the /other/ link # that got us here). if len(links_here) != 1: return False # Okay, we've got a link to use. Pull it from the link_list ... link_list.remove(links_here[0]) # ... and move to the node at the other end. We abuse the nodeListAtLink # function; I used to have a custom one that did fancy if/then stuff to # get the other end of a node-plus-link combo, but this is easier. # Please forgive the rather scary abuse of a list comprehension to do so. curr_node = [x for x in nodeListAtLink(links_here[0]) if x != curr_node][0] # If we're at the final_node, we can quit. There may be extra links # here (which is an invalid puzzle), but we'll check that outside the # loop. if curr_node == final_node: done = True # Okay, we've made a loop in the puzzle. If there are /any/ segments left, # the puzzle is invalid. Otherwise, it's got one and only one loop. if len(link_list) > 0: return False return True def checkNumbers(puzzle): # This function makes sure that any non-UNKNOWN ("?") number in the puzzle # has the proper number of links around it. # Loop through the size of the puzzle. Note that the values are at # odd-valued locations on the grid, assuming indices that start at 0: # # 01234 # #-#-# 0 # |?|?| 1 # #-#-# 2 # |?|?| 3 # #-#-# 4 # for i in range(puzzle.height): for j in range(puzzle.width): # Convert the loop numbers to actual grid-number locations. num_x = i * 2 + 1 num_y = j * 2 + 1 # We only care about this location if it's not UNKNOWN. number = puzzle.grid[(num_x, num_y)] if number != UNKNOWN: # Get the list of links surrounding this spot. potential_links = linkListAtSpot((num_x, num_y)) links = [x for x in puzzle.links if x in potential_links] # If the number doesn't equal the number of links, it's invalid. if len(links) != number: return False # We checked all the numbers, and they were all valid. return True if "__main__" == __name__: dataset_count = int(sys.stdin.readline()) for dataset_loop in range(dataset_count): # Read the puzzle in. puzzle = readPuzzle() # First, check for a one-and-only-one loop ... one_loop = oneAndOnlyOneLoop(puzzle) if not one_loop: print "INVALID" else: # Drat, we actually have to check the numbers. valid_numbers = checkNumbers(puzzle) if not valid_numbers: print "INVALID" else: # Passed both tests. print "VALID"