mirror of
https://github.com/TheAlgorithms/Python.git
synced 2025-02-20 00:02:04 +00:00
Compare commits
15 Commits
26ecb64d7a
...
57c96baa93
Author | SHA1 | Date | |
---|---|---|---|
|
57c96baa93 | ||
|
6c92c5a539 | ||
|
13e4d3e76c | ||
|
c666db3729 | ||
|
9fb51b4169 | ||
|
1f74db0c06 | ||
|
064704be46 | ||
|
20184aa433 | ||
|
cdb28e53e5 | ||
|
9049228ff8 | ||
|
84b29c0eed | ||
|
dbd29aed76 | ||
|
d62f39f647 | ||
|
c9c9639803 | ||
|
39be73f0b5 |
|
@ -16,13 +16,13 @@ repos:
|
|||
- id: auto-walrus
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.1
|
||||
rev: v0.9.3
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.3.0
|
||||
rev: v2.4.0
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies:
|
||||
|
|
|
@ -88,7 +88,7 @@ def __prepare(
|
|||
...
|
||||
KeyError: 'Length of alphabet has to be 27.'
|
||||
|
||||
Testing with punctuations that are not in the given alphabet
|
||||
Testing with punctuation not in the given alphabet
|
||||
|
||||
>>> __prepare('am i a boy?','abCdeFghijkLmnopqrStuVwxYZ+')
|
||||
Traceback (most recent call last):
|
||||
|
@ -128,7 +128,7 @@ def encrypt_message(
|
|||
encrypt_message
|
||||
===============
|
||||
|
||||
Encrypts a message using the trifid_cipher. Any punctuatuions that
|
||||
Encrypts a message using the trifid_cipher. Any punctuatuion chars that
|
||||
would be used should be added to the alphabet.
|
||||
|
||||
PARAMETERS
|
||||
|
|
|
@ -9,7 +9,9 @@ import time
|
|||
|
||||
|
||||
def cross(items_a, items_b):
|
||||
"Cross product of elements in A and elements in B."
|
||||
"""
|
||||
Cross product of elements in A and elements in B.
|
||||
"""
|
||||
return [a + b for a in items_a for b in items_b]
|
||||
|
||||
|
||||
|
@ -27,7 +29,7 @@ peers = {s: {x for u in units[s] for x in u} - {s} for s in squares}
|
|||
|
||||
|
||||
def test():
|
||||
"A set of unit tests."
|
||||
"""A set of unit tests."""
|
||||
assert len(squares) == 81
|
||||
assert len(unitlist) == 27
|
||||
assert all(len(units[s]) == 3 for s in squares)
|
||||
|
@ -47,8 +49,10 @@ def test():
|
|||
|
||||
|
||||
def parse_grid(grid):
|
||||
"""Convert grid to a dict of possible values, {square: digits}, or
|
||||
return False if a contradiction is detected."""
|
||||
"""
|
||||
Convert grid to a dict of possible values, {square: digits}, or
|
||||
return False if a contradiction is detected.
|
||||
"""
|
||||
## To start, every square can be any digit; then assign values from the grid.
|
||||
values = {s: digits for s in squares}
|
||||
for s, d in grid_values(grid).items():
|
||||
|
@ -58,15 +62,19 @@ def parse_grid(grid):
|
|||
|
||||
|
||||
def grid_values(grid):
|
||||
"Convert grid into a dict of {square: char} with '0' or '.' for empties."
|
||||
"""
|
||||
Convert grid into a dict of {square: char} with '0' or '.' for empties.
|
||||
"""
|
||||
chars = [c for c in grid if c in digits or c in "0."]
|
||||
assert len(chars) == 81
|
||||
return dict(zip(squares, chars))
|
||||
|
||||
|
||||
def assign(values, s, d):
|
||||
"""Eliminate all the other values (except d) from values[s] and propagate.
|
||||
Return values, except return False if a contradiction is detected."""
|
||||
"""
|
||||
Eliminate all the other values (except d) from values[s] and propagate.
|
||||
Return values, except return False if a contradiction is detected.
|
||||
"""
|
||||
other_values = values[s].replace(d, "")
|
||||
if all(eliminate(values, s, d2) for d2 in other_values):
|
||||
return values
|
||||
|
@ -75,8 +83,10 @@ def assign(values, s, d):
|
|||
|
||||
|
||||
def eliminate(values, s, d):
|
||||
"""Eliminate d from values[s]; propagate when values or places <= 2.
|
||||
Return values, except return False if a contradiction is detected."""
|
||||
"""
|
||||
Eliminate d from values[s]; propagate when values or places <= 2.
|
||||
Return values, except return False if a contradiction is detected.
|
||||
"""
|
||||
if d not in values[s]:
|
||||
return values ## Already eliminated
|
||||
values[s] = values[s].replace(d, "")
|
||||
|
@ -99,7 +109,9 @@ def eliminate(values, s, d):
|
|||
|
||||
|
||||
def display(values):
|
||||
"Display these values as a 2-D grid."
|
||||
"""
|
||||
Display these values as a 2-D grid.
|
||||
"""
|
||||
width = 1 + max(len(values[s]) for s in squares)
|
||||
line = "+".join(["-" * (width * 3)] * 3)
|
||||
for r in rows:
|
||||
|
@ -114,11 +126,14 @@ def display(values):
|
|||
|
||||
|
||||
def solve(grid):
|
||||
"""
|
||||
Solve the grid.
|
||||
"""
|
||||
return search(parse_grid(grid))
|
||||
|
||||
|
||||
def some(seq):
|
||||
"Return some element of seq that is true."
|
||||
"""Return some element of seq that is true."""
|
||||
for e in seq:
|
||||
if e:
|
||||
return e
|
||||
|
@ -126,7 +141,9 @@ def some(seq):
|
|||
|
||||
|
||||
def search(values):
|
||||
"Using depth-first search and propagation, try all possible values."
|
||||
"""
|
||||
Using depth-first search and propagation, try all possible values.
|
||||
"""
|
||||
if values is False:
|
||||
return False ## Failed earlier
|
||||
if all(len(values[s]) == 1 for s in squares):
|
||||
|
@ -137,9 +154,11 @@ def search(values):
|
|||
|
||||
|
||||
def solve_all(grids, name="", showif=0.0):
|
||||
"""Attempt to solve a sequence of grids. Report results.
|
||||
"""
|
||||
Attempt to solve a sequence of grids. Report results.
|
||||
When showif is a number of seconds, display puzzles that take longer.
|
||||
When showif is None, don't display any puzzles."""
|
||||
When showif is None, don't display any puzzles.
|
||||
"""
|
||||
|
||||
def time_solve(grid):
|
||||
start = time.monotonic()
|
||||
|
@ -162,7 +181,9 @@ def solve_all(grids, name="", showif=0.0):
|
|||
|
||||
|
||||
def solved(values):
|
||||
"A puzzle is solved if each unit is a permutation of the digits 1 to 9."
|
||||
"""
|
||||
A puzzle is solved if each unit is a permutation of the digits 1 to 9.
|
||||
"""
|
||||
|
||||
def unitsolved(unit):
|
||||
return {values[s] for s in unit} == set(digits)
|
||||
|
@ -177,9 +198,11 @@ def from_file(filename, sep="\n"):
|
|||
|
||||
|
||||
def random_puzzle(assignments=17):
|
||||
"""Make a random puzzle with N or more assignments. Restart on contradictions.
|
||||
"""
|
||||
Make a random puzzle with N or more assignments. Restart on contradictions.
|
||||
Note the resulting puzzle is not guaranteed to be solvable, but empirically
|
||||
about 99.8% of them are solvable. Some have multiple solutions."""
|
||||
about 99.8% of them are solvable. Some have multiple solutions.
|
||||
"""
|
||||
values = {s: digits for s in squares}
|
||||
for s in shuffled(squares):
|
||||
if not assign(values, s, random.choice(values[s])):
|
||||
|
@ -191,7 +214,9 @@ def random_puzzle(assignments=17):
|
|||
|
||||
|
||||
def shuffled(seq):
|
||||
"Return a randomly shuffled copy of the input sequence."
|
||||
"""
|
||||
Return a randomly shuffled copy of the input sequence.
|
||||
"""
|
||||
seq = list(seq)
|
||||
random.shuffle(seq)
|
||||
return seq
|
||||
|
|
|
@ -221,6 +221,10 @@ def del_node(root: MyNode, data: Any) -> MyNode | None:
|
|||
else:
|
||||
root.set_right(del_node(right_child, data))
|
||||
|
||||
# Re-fetch left_child and right_child references
|
||||
left_child = root.get_left()
|
||||
right_child = root.get_right()
|
||||
|
||||
if get_height(right_child) - get_height(left_child) == 2:
|
||||
assert right_child is not None
|
||||
if get_height(right_child.get_right()) > get_height(right_child.get_left()):
|
||||
|
|
328
genetic_algorithm/genetic_algorithm_optimization.py
Normal file
328
genetic_algorithm/genetic_algorithm_optimization.py
Normal file
|
@ -0,0 +1,328 @@
|
|||
import random
|
||||
from collections.abc import Callable, Sequence
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import numpy as np
|
||||
|
||||
# Parameters
|
||||
N_POPULATION = 100 # Population size
|
||||
N_GENERATIONS = 500 # Maximum number of generations
|
||||
N_SELECTED = 50 # Number of parents selected for the next generation
|
||||
MUTATION_PROBABILITY = 0.1 # Mutation probability
|
||||
CROSSOVER_RATE = 0.8 # Probability of crossover
|
||||
SEARCH_SPACE = (-10, 10) # Search space for the variables
|
||||
|
||||
# Random number generator
|
||||
rng = np.random.default_rng()
|
||||
|
||||
|
||||
class GeneticAlgorithm:
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[float, float], float],
|
||||
bounds: Sequence[tuple[int | float, int | float]],
|
||||
population_size: int,
|
||||
generations: int,
|
||||
mutation_prob: float,
|
||||
crossover_rate: float,
|
||||
maximize: bool = True,
|
||||
) -> None:
|
||||
self.function = function # Target function to optimize
|
||||
self.bounds = bounds # Search space bounds (for each variable)
|
||||
self.population_size = population_size
|
||||
self.generations = generations
|
||||
self.mutation_prob = mutation_prob
|
||||
self.crossover_rate = crossover_rate
|
||||
self.maximize = maximize
|
||||
self.dim = len(bounds) # Dimensionality of the function (number of variables)
|
||||
|
||||
# Initialize population
|
||||
self.population = self.initialize_population()
|
||||
|
||||
def initialize_population(self) -> list[np.ndarray]:
|
||||
"""
|
||||
Initialize the population with random individuals within the search space.
|
||||
|
||||
Example:
|
||||
>>> ga = GeneticAlgorithm(
|
||||
... function=lambda x, y: x**2 + y**2,
|
||||
... bounds=[(-10, 10), (-10, 10)],
|
||||
... population_size=5,
|
||||
... generations=10,
|
||||
... mutation_prob=0.1,
|
||||
... crossover_rate=0.8,
|
||||
... maximize=False
|
||||
... )
|
||||
>>> len(ga.initialize_population())
|
||||
5 # The population size should be equal to 5.
|
||||
>>> all(len(ind) == 2 for ind in ga.initialize_population())
|
||||
# Each individual should have 2 variables
|
||||
True
|
||||
"""
|
||||
return [
|
||||
np.array([rng.uniform(b[0], b[1]) for b in self.bounds])
|
||||
for _ in range(self.population_size)
|
||||
]
|
||||
|
||||
def fitness(self, individual: np.ndarray) -> float:
|
||||
"""
|
||||
Calculate the fitness value (function value) for an individual.
|
||||
|
||||
Example:
|
||||
>>> ga = GeneticAlgorithm(
|
||||
... function=lambda x, y: x**2 + y**2,
|
||||
... bounds=[(-10, 10), (-10, 10)],
|
||||
... population_size=10,
|
||||
... generations=10,
|
||||
... mutation_prob=0.1,
|
||||
... crossover_rate=0.8,
|
||||
... maximize=False
|
||||
... )
|
||||
>>> individual = np.array([1.0, 2.0])
|
||||
>>> ga.fitness(individual)
|
||||
-5.0 # The fitness should be -1^2 + 2^2 = 5 for minimizing
|
||||
>>> ga.maximize = True
|
||||
>>> ga.fitness(individual)
|
||||
5.0 # The fitness should be 1^2 + 2^2 = 5 when maximizing
|
||||
"""
|
||||
value = float(self.function(*individual)) # Ensure fitness is a float
|
||||
return value if self.maximize else -value # If minimizing, invert the fitness
|
||||
|
||||
def select_parents(
|
||||
self, population_score: list[tuple[np.ndarray, float]]
|
||||
) -> list[np.ndarray]:
|
||||
"""
|
||||
Select top N_SELECTED parents based on fitness.
|
||||
|
||||
Example:
|
||||
>>> ga = GeneticAlgorithm(
|
||||
... function=lambda x, y: x**2 + y**2,
|
||||
... bounds=[(-10, 10), (-10, 10)],
|
||||
... population_size=10,
|
||||
... generations=10,
|
||||
... mutation_prob=0.1,
|
||||
... crossover_rate=0.8,
|
||||
... maximize=False
|
||||
... )
|
||||
>>> population_score = [
|
||||
... (np.array([1.0, 2.0]), 5.0),
|
||||
... (np.array([-1.0, -2.0]), 5.0),
|
||||
... (np.array([0.0, 0.0]), 0.0),
|
||||
... ]
|
||||
>>> selected_parents = ga.select_parents(population_score)
|
||||
>>> len(selected_parents)
|
||||
2 # Should select the two parents with the best fitness scores.
|
||||
>>> np.array_equal(selected_parents[0], np.array([1.0, 2.0]))
|
||||
True # Parent 1 should be [1.0, 2.0]
|
||||
>>> np.array_equal(selected_parents[1], np.array([-1.0, -2.0]))
|
||||
True # Parent 2 should be [-1.0, -2.0]
|
||||
|
||||
>>> population_score = [
|
||||
... (np.array([1.0, 2.0]), 5.0),
|
||||
... (np.array([1.0, -2.0]), 5.0),
|
||||
... (np.array([0.0, 0.0]), 0.0),
|
||||
... (np.array([-1.0, 2.0]), 5.0),
|
||||
... (np.array([-1.0, -2.0]), 5.0)
|
||||
... ]
|
||||
>>> selected_parents = ga.select_parents(population_score)
|
||||
>>> len(selected_parents)
|
||||
5 # Should select the top 5 parents with the best fitness scores.
|
||||
>>> np.array_equal(selected_parents[0], np.array([1.0, 2.0]))
|
||||
True # Parent 1 should be [1.0, 2.0]
|
||||
"""
|
||||
|
||||
if not population_score:
|
||||
raise ValueError("Population score is empty, cannot select parents.")
|
||||
|
||||
population_score.sort(key=lambda score_tuple: score_tuple[1], reverse=True)
|
||||
selected_count = min(N_SELECTED, len(population_score))
|
||||
return [ind for ind, _ in population_score[:selected_count]]
|
||||
|
||||
def crossover(
|
||||
self, parent1: np.ndarray, parent2: np.ndarray
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Perform uniform crossover between two parents to generate offspring.
|
||||
|
||||
Args:
|
||||
parent1 (np.ndarray): The first parent.
|
||||
parent2 (np.ndarray): The second parent.
|
||||
Returns:
|
||||
tuple[np.ndarray, np.ndarray]: The two offspring generated by crossover.
|
||||
|
||||
Example:
|
||||
>>> ga = GeneticAlgorithm(
|
||||
... lambda x, y: -(x**2 + y**2),
|
||||
... [(-10, 10), (-10, 10)],
|
||||
... 10, 100, 0.1, 0.8, True
|
||||
... )
|
||||
>>> parent1, parent2 = np.array([1, 2]), np.array([3, 4])
|
||||
>>> len(ga.crossover(parent1, parent2)) == 2
|
||||
True
|
||||
"""
|
||||
if random.random() < self.crossover_rate:
|
||||
cross_point = random.randint(1, self.dim - 1)
|
||||
child1 = np.concatenate((parent1[:cross_point], parent2[cross_point:]))
|
||||
child2 = np.concatenate((parent2[:cross_point], parent1[cross_point:]))
|
||||
return child1, child2
|
||||
return parent1, parent2
|
||||
|
||||
def mutate(self, individual: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Apply mutation to an individual.
|
||||
|
||||
Args:
|
||||
individual (np.ndarray): The individual to mutate.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The mutated individual.
|
||||
|
||||
Example:
|
||||
>>> ga = GeneticAlgorithm(
|
||||
... lambda x, y: -(x**2 + y**2),
|
||||
... [(-10, 10), (-10, 10)],
|
||||
... 10, 100, 0.1, 0.8, True
|
||||
... )
|
||||
>>> ind = np.array([1.0, 2.0])
|
||||
>>> mutated = ga.mutate(ind)
|
||||
>>> len(mutated) == 2 # Ensure it still has the correct number of dimensions
|
||||
True
|
||||
"""
|
||||
for i in range(self.dim):
|
||||
if random.random() < self.mutation_prob:
|
||||
individual[i] = rng.uniform(self.bounds[i][0], self.bounds[i][1])
|
||||
return individual
|
||||
|
||||
def evaluate_population(self) -> list[tuple[np.ndarray, float]]:
|
||||
"""
|
||||
Evaluate the fitness of the entire population in parallel.
|
||||
|
||||
Returns:
|
||||
list[tuple[np.ndarray, float]]:
|
||||
The population with their respective fitness values.
|
||||
|
||||
Example:
|
||||
>>> ga = GeneticAlgorithm(
|
||||
... lambda x, y: -(x**2 + y**2),
|
||||
... [(-10, 10), (-10, 10)],
|
||||
... 10, 100, 0.1, 0.8, True
|
||||
... )
|
||||
>>> eval_population = ga.evaluate_population()
|
||||
>>> len(eval_population) == ga.population_size # Ensure population size
|
||||
True
|
||||
>>> all(
|
||||
... isinstance(ind, tuple) and isinstance(ind[1], float)
|
||||
... for ind in eval_population
|
||||
... )
|
||||
True
|
||||
"""
|
||||
with ThreadPoolExecutor() as executor:
|
||||
return list(
|
||||
executor.map(
|
||||
lambda individual: (individual, self.fitness(individual)),
|
||||
self.population,
|
||||
)
|
||||
)
|
||||
|
||||
def evolve(self, verbose: bool = True) -> np.ndarray:
|
||||
"""
|
||||
Evolve the population over the generations to find the best solution.
|
||||
|
||||
Args:
|
||||
verbose (bool): If True, prints the progress of the generations.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The best individual found during the evolution process.
|
||||
|
||||
Example:
|
||||
>>> ga = GeneticAlgorithm(
|
||||
... function=lambda x, y: x**2 + y**2,
|
||||
... bounds=[(-10, 10), (-10, 10)],
|
||||
... population_size=10,
|
||||
... generations=10,
|
||||
... mutation_prob=0.1,
|
||||
... crossover_rate=0.8,
|
||||
... maximize=False
|
||||
... )
|
||||
>>> best_solution = ga.evolve(verbose=False)
|
||||
>>> len(best_solution)
|
||||
2 # The best solution should be a 2-element array (var_x, var_y)
|
||||
>>> isinstance(best_solution[0], float) # First element should be a float
|
||||
True
|
||||
>>> isinstance(best_solution[1], float) # Second element should be a float
|
||||
True
|
||||
"""
|
||||
best_individual = None
|
||||
for generation in range(self.generations):
|
||||
# Evaluate population fitness (multithreaded)
|
||||
population_score = self.evaluate_population()
|
||||
|
||||
# Ensure population_score isn't empty
|
||||
if not population_score:
|
||||
raise ValueError("Population score is empty. No individuals evaluated.")
|
||||
|
||||
# Check the best individual
|
||||
best_individual = max(
|
||||
population_score, key=lambda score_tuple: score_tuple[1]
|
||||
)[0]
|
||||
best_fitness = self.fitness(best_individual)
|
||||
|
||||
# Select parents for next generation
|
||||
parents = self.select_parents(population_score)
|
||||
next_generation = []
|
||||
|
||||
# Generate offspring using crossover and mutation
|
||||
for i in range(0, len(parents), 2):
|
||||
parent1, parent2 = (
|
||||
parents[i],
|
||||
parents[(i + 1) % len(parents)],
|
||||
) # Wrap around for odd cases
|
||||
child1, child2 = self.crossover(parent1, parent2)
|
||||
next_generation.append(self.mutate(child1))
|
||||
next_generation.append(self.mutate(child2))
|
||||
|
||||
# Ensure population size remains the same
|
||||
self.population = next_generation[: self.population_size]
|
||||
|
||||
if verbose and generation % 10 == 0:
|
||||
print(f"Generation {generation}: Best Fitness = {best_fitness}")
|
||||
|
||||
return best_individual
|
||||
|
||||
|
||||
# Example target function for optimization
|
||||
def target_function(var_x: float, var_y: float) -> float:
|
||||
"""
|
||||
Example target function (parabola) for optimization.
|
||||
Args:
|
||||
var_x (float): The x-coordinate.
|
||||
var_y (float): The y-coordinate.
|
||||
Returns:
|
||||
float: The value of the function at (var_x, var_y).
|
||||
|
||||
Example:
|
||||
>>> target_function(0, 0)
|
||||
0
|
||||
>>> target_function(1, 1)
|
||||
2
|
||||
"""
|
||||
return var_x**2 + var_y**2 # Simple parabolic surface (minimization)
|
||||
|
||||
|
||||
# Set bounds for the variables (var_x, var_y)
|
||||
bounds = [(-10, 10), (-10, 10)] # Both var_x and var_y range from -10 to 10
|
||||
|
||||
# Instantiate and run the genetic algorithm
|
||||
ga = GeneticAlgorithm(
|
||||
function=target_function,
|
||||
bounds=bounds,
|
||||
population_size=N_POPULATION,
|
||||
generations=N_GENERATIONS,
|
||||
mutation_prob=MUTATION_PROBABILITY,
|
||||
crossover_rate=CROSSOVER_RATE,
|
||||
maximize=False, # Minimize the function
|
||||
)
|
||||
|
||||
best_solution = ga.evolve()
|
||||
print(f"Best solution found: {best_solution}")
|
||||
print(f"Best fitness (minimum value of function): {target_function(*best_solution)}")
|
|
@ -159,7 +159,7 @@ lint.pylint.max-returns = 8 # default: 6
|
|||
lint.pylint.max-statements = 88 # default: 50
|
||||
|
||||
[tool.codespell]
|
||||
ignore-words-list = "3rt,ans,bitap,crate,damon,fo,followings,hist,iff,kwanza,manuel,mater,secant,som,sur,tim,toi,zar"
|
||||
ignore-words-list = "3rt,abd,aer,ans,bitap,crate,damon,fo,followings,hist,iff,kwanza,manuel,mater,secant,som,sur,tim,toi,zar"
|
||||
skip = "./.*,*.json,*.lock,ciphers/prehistoric_men.txt,project_euler/problem_022/p022_names.txt,pyproject.toml,strings/dictionary.txt,strings/words.txt"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
|
|
|
@ -85,6 +85,8 @@ def bubble_sort_recursive(collection: list[Any]) -> list[Any]:
|
|||
[1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7]
|
||||
>>> bubble_sort_recursive([1, 3.3, 5, 7.7, 2, 4.4, 6])
|
||||
[1, 2, 3.3, 4.4, 5, 6, 7.7]
|
||||
>>> bubble_sort_recursive(['a', 'Z', 'B', 'C', 'A', 'c'])
|
||||
['A', 'B', 'C', 'Z', 'a', 'c']
|
||||
>>> import random
|
||||
>>> collection_arg = random.sample(range(-50, 50), 100)
|
||||
>>> bubble_sort_recursive(collection_arg) == sorted(collection_arg)
|
||||
|
|
Loading…
Reference in New Issue
Block a user