code: def best_first_graph_search(problem, f):
      
      f = memoize(f, 'f')
      node = Node(problem.initial)
      if problem.goal_test(node.state):
          return node
      frontier = PriorityQueue(min, f)
      frontier.append(node)
      explored = set()
      while frontier:
          node = frontier.pop()
          if problem.goal_test(node.state):
              return node
          explored.add(node.state)
          for child in node.expand(problem):
              if child.state not in explored and child not in frontier:
                  frontier.append(child)
              elif child in frontier:
                  incumbent = frontier[child]
                  if f(child) < f(incumbent):
                      del frontier[incumbent]
                      frontier.append(child)
      return None
docs: Search the nodes with the lowest f scores first.
      You specify the function f(node) that you want to minimize; for example,
      if f is a heuristic estimate to the goal, then we have greedy best
      first search; if f is node.depth then we have breadth-first search.
      There is a subtlety: the line "f = memoize(f, 'f')" means that the f
      values will be cached on the nodes as they are computed. So after doing
      a best first search you can examine the f values of the path returned.