public List <Action> search(Problem p)
        {
            assert(p is BidirectionalProblem);

            searchOutcome = SearchOutcome.PATH_NOT_FOUND;

            clearInstrumentation();

            Problem op = ((BidirectionalProblem)p).getOriginalProblem();
            Problem rp = ((BidirectionalProblem)p).getReverseProblem();

            CachedStateQueue <Node> opFrontier = new CachedStateQueue <Node>();
            CachedStateQueue <Node> rpFrontier = new CachedStateQueue <Node>();

            GraphSearch ogs = new GraphSearch();
            GraphSearch rgs = new GraphSearch();

            // Ensure the instrumentation for these
            // are cleared down as their values
            // are used in calculating the overall
            // bidirectional metrics.
            ogs.clearInstrumentation();
            rgs.clearInstrumentation();

            Node opNode = new Node(op.getInitialState());
            Node rpNode = new Node(rp.getInitialState());

            opFrontier.insert(opNode);
            rpFrontier.insert(rpNode);

            setQueueSize(opFrontier.Count + rpFrontier.Count);
            setNodesExpanded(ogs.getNodesExpanded() + rgs.getNodesExpanded());

            while (!(opFrontier.isEmpty() && rpFrontier.isEmpty()))
            {
                // Determine the nodes to work with and expand their fringes
                // in preparation for testing whether or not the two
                // searches meet or one or other is at the GOAL.
                if (!opFrontier.isEmpty())
                {
                    opNode = opFrontier.pop();
                    opFrontier.AddRange(ogs.getResultingNodesToAddToFrontier(opNode,
                                                                             op));
                }
                else
                {
                    opNode = null;
                }
                if (!rpFrontier.isEmpty())
                {
                    rpNode = rpFrontier.pop();
                    rpFrontier.AddRange(rgs.getResultingNodesToAddToFrontier(rpNode,
                                                                             rp));
                }
                else
                {
                    rpNode = null;
                }

                setQueueSize(opFrontier.Count + rpFrontier.Count);
                setNodesExpanded(ogs.getNodesExpanded() + rgs.getNodesExpanded());

                //
                // First Check if either frontier contains the other's state
                if (null != opNode && null != rpNode)
                {
                    Node popNode = null;
                    Node prpNode = null;
                    if (opFrontier.containsNodeBasedOn(rpNode.getState()))
                    {
                        popNode = opFrontier.getNodeBasedOn(rpNode.getState());
                        prpNode = rpNode;
                    }
                    else if (rpFrontier.containsNodeBasedOn(opNode.getState()))
                    {
                        popNode = opNode;
                        prpNode = rpFrontier.getNodeBasedOn(opNode.getState());
                        // Need to also check whether or not the nodes that
                        // have been taken off the frontier actually represent the
                        // same state, otherwise there are instances whereby
                        // the searches can pass each other by
                    }
                    else if (opNode.getState().Equals(rpNode.getState()))
                    {
                        popNode = opNode;
                        prpNode = rpNode;
                    }
                    if (null != popNode && null != prpNode)
                    {
                        List <Action> actions = retrieveActions(op, rp, popNode,
                                                                prpNode);
                        // It may be the case that it is not in fact possible to
                        // traverse from the original node to the goal node based on
                        // the reverse path (i.e. unidirectional links: e.g.
                        // InitialState(A)<->C<-Goal(B) )
                        if (null != actions)
                        {
                            return(actions);
                        }
                    }
                }

                //
                // Check if the original problem is at the GOAL state
                if (null != opNode && SearchUtils.isGoalState(op, opNode))
                {
                    // No need to check return value for null here
                    // as an action path discovered from the goal
                    // is guaranteed to exist
                    return(retrieveActions(op, rp, opNode, null));
                }
                //
                // Check if the reverse problem is at the GOAL state
                if (null != rpNode && SearchUtils.isGoalState(rp, rpNode))
                {
                    List <Action> actions = retrieveActions(op, rp, null, rpNode);
                    // It may be the case that it is not in fact possible to
                    // traverse from the original node to the goal node based on
                    // the reverse path (i.e. unidirectional links: e.g.
                    // InitialState(A)<-Goal(B) )
                    if (null != actions)
                    {
                        return(actions);
                    }
                }
            }

            // Empty List can indicate already at Goal
            // or unable to find valid set of actions
            return(new List <Action>());
        }