package aima.core.search.local; import aima.core.agent.Action; import aima.core.search.framework.*; import aima.core.search.framework.evalfunc.HeuristicFunction; import aima.core.search.framework.problem.Problem; import aima.core.util.CancelableThread; import java.util.List; import java.util.function.Consumer; /** * Artificial Intelligence A Modern Approach (3rd Edition): Figure 4.2, page * 122.<br> * <br> * * <pre> * function HILL-CLIMBING(problem) returns a state that is a local maximum * * current <- MAKE-NODE(problem.INITIAL-STATE) * loop do * neighbor <- a highest-valued successor of current * if neighbor.VALUE <= current.VALUE then return current.STATE * current <- neighbor * </pre> * * Figure 4.2 The hill-climbing search algorithm, which is the most basic local * search technique. At each step the current node is replaced by the best * neighbor; in this version, that means the neighbor with the highest VALUE, * but if a heuristic cost estimate h is used, we would find the neighbor with * the lowest h. * * @author Ravi Mohan * @author Mike Stampone * @author Ruediger Lunde */ public class HillClimbingSearch implements SearchForActions, SearchForStates, Informed { public enum SearchOutcome { FAILURE, SOLUTION_FOUND } public static final String METRIC_NODES_EXPANDED = "nodesExpanded"; public static final String METRIC_NODE_VALUE = "nodeValue"; private HeuristicFunction hf = null; private final NodeExpander nodeExpander; private SearchOutcome outcome = SearchOutcome.FAILURE; private Object lastState = null; private Metrics metrics = new Metrics(); /** * Constructs a hill-climbing search from the specified heuristic function. * * @param hf * a heuristic function */ public HillClimbingSearch(HeuristicFunction hf) { this(hf, new NodeExpander()); } public HillClimbingSearch(HeuristicFunction hf, NodeExpander nodeExpander) { this.hf = hf; this.nodeExpander = nodeExpander; nodeExpander.addNodeListener((node) -> metrics.incrementInt(METRIC_NODES_EXPANDED)); } @Override public void setHeuristicFunction(HeuristicFunction hf) { this.hf = hf; } @Override public List<Action> findActions(Problem p) { nodeExpander.useParentLinks(true); Node node = findNode(p); return node == null ? SearchUtils.failure() : SearchUtils.getSequenceOfActions(node); } @Override public Object findState(Problem p) { nodeExpander.useParentLinks(false); Node node = findNode(p); return node == null ? null : node.getState(); } /** * Returns a list of actions to the local maximum if the local maximum was * found, a list containing a single NoOp Action if already at the local * maximum, or an empty list if the search was canceled by the user. * * @param p * the search problem * * @return a list of actions to the local maximum if the local maximum was * found, a list containing a single NoOp Action if already at the * local maximum, or an empty list if the search was canceled by the * user. */ // function HILL-CLIMBING(problem) returns a state that is a local maximum public Node findNode(Problem p) { clearInstrumentation(); outcome = SearchOutcome.FAILURE; // current <- MAKE-NODE(problem.INITIAL-STATE) Node current = nodeExpander.createRootNode(p.getInitialState()); Node neighbor; // loop do while (!CancelableThread.currIsCanceled()) { lastState = current.getState(); metrics.set(METRIC_NODE_VALUE, getValue(current)); List<Node> children = nodeExpander.expand(current, p); // neighbor <- a highest-valued successor of current neighbor = getHighestValuedNodeFrom(children); // if neighbor.VALUE <= current.VALUE then return current.STATE if ((neighbor == null) || (getValue(neighbor) <= getValue(current))) { if (SearchUtils.isGoalState(p, current)) outcome = SearchOutcome.SOLUTION_FOUND; return current; } // current <- neighbor current = neighbor; } return null; } /** * Returns SOLUTION_FOUND if the local maximum is a goal state, or FAILURE * if the local maximum is not a goal state. * * @return SOLUTION_FOUND if the local maximum is a goal state, or FAILURE * if the local maximum is not a goal state. */ public SearchOutcome getOutcome() { return outcome; } /** * Returns the last state from which the hill climbing search found the * local maximum. * * @return the last state from which the hill climbing search found the * local maximum. */ public Object getLastSearchState() { return lastState; } /** * Returns all the search metrics. */ public Metrics getMetrics() { return metrics; } /** * Sets all metrics to zero. */ private void clearInstrumentation() { metrics.set(METRIC_NODES_EXPANDED, 0); metrics.set(METRIC_NODE_VALUE, 0); } @Override public void addNodeListener(Consumer<Node> listener) { nodeExpander.addNodeListener(listener); } @Override public boolean removeNodeListener(Consumer<Node> listener) { return nodeExpander.removeNodeListener(listener); } // // PRIVATE METHODS // private Node getHighestValuedNodeFrom(List<Node> children) { double highestValue = Double.NEGATIVE_INFINITY; Node nodeWithHighestValue = null; for (Node child : children) { double value = getValue(child); if (value > highestValue) { highestValue = value; nodeWithHighestValue = child; } } return nodeWithHighestValue; } private double getValue(Node n) { // assumption greater heuristic value => // HIGHER on hill; 0 == goal state; return -1 * hf.h(n.getState()); } }