001 package aima.search.uninformed; 002 003 import java.util.ArrayList; 004 import java.util.List; 005 006 import aima.search.framework.BidirectionalProblem; 007 import aima.search.framework.GraphSearch; 008 import aima.search.framework.Metrics; 009 import aima.search.framework.Node; 010 import aima.search.framework.Problem; 011 import aima.search.framework.Search; 012 import aima.search.framework.SearchUtils; 013 import aima.search.framework.Successor; 014 import aima.search.nodestore.CachedStateNodeStore; 015 import aima.search.nodestore.FIFONodeStore; 016 017 /** 018 * Artificial Intelligence A Modern Approach (2nd Edition): page 79. 019 * Bidirectional search. 020 */ 021 022 /** 023 * @author Ciaran O'Reilly 024 * 025 */ 026 public class BidirectionalSearch implements Search { 027 public enum SearchOutcome { 028 PATH_FOUND_FROM_ORIGINAL_PROBLEM, PATH_FOUND_FROM_REVERSE_PROBLEM, PATH_FOUND_BETWEEN_PROBLEMS, PATH_NOT_FOUND 029 }; 030 031 protected Metrics metrics; 032 033 private SearchOutcome searchOutcome = SearchOutcome.PATH_NOT_FOUND; 034 035 private static final String NODES_EXPANDED = "nodesExpanded"; 036 037 private static final String QUEUE_SIZE = "queueSize"; 038 039 private static final String MAX_QUEUE_SIZE = "maxQueueSize"; 040 041 private static final String PATH_COST = "pathCost"; 042 043 public BidirectionalSearch() { 044 metrics = new Metrics(); 045 } 046 047 public List<String> search(Problem p) throws Exception { 048 049 assert (p instanceof BidirectionalProblem); 050 051 searchOutcome = SearchOutcome.PATH_NOT_FOUND; 052 053 clearInstrumentation(); 054 055 Problem op = ((BidirectionalProblem) p).getOriginalProblem(); 056 Problem rp = ((BidirectionalProblem) p).getReverseProblem(); 057 058 CachedStateNodeStore opFringe = new CachedStateNodeStore( 059 new FIFONodeStore()); 060 CachedStateNodeStore rpFringe = new CachedStateNodeStore( 061 new FIFONodeStore()); 062 063 GraphSearch ogs = new GraphSearch(); 064 GraphSearch rgs = new GraphSearch(); 065 // Ensure the instrumentation for these 066 // are cleared down as their values 067 // are used in calculating the overall 068 // bidirectional metrics. 069 ogs.clearInstrumentation(); 070 rgs.clearInstrumentation(); 071 072 Node opNode = new Node(op.getInitialState()); 073 Node rpNode = new Node(rp.getInitialState()); 074 opFringe.add(opNode); 075 rpFringe.add(rpNode); 076 077 setQueueSize(opFringe.size() + rpFringe.size()); 078 setNodesExpanded(ogs.getNodesExpanded() + rgs.getNodesExpanded()); 079 080 while (!(opFringe.isEmpty() && rpFringe.isEmpty())) { 081 // Determine the nodes to work with and expand their fringes 082 // in preparation for testing whether or not the two 083 // searches meet or one or other is at the GOAL. 084 if (!opFringe.isEmpty()) { 085 opNode = opFringe.remove(); 086 ogs.addExpandedNodesToFringe(opFringe, opNode, op); 087 } else { 088 opNode = null; 089 } 090 if (!rpFringe.isEmpty()) { 091 rpNode = rpFringe.remove(); 092 rgs.addExpandedNodesToFringe(rpFringe, rpNode, rp); 093 } else { 094 rpNode = null; 095 } 096 097 setQueueSize(opFringe.size() + rpFringe.size()); 098 setNodesExpanded(ogs.getNodesExpanded() + rgs.getNodesExpanded()); 099 100 // 101 // First Check if either fringe contains the other's state 102 if (null != opNode && null != rpNode) { 103 Node popNode = null; 104 Node prpNode = null; 105 if (opFringe.containsNodeBasedOn(rpNode.getState())) { 106 popNode = opFringe.getNodeBasedOn(rpNode.getState()); 107 prpNode = rpNode; 108 } else if (rpFringe.containsNodeBasedOn(opNode.getState())) { 109 popNode = opNode; 110 prpNode = rpFringe.getNodeBasedOn(opNode.getState()); 111 // Need to also check whether or not the nodes that 112 // have been taken off the fringe actually represent the 113 // same state, otherwise there are instances whereby 114 // the searches can pass each other by 115 } else if (opNode.getState().equals(rpNode.getState())) { 116 popNode = opNode; 117 prpNode = rpNode; 118 } 119 if (null != popNode && null != prpNode) { 120 List<String> actions = retrieveActions(op, rp, popNode, 121 prpNode); 122 // It may be the case that it is not in fact possible to 123 // traverse from the original node to the goal node based on 124 // the reverse path (i.e. unidirectional links: e.g. 125 // InitialState(A)<->C<-Goal(B) ) 126 if (null != actions) { 127 return actions; 128 } 129 } 130 } 131 132 // 133 // Check if the original problem is at the GOAL state 134 if (null != opNode && op.isGoalState(opNode.getState())) { 135 // No need to check return value for null here 136 // as an action path discovered from the goal 137 // is guaranteed to exist 138 return retrieveActions(op, rp, opNode, null); 139 } 140 // 141 // Check if the reverse problem is at the GOAL state 142 if (null != rpNode && rp.isGoalState(rpNode.getState())) { 143 List<String> actions = retrieveActions(op, rp, null, rpNode); 144 // It may be the case that it is not in fact possible to 145 // traverse from the original node to the goal node based on 146 // the reverse path (i.e. unidirectional links: e.g. 147 // InitialState(A)<-Goal(B) ) 148 if (null != actions) { 149 return actions; 150 } 151 } 152 } 153 154 // Empty List can indicate already at Goal 155 // or unable to find valid set of actions 156 return new ArrayList<String>(); 157 } 158 159 public SearchOutcome getSearchOutcome() { 160 return searchOutcome; 161 } 162 163 public Metrics getMetrics() { 164 return metrics; 165 } 166 167 public void clearInstrumentation() { 168 metrics.set(NODES_EXPANDED, 0); 169 metrics.set(QUEUE_SIZE, 0); 170 metrics.set(MAX_QUEUE_SIZE, 0); 171 metrics.set(PATH_COST, 0.0); 172 } 173 174 public int getNodesExpanded() { 175 return metrics.getInt(NODES_EXPANDED); 176 } 177 178 public void setNodesExpanded(int nodesExpanded) { 179 metrics.set(NODES_EXPANDED, nodesExpanded); 180 } 181 182 public int getQueueSize() { 183 return metrics.getInt(QUEUE_SIZE); 184 } 185 186 public void setQueueSize(int queueSize) { 187 metrics.set(QUEUE_SIZE, queueSize); 188 int maxQSize = metrics.getInt(MAX_QUEUE_SIZE); 189 if (queueSize > maxQSize) { 190 metrics.set(MAX_QUEUE_SIZE, queueSize); 191 } 192 } 193 194 public int getMaxQueueSize() { 195 return metrics.getInt(MAX_QUEUE_SIZE); 196 } 197 198 public double getPathCost() { 199 return metrics.getDouble(PATH_COST); 200 } 201 202 public void setPathCost(Double pathCost) { 203 metrics.set(PATH_COST, pathCost); 204 } 205 206 // 207 // PRIVATE METHODS 208 // 209 private List<String> retrieveActions(Problem op, Problem rp, 210 Node originalPath, Node reversePath) { 211 List<String> actions = new ArrayList<String>(); 212 213 if (null == reversePath) { 214 // This is the simple case whereby the path has been found 215 // from the original problem first 216 setPathCost(originalPath.getPathCost()); 217 searchOutcome = SearchOutcome.PATH_FOUND_FROM_ORIGINAL_PROBLEM; 218 actions = SearchUtils.actionsFromNodes(originalPath 219 .getPathFromRoot()); 220 } else { 221 List<Node> nodePath = new ArrayList<Node>(); 222 Object originalState = null; 223 if (null != originalPath) { 224 nodePath.addAll(originalPath.getPathFromRoot()); 225 originalState = originalPath.getState(); 226 } 227 // Only append the reverse path if it is not the 228 // GOAL state from the original problem (if you don't 229 // you could end up appending a partial reverse path 230 // that looks back on its initial state) 231 if (!op.isGoalState(reversePath.getState())) { 232 List<Node> rpath = reversePath.getPathFromRoot(); 233 for (int i = rpath.size() - 1; i >= 0; i--) { 234 // Ensure do not include the node from the reverse path 235 // that is the one that potentially overlaps with the 236 // original path (i.e. if started in goal state or where 237 // they meet in the middle). 238 if (!rpath.get(i).getState().equals(originalState)) { 239 nodePath.add(rpath.get(i)); 240 } 241 } 242 } 243 244 if (!canTraversePathFromOriginalProblem(op, nodePath, actions)) { 245 // This is where it is possible to get to the initial state 246 // from the goal state (i.e. reverse path) but not the other way 247 // round, null returned to indicate an invalid path found from 248 // the reverse problem 249 return null; 250 } 251 252 if (null == originalPath) { 253 searchOutcome = SearchOutcome.PATH_FOUND_FROM_REVERSE_PROBLEM; 254 } else { 255 // Need to ensure that where the original and reverse paths 256 // overlap, as they can link based on their fringes, that 257 // the reverse path is actually capable of connecting to 258 // the previous node in the original path (if not root). 259 if (canConnectToOriginalFromReverse(rp, originalPath, 260 reversePath)) { 261 searchOutcome = SearchOutcome.PATH_FOUND_BETWEEN_PROBLEMS; 262 } else { 263 searchOutcome = SearchOutcome.PATH_FOUND_FROM_ORIGINAL_PROBLEM; 264 } 265 } 266 } 267 268 return actions; 269 } 270 271 private boolean canTraversePathFromOriginalProblem(Problem op, 272 List<Node> path, List<String> actions) { 273 boolean rVal = true; 274 double pc = 0.0; 275 276 for (int i = 0; i < (path.size() - 1); i++) { 277 Object currentState = path.get(i).getState(); 278 Object nextState = path.get(i + 1).getState(); 279 List<Successor> successors = op.getSuccessorFunction() 280 .getSuccessors(currentState); 281 boolean found = false; 282 for (Successor s : successors) { 283 if (nextState.equals(s.getState())) { 284 found = true; 285 pc += op.getStepCostFunction().calculateStepCost( 286 currentState, nextState, s.getAction()); 287 actions.add(s.getAction()); 288 break; 289 } 290 } 291 292 if (!found) { 293 rVal = false; 294 break; 295 } 296 } 297 298 setPathCost(true == rVal ? pc : 0.0); 299 300 return rVal; 301 } 302 303 private boolean canConnectToOriginalFromReverse(Problem rp, 304 Node originalPath, Node reversePath) { 305 boolean rVal = true; 306 307 // Only need to test if not already at root 308 if (!originalPath.isRootNode()) { 309 rVal = false; 310 List<Successor> successors = rp.getSuccessorFunction() 311 .getSuccessors(reversePath.getState()); 312 for (Successor s : successors) { 313 if (originalPath.getParent().getState().equals(s.getState())) { 314 rVal = true; 315 break; 316 } 317 } 318 } 319 320 return rVal; 321 } 322 }