home
/
blog
/
algoritmo-genetico-sorteo

A Genetic Algorithm to Generate the FIFA World Cup Group Draw

23 minutes
We use Genetic Algorithms to generate the 2026 FIFA World Cup group draw, a constrained combinatorial optimization problem.

Content

The FIFA World Cup 2026 Group Draw

The 2026 FIFA World Cup will be the first to feature 48 national teams from across the globe. These 48 teams must be distributed into 12 groups through a draw procedure that places some interesting constraints on how these groups are formed. Thanks to these constraints, the draw can be conducted quickly and smoothly. Once the draw is complete, the initial matchups for the tournament are set in stone.
In the 2026 edition, with 48 participating teams, the draw becomes even more complex due to the multiple constraints that must be satisfied[1].
Knockout round itinerary according to FIFA. This configuration enforces the restriction that Spain and Argentina, or France and England, cannot face each other before the final, affecting their group assignment in the draw.

Fig: 1. Knockout round itinerary according to FIFA. This configuration enforces the restriction that Spain and Argentina, or France and England, cannot face each other before the final, affecting their group assignment in the draw.

The challenge we face when generating the group draw is a constrained combinatorial optimization problem. There is only a limited number of possible combinations that comply with all the restrictions imposed by FIFA; these are called feasible solutions.

Feasible Solution

A feasible solution is one that satisfies the objective function and does not violate any of the problem's constraints—or at least those considered mandatory.
In the context of World Cup groups, a feasible solution is a draw configuration that complies with all imposed rules, such as confederation distribution and team rankings. Most of the possible combinations that can be made with 48 teams, 4 pots, and 12 groups will not be feasible solutions.
Here lies the difficulty of the problem: finding a draw that is valid across all constraints.
This solution space can be extremely large and complex, especially when considering all confederation and ranking constraints. For example, for the 2026 World Cup, the number of possible draw configurations is astronomical. Let's crunch the numbers.

Possible Combinations

In the draw, there are 42 teams distributed into 4 pots (containers) to be allocated into 12 groups (from A to L). Teams are drawn randomly from each pot until it is empty, ensuring that each group has one team from each pot. The hosts (Canada, Mexico, and the United States) have their groups assigned beforehand, and there are 12 teams in each pot.
Order of teams in the pots. Host teams do not participate in the draw as their respective groups are pre-assigned.

Fig: 2. Order of teams in the pots. Host teams do not participate in the draw as their respective groups are pre-assigned.

Now, let's calculate all possible combinations without considering restrictions, using factorials (n!n!).
  • Pot 1: 9!9!,
  • Pots 2, 3, and 4: 12!12!
  • Total: 9!×12!×12!×12!=3.9881×10319! \times 12! \times 12! \times12! = 3.9881 \times 10^{31} possible combinations.
Just how big is this number? 39, 881, 724, 135, 495, 319, 399, 956, 480, 000, 000 possible ways to organize teams into groups. Ignoring constraints.
To get an idea of the magnitude: if we could evaluate every draw generated by a brute-force algorithm in 0.05 seconds, evaluating all possible solutions would take 6.3×10226.3 \times 10^{22} years. If the algorithm had started at the Big Bang, it wouldn't have finished yet; in fact, it would only be 0.00000000002% of the way through.
Therefore, a brute-force algorithm—one that generates every possible combination until it finds one that doesn't violate restrictions (a feasible one)—could take millions of years. Fortunately, optimization algorithms exist whose search is far more efficient than brute force, reducing this time to a couple of seconds. For the FIFA World Cup group draw, we will use a Genetic Algorithm.

Draw Constraints

