mirror of
https://github.com/TheAlgorithms/Python.git
synced 2024-12-05 02:40:16 +00:00
516a3028d1
* Enable ruff PLR5501 rule * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
197 lines
6.6 KiB
Python
197 lines
6.6 KiB
Python
# https://en.wikipedia.org/wiki/Hill_climbing
|
|
import math
|
|
|
|
|
|
class SearchProblem:
|
|
"""
|
|
An interface to define search problems.
|
|
The interface will be illustrated using the example of mathematical function.
|
|
"""
|
|
|
|
def __init__(self, x: int, y: int, step_size: int, function_to_optimize):
|
|
"""
|
|
The constructor of the search problem.
|
|
|
|
x: the x coordinate of the current search state.
|
|
y: the y coordinate of the current search state.
|
|
step_size: size of the step to take when looking for neighbors.
|
|
function_to_optimize: a function to optimize having the signature f(x, y).
|
|
"""
|
|
self.x = x
|
|
self.y = y
|
|
self.step_size = step_size
|
|
self.function = function_to_optimize
|
|
|
|
def score(self) -> int:
|
|
"""
|
|
Returns the output of the function called with current x and y coordinates.
|
|
>>> def test_function(x, y):
|
|
... return x + y
|
|
>>> SearchProblem(0, 0, 1, test_function).score() # 0 + 0 = 0
|
|
0
|
|
>>> SearchProblem(5, 7, 1, test_function).score() # 5 + 7 = 12
|
|
12
|
|
"""
|
|
return self.function(self.x, self.y)
|
|
|
|
def get_neighbors(self):
|
|
"""
|
|
Returns a list of coordinates of neighbors adjacent to the current coordinates.
|
|
|
|
Neighbors:
|
|
| 0 | 1 | 2 |
|
|
| 3 | _ | 4 |
|
|
| 5 | 6 | 7 |
|
|
"""
|
|
step_size = self.step_size
|
|
return [
|
|
SearchProblem(x, y, step_size, self.function)
|
|
for x, y in (
|
|
(self.x - step_size, self.y - step_size),
|
|
(self.x - step_size, self.y),
|
|
(self.x - step_size, self.y + step_size),
|
|
(self.x, self.y - step_size),
|
|
(self.x, self.y + step_size),
|
|
(self.x + step_size, self.y - step_size),
|
|
(self.x + step_size, self.y),
|
|
(self.x + step_size, self.y + step_size),
|
|
)
|
|
]
|
|
|
|
def __hash__(self):
|
|
"""
|
|
hash the string representation of the current search state.
|
|
"""
|
|
return hash(str(self))
|
|
|
|
def __eq__(self, obj):
|
|
"""
|
|
Check if the 2 objects are equal.
|
|
"""
|
|
if isinstance(obj, SearchProblem):
|
|
return hash(str(self)) == hash(str(obj))
|
|
return False
|
|
|
|
def __str__(self):
|
|
"""
|
|
string representation of the current search state.
|
|
>>> str(SearchProblem(0, 0, 1, None))
|
|
'x: 0 y: 0'
|
|
>>> str(SearchProblem(2, 5, 1, None))
|
|
'x: 2 y: 5'
|
|
"""
|
|
return f"x: {self.x} y: {self.y}"
|
|
|
|
|
|
def hill_climbing(
|
|
search_prob,
|
|
find_max: bool = True,
|
|
max_x: float = math.inf,
|
|
min_x: float = -math.inf,
|
|
max_y: float = math.inf,
|
|
min_y: float = -math.inf,
|
|
visualization: bool = False,
|
|
max_iter: int = 10000,
|
|
) -> SearchProblem:
|
|
"""
|
|
Implementation of the hill climbling algorithm.
|
|
We start with a given state, find all its neighbors,
|
|
move towards the neighbor which provides the maximum (or minimum) change.
|
|
We keep doing this until we are at a state where we do not have any
|
|
neighbors which can improve the solution.
|
|
Args:
|
|
search_prob: The search state at the start.
|
|
find_max: If True, the algorithm should find the maximum else the minimum.
|
|
max_x, min_x, max_y, min_y: the maximum and minimum bounds of x and y.
|
|
visualization: If True, a matplotlib graph is displayed.
|
|
max_iter: number of times to run the iteration.
|
|
Returns a search state having the maximum (or minimum) score.
|
|
"""
|
|
current_state = search_prob
|
|
scores = [] # list to store the current score at each iteration
|
|
iterations = 0
|
|
solution_found = False
|
|
visited = set()
|
|
while not solution_found and iterations < max_iter:
|
|
visited.add(current_state)
|
|
iterations += 1
|
|
current_score = current_state.score()
|
|
scores.append(current_score)
|
|
neighbors = current_state.get_neighbors()
|
|
max_change = -math.inf
|
|
min_change = math.inf
|
|
next_state = None # to hold the next best neighbor
|
|
for neighbor in neighbors:
|
|
if neighbor in visited:
|
|
continue # do not want to visit the same state again
|
|
if (
|
|
neighbor.x > max_x
|
|
or neighbor.x < min_x
|
|
or neighbor.y > max_y
|
|
or neighbor.y < min_y
|
|
):
|
|
continue # neighbor outside our bounds
|
|
change = neighbor.score() - current_score
|
|
if find_max: # finding max
|
|
# going to direction with greatest ascent
|
|
if change > max_change and change > 0:
|
|
max_change = change
|
|
next_state = neighbor
|
|
elif change < min_change and change < 0: # finding min
|
|
# to direction with greatest descent
|
|
min_change = change
|
|
next_state = neighbor
|
|
if next_state is not None:
|
|
# we found at least one neighbor which improved the current state
|
|
current_state = next_state
|
|
else:
|
|
# since we have no neighbor that improves the solution we stop the search
|
|
solution_found = True
|
|
|
|
if visualization:
|
|
from matplotlib import pyplot as plt
|
|
|
|
plt.plot(range(iterations), scores)
|
|
plt.xlabel("Iterations")
|
|
plt.ylabel("Function values")
|
|
plt.show()
|
|
|
|
return current_state
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import doctest
|
|
|
|
doctest.testmod()
|
|
|
|
def test_f1(x, y):
|
|
return (x**2) + (y**2)
|
|
|
|
# starting the problem with initial coordinates (3, 4)
|
|
prob = SearchProblem(x=3, y=4, step_size=1, function_to_optimize=test_f1)
|
|
local_min = hill_climbing(prob, find_max=False)
|
|
print(
|
|
"The minimum score for f(x, y) = x^2 + y^2 found via hill climbing: "
|
|
f"{local_min.score()}"
|
|
)
|
|
|
|
# starting the problem with initial coordinates (12, 47)
|
|
prob = SearchProblem(x=12, y=47, step_size=1, function_to_optimize=test_f1)
|
|
local_min = hill_climbing(
|
|
prob, find_max=False, max_x=100, min_x=5, max_y=50, min_y=-5, visualization=True
|
|
)
|
|
print(
|
|
"The minimum score for f(x, y) = x^2 + y^2 with the domain 100 > x > 5 "
|
|
f"and 50 > y > - 5 found via hill climbing: {local_min.score()}"
|
|
)
|
|
|
|
def test_f2(x, y):
|
|
return (3 * x**2) - (6 * y)
|
|
|
|
prob = SearchProblem(x=3, y=4, step_size=1, function_to_optimize=test_f1)
|
|
local_min = hill_climbing(prob, find_max=True)
|
|
print(
|
|
"The maximum score for f(x, y) = x^2 + y^2 found via hill climbing: "
|
|
f"{local_min.score()}"
|
|
)
|