Перейти к основному содержимому

Пример алгоритма восхождения на холм в Java

· 7 мин. чтения

1. Обзор

В этом уроке мы покажем алгоритм Hill-Climbing и его реализацию. Мы также рассмотрим его преимущества и недостатки. Прежде чем перейти непосредственно к этому, давайте кратко обсудим алгоритмы генерации и тестирования.

2. Алгоритм создания и тестирования

Это очень простая техника, которая позволяет нам алгоритмизировать поиск решений:

  1. Определить текущее состояние как начальное состояние
  2. Примените любую возможную операцию к текущему состоянию и сгенерируйте возможное решение
  3. Сравните вновь созданное решение с целевым состоянием
  4. Если цель достигнута или новые состояния не могут быть созданы, выйдите. В противном случае вернитесь к шагу 2

Очень хорошо работает с простыми задачами. Поскольку это исчерпывающий поиск, его невозможно рассматривать при работе с большими проблемными пространствами. Он также известен как алгоритм Британского музея (попытка найти артефакт в Британском музее, исследуя его случайным образом).

Это также основная идея Hill-Climbing Attack в мире биометрии. Этот подход может быть использован для генерации синтетических биометрических данных.

3. Введение в простой алгоритм восхождения на холм

В технике альпинизма, начиная с подножия холма, мы идем вверх, пока не достигнем вершины холма. Другими словами, мы начинаем с начального состояния и продолжаем улучшать решение до тех пор, пока оно не станет оптимальным.

Это вариант алгоритма генерации и тестирования, который отбрасывает все состояния, которые не выглядят многообещающими или вряд ли приведут нас к целевому состоянию. Для принятия таких решений он использует эвристику (функцию оценки), которая показывает, насколько близко текущее состояние к целевому состоянию.

Проще говоря, Hill-Climbing = генерация-и-тестирование + эвристика.

Давайте посмотрим на алгоритм восхождения на простой холм:

  1. Определите текущее состояние как начальное состояние

  2. Цикл до тех пор, пока не будет достигнуто целевое состояние или к текущему состоянию нельзя будет применить больше операторов:

  3. Применить операцию к текущему состоянию и получить новое состояние

  4. Сравните новое состояние с целью

  5. Выйти , если целевое состояние достигнуто

  6. Оцените новое состояние с помощью эвристической функции и сравните его с текущим состоянием.

  7. Если новое состояние ближе к цели по сравнению с текущим состоянием, обновите текущее состояние.

Как мы видим, он достигает целевого состояния с итеративными улучшениями. В алгоритме Hill-Climbing поиск цели эквивалентен достижению вершины холма.

4. Пример

Алгоритм восхождения на холм можно отнести к категории информированного поиска. Таким образом, мы можем реализовать любой поиск на основе узлов или задачи, такие как проблема n-королев, используя его. Чтобы легко понять концепцию, мы возьмем очень простой пример.

Давайте посмотрим на изображение ниже:

./d8aea58eb17a7b8bdcaebb64eb4faec8.png

Ключевым моментом при решении любой задачи о восхождении на холм является выбор подходящей эвристической функции .

Определим такую функцию h:

h(x) = +1 для всех блоков опорной конструкции, если блок расположен правильно, в противном случае -1 для всех блоков опорной конструкции.

Здесь мы будем называть любой блок правильно расположенным, если он имеет ту же структуру поддержки, что и целевое состояние. В соответствии с описанной ранее процедурой восхождения на холм давайте рассмотрим все итерации и их эвристику для достижения целевого состояния:

./01331c379e918b2ce0758581bb92cfd0.png

5. Реализация

Теперь давайте реализуем тот же пример, используя алгоритм Hill-Climbing.

Прежде всего, нам нужен класс State , в котором будет храниться список стеков, представляющих позиции блоков в каждом состоянии. Он также будет хранить эвристики для этого конкретного состояния:

public class State {
private List<Stack<String>> state;
private int heuristics;

// copy constructor, setters, and getters
}

Нам также нужен метод, который будет вычислять эвристическое значение состояния.

public int getHeuristicsValue(
List<Stack<String>> currentState, Stack<String> goalStateStack) {

Integer heuristicValue;
heuristicValue = currentState.stream()
.mapToInt(stack -> {
return getHeuristicsValueForStack(
stack, currentState, goalStateStack);
}).sum();

return heuristicValue;
}

public int getHeuristicsValueForStack(
Stack<String> stack,
List<Stack<String>> currentState,
Stack<String> goalStateStack) {

int stackHeuristics = 0;
boolean isPositioneCorrect = true;
int goalStartIndex = 0;
for (String currentBlock : stack) {
if (isPositioneCorrect
&& currentBlock.equals(goalStateStack.get(goalStartIndex))) {
stackHeuristics += goalStartIndex;
} else {
stackHeuristics -= goalStartIndex;
isPositioneCorrect = false;
}
goalStartIndex++;
}
return stackHeuristics;
}

Кроме того, нам нужно определить операторные методы, которые дадут нам новое состояние. Для нашего примера мы определим два из этих методов:

  1. Извлеките блок из стека и поместите его в новый стек
  2. Извлеките блок из стека и поместите его в один из других стеков.