The draw constraints define the rules of engagement when pulling teams from pots to assign them to groups. FIFA has redefined the restrictions for the 2026 World Cup since it is the first with 48 teams and 12 groups.
First, it must be clarified that the names of the 42 teams are divided into four containers (pots) ordered according to the FIFA Ranking [2].
The restrictions are as follows [3]:
  • Hosts: Mexico will be assigned position A1, Canada position B1, and the United States position D1.
  • Pot 1: The other nine teams in Pot 1 will be identified by nine balls of the same color and automatically assigned to position 1 of the group into which they are drawn.
  • Uniform Distribution: Each group must have one team from each pot.
  • Confederations per Group: No group can have more than one team from the same confederation, except for UEFA, which can have up to two teams in the same group.
  • Top 4 Ranking: Finally, Spain and Argentina on one side, and France and England on the other, cannot face each other until the final, as they are the top four in the FIFA Men's Ranking at the time [2]. This restricts their placement into arbitrary groups.
Distribution of brackets or team itineraries. Teams from one bracket that qualify for the next round will compete against teams from the other bracket. Spain vs Argentina and England vs France cannot meet until the World Cup final. This assumes each qualifies first in their respective groups.

Fig: 3. Distribution of brackets or team itineraries. Teams from one bracket that qualify for the next round will compete against teams from the other bracket. Spain vs Argentina and England vs France cannot meet until the World Cup final. This assumes each qualifies first in their respective groups.

These are what we call the constraints of the problem. Attempting to generate all possible combinations and then filtering for those that meet the constraints would be computationally intractable, as we have already seen.

What is a Genetic Algorithm?

Without diving too deep, a Genetic Algorithm (GA) is an optimization algorithm inspired by the reproduction of populations from generation to generation and biological evolutionary processes. They fall under the umbrella of evolutionary algorithms.
The theoretical foundations and initial concepts of genetic algorithms belong to the work of John Holland and Lawrence Fogel in the 1960s[4][5].
A genetic algorithm consists of populations of individuals representing candidate solutions to the problem we want to solve. Each of these solutions is evaluated by a cost, fitness, or objective function that measures how good the solution is. Then, the best solutions are selected to reproduce and generate new solutions through crossover and mutation operations.
This process repeats over several generations until a termination criterion is reached, such as a maximum number of generations or a sufficiently good solution.

Basic Genetic Algorithm

In the lifecycle of a genetic algorithm, there are values that generally do not change, called hyperparameters, and their value can determine the success or failure of the algorithm, as some metaheuristics are very sensitive to these inputs. In our case, these hyperparameters are:
  • Population Size: How many individuals (candidate solutions) will make up the population in each generation.
  • Number of Generations: How many times the algorithm's lifecycle will repeat.
  • Crossover Probability: Set at 80%, as this value is usually effective for exploring the search space.
  • Mutation Probability: Set at 20%, as this value effectively maintains genetic diversity without altering existing solutions too much.
python
1POBLACION_TAM = 10 # Population size
2GENERACIONES = 1000 # Number of generations
3PROB_CROSSOVER = 0.8  # 80% crossover probability
4PROB_MUTACION = 0.2  # 20% mutation probability
The lifecycle of a basic genetic algorithm can be represented by the following flowchart:

Initial Population

