77
88# =========================== Problem setup: Knapsack ===========================
99
10- KNAPSACK_N_ITEMS = 42 # Number of items in the knapsack problem
11- KNAPSACK_VALUE_RANGE = (10 , 100 ) # Range of item values
12- KNAPSACK_WEIGHT_RANGE = (5 , 50 ) # Range of item weights
13- KNAPSACK_CAPACITY_RATIO = 0.5 # Capacity as a fraction of total weight
10+ KNAPSACK_N_ITEMS = 42 # Number of items in the knapsack problem
11+ KNAPSACK_VALUE_RANGE = (10 , 100 ) # Range of item values
12+ KNAPSACK_WEIGHT_RANGE = (5 , 50 ) # Range of item weights
13+ KNAPSACK_CAPACITY_RATIO = 0.5 # Capacity as a fraction of total weight
14+
1415
1516@dataclass
1617class Item :
1718 value : int
1819 weight : int
1920
20- def generate_knapsack_instance (n_items : int , value_range : tuple [int , int ], weight_range : tuple [int , int ], capacity_ratio = float ) -> tuple [list [Item ], int ]:
21+
22+ def generate_knapsack_instance (
23+ n_items : int ,
24+ value_range : tuple [int , int ],
25+ weight_range : tuple [int , int ],
26+ capacity_ratio = float ,
27+ ) -> tuple [list [Item ], int ]:
2128 """Generates a random knapsack problem instance."""
2229 items = []
2330 for _ in range (n_items ):
@@ -28,8 +35,13 @@ def generate_knapsack_instance(n_items: int, value_range: tuple[int, int], weigh
2835 capacity = int (sum (it .weight for it in items ) * capacity_ratio )
2936 return items , capacity
3037
31- items , capacity = generate_knapsack_instance (n_items = KNAPSACK_N_ITEMS , value_range = KNAPSACK_VALUE_RANGE , weight_range = KNAPSACK_WEIGHT_RANGE , capacity_ratio = KNAPSACK_CAPACITY_RATIO )
3238
39+ items , capacity = generate_knapsack_instance (
40+ n_items = KNAPSACK_N_ITEMS ,
41+ value_range = KNAPSACK_VALUE_RANGE ,
42+ weight_range = KNAPSACK_WEIGHT_RANGE ,
43+ capacity_ratio = KNAPSACK_CAPACITY_RATIO ,
44+ )
3345
3446
3547# ============================== GA Representation ==============================
@@ -45,7 +57,8 @@ def generate_knapsack_instance(n_items: int, value_range: tuple[int, int], weigh
4557
4658OVERWEIGHT_PENALTY_FACTOR = 10
4759
48- Genome = list [int ] # An index list where 1 means item is included, 0 means excluded
60+ Genome = list [int ] # An index list where 1 means item is included, 0 means excluded
61+
4962
5063def evaluate (genome : Genome , items : list [Item ], capacity : int ) -> tuple [int , int ]:
5164 """Evaluation function - calculates the fitness of each candidate based on total value and weight."""
@@ -57,13 +70,15 @@ def evaluate(genome: Genome, items: list[Item], capacity: int) -> tuple[int, int
5770 total_weight += item .weight
5871 if total_weight > capacity :
5972 # Penalize overweight solutions: return small value scaled by overflow
60- overflow = ( total_weight - capacity )
73+ overflow = total_weight - capacity
6174 total_value = max (0 , total_value - overflow * OVERWEIGHT_PENALTY_FACTOR )
6275 return total_value , total_weight
6376
77+
6478def random_genome (n : int ) -> Genome :
6579 """Generates a random genome of length n."""
66- return [random .randint (0 ,1 ) for _ in range (n )]
80+ return [random .randint (0 , 1 ) for _ in range (n )]
81+
6782
6883def selection (population : list [Genome ], fitnesses : list [int ], k : int ) -> Genome :
6984 """Performs tournament selection to choose genomes from the population.
@@ -73,21 +88,24 @@ def selection(population: list[Genome], fitnesses: list[int], k: int) -> Genome:
7388 get_fitness = lambda x : x [1 ]
7489 return max (contenders , key = get_fitness )[0 ][:]
7590
91+
7692def crossover (a : Genome , b : Genome , p_crossover : float ) -> tuple [Genome , Genome ]:
7793 """Performs single-point crossover between two genomes.
7894 Note that other crossover strategies exist such as two-point crossover, uniform crossover, etc."""
7995 min_length = min (len (a ), len (b ))
8096 if random .random () > p_crossover or min_length < 2 :
8197 return a [:], b [:]
8298 cutoff_point = random .randint (1 , min_length - 1 )
83- return a [:cutoff_point ]+ b [cutoff_point :], b [:cutoff_point ]+ a [cutoff_point :]
99+ return a [:cutoff_point ] + b [cutoff_point :], b [:cutoff_point ] + a [cutoff_point :]
100+
84101
85102def mutation (g : Genome , p_mutation : int ) -> Genome :
86103 """Performs bit-flip mutation on a genome.
87104 Note that other mutation strategies exist such as swap mutation, scramble mutation, etc.
88105 """
89106 return [(1 - gene ) if random .random () < p_mutation else gene for gene in g ]
90107
108+
91109def run_ga (
92110 items : list [Item ],
93111 capacity : int ,
@@ -120,8 +138,10 @@ def run_ga(
120138
121139 # Elitism
122140 get_fitness = lambda i : fitnesses [i ]
123- elite_indices = sorted (range (pop_size ), key = get_fitness , reverse = True )[:elitism ] # Sort the population by fitness and get the top `elitism` indices
124- elites = [population [i ][:] for i in elite_indices ] # Make nepo babies
141+ elite_indices = sorted (range (pop_size ), key = get_fitness , reverse = True )[
142+ :elitism
143+ ] # Sort the population by fitness and get the top `elitism` indices
144+ elites = [population [i ][:] for i in elite_indices ] # Make nepo babies
125145
126146 # New generation
127147 new_pop = elites [:]
@@ -145,12 +165,15 @@ def run_ga(
145165 "avg_history" : avg_history ,
146166 }
147167
168+
148169result = run_ga (items , capacity )
149170
150171best_items = [items [i ] for i , bit in enumerate (result ["best_genome" ]) if bit == 1 ]
151172
152- print (f"Knapsack capacity: { result ["capacity" ]} " )
153- print (f"Best solution: value = { result ["best_value" ]} , weight = { result ["best_weight" ]} " )
173+ print (f"Knapsack capacity: { result ['capacity' ]} " )
174+ print (
175+ f"Best solution: value = { result ['best_value' ]} , weight = { result ['best_weight' ]} "
176+ )
154177
155178# print("Items included in the best solution:", best_items)
156179
0 commit comments