private State pushElementToNewStack(
List<Stack<String>> currentStackList,
String block,
int currentStateHeuristics,
Stack<String> goalStateStack) {

State newState = null;
Stack<String> newStack = new Stack<>();
newStack.push(block);
currentStackList.add(newStack);
int newStateHeuristics
= getHeuristicsValue(currentStackList, goalStateStack);
if (newStateHeuristics > currentStateHeuristics) {
newState = new State(currentStackList, newStateHeuristics);
} else {
currentStackList.remove(newStack);
}
return newState;
}
private State pushElementToExistingStacks(
Stack currentStack,
List<Stack<String>> currentStackList,
String block,
int currentStateHeuristics,
Stack<String> goalStateStack) {

return currentStackList.stream()
.filter(stack -> stack != currentStack)
.map(stack -> {
return pushElementToStack(
stack, block, currentStackList,
currentStateHeuristics, goalStateStack);
})
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}

private State pushElementToStack(
Stack stack,
String block,
List<Stack<String>> currentStackList,
int currentStateHeuristics,
Stack<String> goalStateStack) {

stack.push(block);
int newStateHeuristics
= getHeuristicsValue(currentStackList, goalStateStack);
if (newStateHeuristics > currentStateHeuristics) {
return new State(currentStackList, newStateHeuristics);
}
stack.pop();
return null;
}

Теперь, когда у нас есть вспомогательные методы, давайте напишем метод для реализации техники восхождения на холм.

Здесь мы продолжаем вычислять новые состояния, которые ближе к цели, чем их предшественники. Мы продолжаем добавлять их на наш путь, пока не достигнем цели.

Если мы не находим никаких новых состояний, алгоритм завершается с сообщением об ошибке:

public List<State> getRouteWithHillClimbing(
Stack<String> initStateStack, Stack<String> goalStateStack) throws Exception {
// instantiate initState with initStateStack
// ...
List<State> resultPath = new ArrayList<>();
resultPath.add(new State(initState));

State currentState = initState;
boolean noStateFound = false;

while (
!currentState.getState().get(0).equals(goalStateStack)
|| noStateFound) {
noStateFound = true;
State nextState = findNextState(currentState, goalStateStack);
if (nextState != null) {
noStateFound = false;
currentState = nextState;
resultPath.add(new State(nextState));
}
}
return resultPath;
}

В дополнение к этому нам также нужен метод findNextState , который применяет все возможные операции к текущему состоянию, чтобы получить следующее состояние:

public State findNextState(State currentState, Stack<String> goalStateStack) {
List<Stack<String>> listOfStacks = currentState.getState();
int currentStateHeuristics = currentState.getHeuristics();

return listOfStacks.stream()
.map(stack -> {
return applyOperationsOnState(
listOfStacks, stack, currentStateHeuristics, goalStateStack);
})
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}

public State applyOperationsOnState(
List<Stack<String>> listOfStacks,
Stack<String> stack,
int currentStateHeuristics,
Stack<String> goalStateStack) {

State tempState;
List<Stack<String>> tempStackList
= new ArrayList<>(listOfStacks);
String block = stack.pop();
if (stack.size() == 0)
tempStackList.remove(stack);
tempState = pushElementToNewStack(
tempStackList, block, currentStateHeuristics, goalStateStack);
if (tempState == null) {
tempState = pushElementToExistingStacks(
stack, tempStackList, block,
currentStateHeuristics, goalStateStack);
stack.push(block);
}
return tempState;
}

6. Алгоритм подъема на холм с самым крутым подъемом

Алгоритм Steepest-Ascent Hill-Climbing (поиск градиента) является вариантом алгоритма Hill Climbing. Мы можем реализовать это с небольшими изменениями в нашем простом алгоритме. В этом алгоритме мы рассматриваем все возможные состояния из текущего состояния, а затем выбираем лучшее в качестве преемника , в отличие от простого метода восхождения на холм.

Другими словами, в случае метода восхождения на холм мы выбрали в качестве преемника любое состояние, которое было ближе к цели, чем текущее состояние, тогда как в алгоритме восхождения на вершину с самым крутым подъемом мы выбираем наилучшего преемника из всех возможных преемников, а затем обновляем текущее состояние.

7. Недостатки

Hill Climbing — недальновидный метод, поскольку он оценивает только ближайшие возможности. Таким образом, он может оказаться в нескольких ситуациях, из которых он не сможет выбрать какие-либо дальнейшие состояния. Давайте посмотрим на эти состояния и некоторые решения для них:

  1. Локальный максимум: это состояние лучше всех соседей, но существует лучшее состояние, которое далеко от текущего состояния; если локальный максимум находится в пределах видимости решения, он известен как «предгорье».
  2. Плато: в этом состоянии все соседние состояния имеют одинаковые эвристические значения, поэтому неясно выбрать следующее состояние путем локальных сравнений.
  3. Хребет: это область, которая выше окружающих штатов, но до нее нельзя добраться за один ход; например, у нас есть четыре возможных направления для исследования (N, E, W, S), и существует область в северо-восточном направлении.

Есть несколько решений для преодоления этих ситуаций:

  1. Мы можем вернуться к одному из предыдущих состояний и исследовать другие направления.
  2. Мы можем пропустить несколько состояний и сделать прыжок в новых направлениях .
  3. Мы можем исследовать несколько направлений , чтобы определить правильный путь.

8. Заключение

Несмотря на то, что метод восхождения на холм намного лучше, чем полный поиск, он все же не оптимален в больших проблемных областях.

Мы всегда можем закодировать глобальную информацию в эвристические функции для принятия более разумных решений, но тогда вычислительная сложность будет намного выше, чем была раньше.

Алгоритм восхождения на холм может быть очень полезным, если его сочетать с другими методами. Как всегда, полный код всех примеров можно найти на GitHub .