Genetic algorithms belong to the set of metaheuristics classified as population-based. In this type of algorithm, a set of solutions is evaluated in each iteration, which we call a population. Each population is made up of individuals, and each of these must be evaluated by the cost function. In the best-case scenario, a solution is reached when an individual is found that has optimal fitness and does not incur any violation of restrictions.
For this problem, each individual is a draw, as mentioned before. This individual may contain schemas that violate the draw rules and constraints, and it is the fitness function that punishes that individual by assigning values that push it further away from the optimum (from its chance to reproduce).
Before defining the population, it is important to first define the individuals that make it up. We call this the individual encoding.
We must find a way to represent it in the algorithm and create it (encoding).
The following code shows the process of creating an individual in a procedure similar to the actual draw; that is, teams are assigned pot by pot, from the first to the fourth. Ignoring restrictions, individuals can be valid or invalid, depending on whether they violate constraints. It is the algorithm that must determine who is the best or optimal among all individuals.
python
1def crear_individuo(lista_equipos):
2    # Create an initial valid individual (draw)
3    bombos = {1: [], 2: [], 3: [], 4: []}
4    # Distribute teams by pot
5    for eq in lista_equipos:
6        bombos[eq.bombo].append(eq)
7    # Assign teams to groups
8    grupos = [[] for _ in range(12)]
9    for b in range(1, 5):
10        equipos = bombos[b]
11        # Shuffle for randomness
12        random.shuffle(equipos)
13        pendientes = []
14        for eq in equipos:
15            if eq.grupo_fijo:
16                # Assign to fixed group and skip (converts group letter to index)
17                idx = ord(eq.grupo_fijo) - 65
18                grupos[idx].append(eq)
19            else:
20                pendientes.append(eq)
21        idx_p = 0
22        # Assign remaining teams from this pot
23        for g_idx in range(12):
24            if len(grupos[g_idx]) < b:
25                grupos[g_idx].append(pendientes[idx_p])
26                idx_p += 1
27    return grupos

Individual Structure

We have already said that each individual of the genetic algorithm represents a candidate solution to the problem. This is a group draw with the 12 complete groups, regardless of whether they are valid or not. Since each draw is composed of groups, each group represents what we call a gene within the individual.
Representation of an individual as a complete draw of 12 groups and 48 teams. It can be seen that this individual does not meet some restrictions, for example, Group H has both Qatar and Jordan. These two teams belong to the same confederation and are not allowed to share a group.

Fig: 4. Representation of an individual as a complete draw of 12 groups and 48 teams. It can be seen that this individual does not meet some restrictions, for example, Group H has both Qatar and Jordan. These two teams belong to the same confederation and are not allowed to share a group.

In Figure 4, we see an example of what our algorithm will treat as an individual, and what we see is a complete draw assigning each team to one of the groups. This particular individual is not valid, since Group H contains Qatar and Jordan, both belonging to the same confederation (AFC), and it is not permitted for them to share a group. Additionally, the group with Croatia, Norway, and the European playoff winner cannot share a group since all three belong to UEFA. Something similar happens with the other non-European playoffs.
For our code, each gene consists of elements of the Equipo (Team) class that contains the relevant information for each national team:
python
1class Equipo:
2    def __init__():
3        self.nombre = nombre
4        self.confederacion = confederacion  # Simple String
5        self.bombo = bombo
6        self.grupo_fijo = grupo_fijo
7        self.ranking_top = ranking_top  # 1, 2, 3, 4 or None
8
9    def __repr__(self):
10        # Shows useful info when printing
11        r =  if self.ranking_top else ""
12        f =  if self.grupo_fijo else ""
13        return
For example, a top 4 team would be represented like this:
python
1Equipo("Spain", ["UEFA"], bombo=1, ranking_top=1)
2Equipo("Argentina", ["CONMEBOL"], bombo=1, ranking_top=2)
And host teams with a fixed group:
python
1Equipo("Mexico", ["CONCACAF"], bombo=1, grupo_fijo="A")
2Equipo("Canada", ["CONCACAF"], bombo=1, grupo_fijo="B")
The rest of the teams are instantiated to the Team class more simply:
python
1Equipo("Germany", ["UEFA"], bombo=1)
2Equipo("Ecuador", ["CONMEBOL"], bombo=2)
3Equipo("Panama", ["CONCACAF"], bombo=3)
4# And so on until the rest are complete...

Fitness Function

The simplest way to evaluate an individual with these characteristics is by creating an objective function that counts the times restrictions are violated. For each violation, it assigns a penalty to that individual—an unfavorable valuation so that in the lifecycle of the algorithm, this individual falls behind, cannot reproduce, and eventually goes extinct.
Evaluation of two individuals. Individual A has a fitness of 900 because it violates the confederation repetition rule in groups C, D, and E. Additionally, it fails to separate the itineraries of Spain and Argentina.

