def graph_search(states, goal_reached, get_successors, combine, old_states=None):
    """
    Given some initial states, explore a state space until reaching the goal,
    taking care not to re-explore previously visited states.

    `states`, `goal_reached`, `get_successors`, and `combine` are identical to
    those arguments in `tree_search`.
    `old_states` is a list of previously encountered states--these should not
    be re-vistited during the search.

    When the goal is reached, the goal state is returned.
    """
    old_states = old_states or [] # initialize, if this is the initial call
    
    # Check for success and failure.
    if not states:
        return None
    if goal_reached(states[0]):
        return states[0]

    def visited(state):
        # A state is "visited" if it's in the list of current states or has
        # been encountered previously.
        return any(state == s for s in states + old_states)

    # Filter out the "visited" states from the next state's successors.
    new_states = [s for s in get_successors(states[0]) if not visited(s)]

    # Combine the new states with the existing ones and recurse.
    next_states = combine(new_states, states[1:])
    return graph_search(next_states, goal_reached, get_successors,
                        combine, old_states + [states[0]])