Fig: 5. Evaluation of two individuals. Individual A has a fitness of 900 because it violates the confederation repetition rule in groups C, D, and E. Additionally, it fails to separate the itineraries of Spain and Argentina.

The algorithm seeks to minimize this number. A fitness of 1400 is a disaster; a fitness of 0 is a valid draw.
The function calcularFitness(sorteo) is the engine of the algorithm. Its goal is to reach zero, meaning it has found an individual whose evaluation violates none of the restrictions.
Upon finding one of these, the algorithm has found the optimum, i.e., a valid draw. This is where the aforementioned constraints come into play.
Here we apply Hard Constraints:
  • Confederations: No group can have more than one team from the same confederation (except Europe, which allows two).
python
1for j in range(len(grupo)):
2            for k in range(j + 1, len(grupo)):
3                equipo_a = grupo[j]
4                equipo_b = grupo[k]
5
6                # Set intersection:
7                set_a = set(equipo_a.confederacion)
8                set_b = set(equipo_b.confederacion)
9                interseccion = set_a.intersection(set_b)
10
11                # If there is intersection and it is NOT UEFA (because UEFA allows 2), we penalize
12                conflicto_real = [c for c in interseccion if c != "UEFA"]
13
14                if conflicto_real:
15                    penalizacion += PENALTY_CONF_LIMIT
  • Itineraries: The Top 4 seeded teams must be distributed in opposite brackets so they do not meet before the semifinals. If Top 1 and Top 2 land on the same side of the bracket, we apply a massive penalty (+500).
python
1# We create a dictionary to know where the Top 4 are
2  ubicacion_tops = {}
3    # For each group we get that location:
4    for equipo in grupo:
5            if equipo.ranking_top:
6                ubicacion_tops[equipo.ranking_top] = i
7
8    # Rule: Top 1 and Top 2 must go on opposite sides of the itinerary
9    if 1 in ubicacion_tops and 2 in ubicacion_tops:
10        it_1 = obtener_itinerario(ubicacion_tops[1])
11        it_2 = obtener_itinerario(ubicacion_tops[2])
12    # Penalize if they are on the same side of the bracket
13    if it_1 == it_2:
14        penalizacion += PENALTY_ITINERARY
Let's see the complete cost function. Its goal is to evaluate each group, count the times a restriction is violated, and apply penalties.
python
1def calcular_fitness(sorteo):
2    """
3    Calculates total penalty of a draw (chromosome).
4    0 = Valid draw.
5    return: penalty
6    """
7    penalizacion = 0
8
9    # Penalty Constants (Weights)
10    PENALTY_CONF_LIMIT = 1000  # Confederation conflict (Severe)
11    PENALTY_UEFA_LIMIT = 500  # More than 2 Europeans
12    PENALTY_ITINERARY = 500  # Top seeds clashing before final
13
14    # Track where Top seeds landed {ranking: group_index}
15    ubicacion_tops = {}
16
17    # --- 1. Loop through Groups ---
18    for i, grupo in enumerate(sorteo):
19
20        # A. Check UEFA limit (Max 2)
21        # Count how many teams have 'UEFA' in their confederation list
22        uefa_count = sum(1 for equipo in grupo if ["UEFA"] in equipo.confederacion)
23
24        if uefa_count > 2:
25            # Penalize to correct it
26            penalizacion += PENALTY_UEFA_LIMIT * (uefa_count - 2)
27
28        # B. Check Confederation clashes (NON-UEFA)
29        # Use double for loop to compare everyone against everyone in the group
30        for j in range(len(grupo)):
31            for k in range(j + 1, len(grupo)):
32                equipo_a = grupo[j]
33                equipo_b = grupo[k]
34
35                # Set intersection:
36                # If Team A is ['CONMEBOL'] and Team B (Playoff) is ['CONMEBOL', 'AFC']
37                # The intersection is {'CONMEBOL'} -> Conflict!
38                set_a = set(equipo_a.confederacion)
39                set_b = set(equipo_b.confederacion)
40                interseccion = set_a.intersection(set_b)
41
42                # If there is intersection and it is NOT UEFA (because UEFA allows 2), we penalize
43                # Note: If both are UEFA, 'UEFA' will be in the intersection,
44                # but we already controlled that above with uefa_count.
45                conflicto_real = [c for c in interseccion if c != "UEFA"]
46
47                if conflicto_real:
48                    penalizacion += PENALTY_CONF_LIMIT
49
50        # C. Save location of Tops for phase 2
51        for equipo in grupo:
52            if equipo.ranking_top:
53                ubicacion_tops[equipo.ranking_top] = i
54
55    # --- 2. Itinerary Penalties (Bracket) ---
56
57    # Rule: Top 1 and Top 2 must go on opposite sides of the itinerary
58    if 1 in ubicacion_tops and 2 in ubicacion_tops:
59        it_1 = obtener_itinerario(ubicacion_tops[1])
60        it_2 = obtener_itinerario(ubicacion_tops[2])
61
62        if it_1 == it_2:
63            penalizacion += PENALTY_ITINERARY
64
65    # Rule: Top 3 and Top 4 must go on opposite sides of the itinerary
66    if 3 in ubicacion_tops and 4 in ubicacion_tops:
67        it_3 = obtener_itinerario(ubicacion_tops[3])
68        it_4 = obtener_itinerario(ubicacion_tops[4])
69
70        if it_3 == it_4:
71            penalizacion += PENALTY_ITINERARY
72
73    return penalizacion

Genetic Algorithm Lifecycle

In a population-based algorithm, a population of individuals is created, and this population is traversed evaluating the cost of each one in search of the optimum. If none is found, we must resort to genetic operators, which are responsible for creating variations in the population to generate new individuals.
These operators are mutation and crossover, simulating aspects of genetics. Mutation is equivalent to taking an individual (chromosome) and randomly changing an element (gene). Crossover simulates the reproduction of two individuals. It consists of taking two individuals and exchanging genetic material (sections of the individual) between one and the other. Both crossover and mutation have many variants[6]; in this case, we apply the simplest ones, which are those defined previously.
In the specific case of the draw, crossover is done by taking two individuals via the roulette mechanism. These two will play the role of father and mother reproducing to generate a child; this child will be the new generated individual.
The cruzar function takes two parents and randomly decides which will be the donor and receptor (although actually both donate). Each donor will swap all groups received from a specific pot with the other parent.
python
1def ejecutar_ga_completo():
2    # --- CONFIGURATION ---
3    POBLACION_TAM = 10
4    GENERACIONES = 1000
5    PROB_CROSSOVER = 0.8  # 80% crossover probability
6    PROB_MUTACION = 0.2  # 20% mutation probability
7    K_TORNEO = 2  # Tournament size for selection
8
9    datos = generar_datos_usuario()
10    poblacion = [crear_individuo(datos) for _ in range(POBLACION_TAM)]
11
12    print()
13
14    for gen in range(GENERACIONES):
15        # Evaluate Fitness
16        # Save (score, individual)
17        evaluados = [(calcular_fitness(ind), ind) for ind in poblacion]
18        evaluados.sort(key=lambda x: x[0])
19
20        best_score = evaluados[0][0]
21        # Monitoring
22        if gen % 100 == 0 or best_score == 0:
23            print()
24
25        if best_score == 0:
26            print()
27            return evaluados[0][1]
28
29        # Selection (Simple Tournament)
30        nueva_poblacion = []
31
32        while len(nueva_poblacion) < POBLACION_TAM:
33            
34            # OPTION A: TOURNAMENT
35            # padre1 = seleccion_torneo(evaluados, k=K_TORNEO)
36            # padre2 = seleccion_torneo(evaluados, k=K_TORNEO)
37
38            # OPTION B: ROULETTE
39            padre1 = seleccion_ruleta(evaluados)
40            padre2 = seleccion_ruleta(evaluados)
41
42            # Crossover subject to probability (0.8)
43            if random.random() < PROB_CROSSOVER:
44                hijo1, hijo2 = cruzar(padre1, padre2)
45            else:
46                hijo1, hijo2 = copy.deepcopy(padre1), copy.deepcopy(padre2)
47            # Mutation subject to probability (0.2)
48            hijo1 = mutar(hijo1, PROB_MUTACION)
49            hijo2 = mutar(hijo2, PROB_MUTACION)
50
51            nueva_poblacion.append(hijo1)
52            if len(nueva_poblacion) < POBLACION_TAM:
53                nueva_poblacion.append(hijo2)
54        poblacion = nueva_poblacion
55
56    print("⚠️ Limit reached. Returning best approximation.")
57    return evaluados[0][1]

Genetic Operators

The first operator to be applied is called selection. The goal of selection is to simulate Darwin's law of survival of the fittest. Like other operators, there are many ways to perform selection, with roulette and tournament being among the most used.
In the tournament operator, pairs of individuals are taken and their fitness value is compared. In each pair, the winner "qualifies" and faces other winners until a tournament winner is obtained. This winner is selected for crossover.
The roulette operator simulates the behavior of a roulette wheel; however, the spaces or divisions of this wheel are not equal. The probability of selecting an individual is proportional to its fitness value. The higher the fitness (or lower cost), the higher the probability of being selected. The selected individuals form part of the parents from which the new population of individuals will emerge via other genetic operators.

Selection

We don't let just anyone reproduce. The roulette operator is utilized, although the tournament operator also appears in the code. This operator takes all individuals of a generation and creates an array called pesos (weights) containing the inverse of the fitness value of each. Then it randomly selects one using the random.choices() function which assigns an individual randomly, but with a probability proportional to its fitness value, designated in the code by the pesos variable.
This process is repeated to generate each Parent, for every individual in every generation. This generates enough individuals to completely replace (in equal numbers) all individuals of a generation.
python
1def seleccion_ruleta(evaluados):
2    """
3    Input: List of tuples (fitness, individual)
4    Selection proportional to fitness (Roulette).
5    Since we seek to MINIMIZE cost, we invert the value.
6    Fitness 0 -> Very high weight. Fitness 500 -> Low weight.
7    """
8    individuos = [item[1] for item in evaluados]
9    costos = [item[0] for item in evaluados]
10
11    # We invert costs to obtain weights (Higher weight = Higher probability)
12    pesos = [1.0 / (1.0 + c) for c in costos]
13
14    # The choices function with 3 arguments (population, weights, quantity)
15    seleccionado = random.choices(population=individuos, weights=pesos, k=1)[0]
16
17    return seleccionado

Crossover

Example of the crossover operator. In the image, we see that Parent A donates all teams belonging to pots 1 and 4 to Child A, and teams from pot 2 and 3 to Child B.

Fig: 6. Example of the crossover operator. In the image, we see that Parent A donates all teams belonging to pots 1 and 4 to Child A, and teams from pot 2 and 3 to Child B.

Crossover is the most important operator, as it is responsible for creating diversity and exploring the search space.
The crossover used here takes two parents and creates two children.
This leads to the replacement of the current population by a new generation. In our case, we must be very careful because we cannot perform a crossover at just any point of the individual (chromosome). If we slice a draw in half and swap it with another, we would break the structure of the pots.
The crossover operator defined in the cruzar() function operates pot by pot. For Pot 1 (Top seeds), flip a coin: Do you inherit the distribution from Parent A or B? Repeat for Pots 2, 3, and 4.
This guarantees that the child is structurally valid (there will always be one team from each pot in each group) but genetically new.
For each pot (1, 2, 3, 4):
  • Flip coin: from which parent to inherit this pot?
  • Child1 inherits complete pot from selected parent
  • Child2 inherits complete pot from other parent
Result: 2 new draws (children)
python
1def cruzar(padre1, padre2):
2    """
3    Stratified Crossover Operator (Uniform Crossover by Pots).
4    The child inherits the entire configuration of a pot from one of the parents.
5    """
6    # Empty children
7    hijo1 = [[] for _ in range(12)]
8    hijo2 = [[] for _ in range(12)]
9    # We iterate through each Pot level (0 to 3)
10    for bombo_idx in range(4):
11        # Random decision: Who donates this pot?
12        if random.random() < 0.5:
13            # Case A: Child 1 inherits from Father 1, Child 2 inherits from Father 2
14            donante_1 = padre1
15            donante_2 = padre2
16        else:
17            # Case B: Crossed
18            donante_1 = padre2
19            donante_2 = padre1
20
21        # We copy the entire row of that pot to the children
22        for g_idx in range(12):
23            equipo_p1 = donante_1[g_idx][bombo_idx]
24            equipo_p2 = donante_2[g_idx][bombo_idx]
25
26            hijo1[g_idx].append(equipo_p1)
27            hijo2[g_idx].append(equipo_p2)
28    return hijo1, hijo2

Mutation

If we only cross parents, eventually all children will look alike, and we will stagnate in a "Local Minimum" (a solution that seems good but is not the best).
Example of the mutation operator.

Fig: 7. Example of the mutation operator.

The mutar(individuo) function introduces controlled chaos. With a 20% probability, we take a draw, pick two random groups, and swap two teams from the same pot.
If it mutates:
  • Pick 2 random groups
  • Pick a random pot (1, 2, 3, or 4)
  • Swap the teams of that pot between the 2 groups
  • Only if neither has a fixed group
python
1def mutar(individuo, prob_mutacion):
2    """
3    Attempts to mutate with probability 'prob_mutacion'.
4    """
5    if random.random() > prob_mutacion:
6        return individuo  # Does not mutate
7
8    # If it mutates, we perform the swap
9    nuevo = copy.deepcopy(individuo)
10    idx_g1, idx_g2 = random.sample(range(12), 2)
11    bombo_idx = random.randint(0, 3)
12
13    e1 = nuevo[idx_g1][bombo_idx]
14    e2 = nuevo[idx_g2][bombo_idx]
15
16    if not e1.grupo_fijo and not e2.grupo_fijo:
17        nuevo[idx_g1][bombo_idx], nuevo[idx_g2][bombo_idx] = (
18            nuevo[idx_g2][bombo_idx],
19            nuevo[idx_g1][bombo_idx],
20        )
21
22    return nuevo

Replacement

The new generation completely replaces the previous one (this is debatable).

Termination Criterion

To end the lifecycle, we must have a termination criterion. One method terminates the algorithm if the maximum number of previously stipulated generations is reached. The other, if it detects that an individual has a fitness equal to zero, takes this individual as the optimal solution.
Termination Criteria:
  • If a draw with penalty=0Success\text{penalty} = 0 \Rightarrow \text{Success} is found.
  • If it reaches generation 10001000 \Rightarrow return the best one found.
Fortunately, the pot structure allows finding solutions quickly with genetic algorithms, and there is usually no need to resort to the generation termination criterion.

Results

Visit this blog entry (clicking here 👀) to see an example of a draw generated by the genetic algorithm. You can also test the full code in the GitHub repository by clicking the link below.
  1. John Holland, Adaptation in Natural and Artificial Systems, 1975, https://mitpress.mit.edu/books/adaptation-natural-and-artificial-systems (Back to text)
  2. Lawrence Fogel, Intelligent decision making through a simulation of evolution, 1966, https://doi.org/10.1002/bs.3830110403 (Back to text)
  3. Larrañaga et al, Genetic Algorithms for the Travelling Salesman Problem: A Review of Representations and Operators, https://doi.org/10.1023/A:1006529012972 (Back to text)