I am trying to implement 8 queens problems in c++. This is my code
#include <iostream>
#include <stdlib.h>
#include <time.h>
#include <vector>
#include <algorithm>
#include <random>
#include <numeric>
using namespace std;
const int nQueens = 8;
const int STOP_CTR = 28;
const double MUTATE = 0.5;
const bool MUTATE_FLAG = true;
const int MAX_ITER = 100000;
int POPULATION = 0;
std::random_device rd;
std::mt19937 gen(rd());
class BoardPosition {
public:
vector<int> sequence;
int fitness;
double survival;
BoardPosition() {
sequence.resize(nQueens);
fitness = 0;
survival = 0.0;
}
};
vector<BoardPosition> population;
void shuffle(vector<int>& array) {
random_device rd;
default_random_engine engine(rd());
shuffle(array.begin(), array.end(), engine);
}
int fitness(const std::vector<int>& individual) {
int attacks = 0;
for (int i = 0; i < nQueens; i++) {
for (int j = i + 1; j < nQueens; j++) {
if (individual[i] == individual[j] || abs(individual[i] - individual[j]) == j - i) {
attacks++;
}
}
}
return 28 - attacks;
}
vector<int> generateChromosome() {
vector<int> init_distribution(nQueens);
for (int i = 0; i < nQueens; i++) {
default_random_engine generator(rd());
uniform_int_distribution<int> distribution(0, nQueens - 1);
init_distribution[i] = distribution(generator);
}
return init_distribution;
}
vector<BoardPosition> generatePopulation(int population_size = 100) {
POPULATION = population_size;
vector<BoardPosition> population(population_size);
for (int i = 0; i < population_size; ++i) {
population[i].sequence = generateChromosome();
population[i].fitness = fitness(population[i].sequence);
}
return population;
}
pair<BoardPosition, BoardPosition> getParent() {
double summation_fitness = 0;
for (auto& x : population) {
summation_fitness += x.fitness;
}
for (auto& each : population) {
each.survival = each.fitness / summation_fitness;
}
// random_device rd;
default_random_engine generator(rd());
uniform_real_distribution<double> distribution(0.0, 1.0);
BoardPosition parent1, parent2;
double offset = 0.0;
while (true) {
double parent1_random = distribution(generator);
for (auto& x : population) {
offset += x.survival;
if (offset <= parent1_random) {
// cout << "Here parent1" << endl;
parent1 = x;
break;
}
}
double parent2_random = distribution(generator);
for (auto& x : population) {
if (x.survival <= parent2_random) {
// cout << "Selected parent2: " << x.fitness << endl;
parent2 = x;
break;
}
}
if (parent1.sequence != parent2.sequence) {
break;
}
// if (!parent2.sequence.empty() && parent2.sequence != parent1.sequence) {
// break;
//}
}
// cout<<"Here " <<endl;
return make_pair(parent1, parent2);
}
BoardPosition reproduce_crossover(std::vector<int>& parent1, std::vector<int>& parent2) {
std::uniform_int_distribution<> dist(0, nQueens - 1);
int cross1 = dist(gen);
int cross2 = dist(gen);
if (cross1 > cross2) std::swap(cross1, cross2);
else if (cross1 == cross2) cross2++;
BoardPosition child;
std::copy(parent1.begin() + cross1, parent1.begin() + cross2, child.sequence.begin() + cross1);
int pos = 0;
for (int i = 0; i < nQueens; i++) {
if (std::find(child.sequence.begin() + cross1, child.sequence.begin() + cross2, parent2[i]) == child.sequence.begin() + cross2) {
while (pos < cross1 || pos >= cross2) pos++;
child.sequence[pos++] = parent2[i];
}
}
return child;
}
BoardPosition mutate(BoardPosition child) {
default_random_engine generator(rd());
for (int i = 0; i < nQueens - 1; i++) {
double r = ((double)rand() / (RAND_MAX));
//cout<<"mutation probability " << r << endl;
if (r < MUTATE) {
uniform_int_distribution<int> distribution(0, nQueens - 1);
child.sequence[i] = distribution(generator);
}
}
return child;
}
vector<BoardPosition> GA(int iteration) {
vector<BoardPosition> newpopulation;
for (int i = 0; i < population.size(); ++i) {
auto parents = getParent();
cout << "Getting parent is done" << endl;
BoardPosition child = reproduce_crossover(parents.first.sequence, parents.second.sequence);
cout << "Crossover is done" << endl;
if (MUTATE_FLAG) {
child = mutate(child);
}
newpopulation.push_back(child);
}
return newpopulation;
}
bool stop(int& iteration) {
for (auto& pos : population) {
// cout << "Pos.fitness " << pos.fitness << endl;
if (pos.fitness == STOP_CTR) {
return true;
}
}
/* if (MAX_ITER == iteration) {
return true;
}*/
return false;
}
int main() {
srand(time(NULL));
population = generatePopulation(100);
int iteration = 0;
while (!stop(iteration)) {
population = GA(iteration);
cout << "iteration number" << iteration << endl;
iteration++;
}
//
for (auto& each : population) {
if (each.fitness == 28) {
for (auto& seq : each.sequence) {
cout << seq << " ";
}
cout << endl;
}
}
return 0;
}
I feel like my selection method and crossover method has an issues but not sure where? Anyone can help here? Am I doing my roullete wheel selection correctly what can I change it there? It is one requirements to have it like that so another method I can't select.
This answer describes the minimum set of changes you need to get your program running.
If something could be done better, but somehow manages to work in the present program, this answer does not change it. For instance, nothing here addresses the many design flaws regarding random number generation.
There are, however, a few suggestions concerning random numbers at the end of this answer.
Preliminaries
#includedirectives.#include <iomanip>.<stdlib.h>to<cstdlib>.<time.h>to<ctime>.MUTATEprobability from0.5to0.25 / nQueens.NULLtonullptr.shuffle. It is never called.By far, the most important of these preliminary changes is to reduce the probability that one of the "genes" in a child "sequence" mutates. With the original value of
MUTATEset to0.5, roughly half the genes in every child will be selected for mutation. In the case wherenQueensis8, that means four of the queens will be randomly repositioned. Such a high rate of mutation creates a chaotic situation where the progress made by selectively crossing over the genes of a child's parents is swamped by mutation.With
MUTATEset to0.25 / nQueens, only about one fourth of the children in a new generation will undergo any mutations at all. Those that do mutate will typically have only one queen repositioned.Are these preliminaries really needed?
Most of them probably are not. However, as they are changes I made to the OP's program, I thought it necessary to document them. You can see them all in the complete program below.
One of the preliminaries is absolutely necessary. That is the change to parameter
MUTATE. The need for this is explained above. After a commenter suggested that, "Really, none of the 'preliminaries' are required," I decided I better run some (more) tests. The table below summarizes the results.With a sample size of only 10 runs, these results must be described as anecdotal. It is worth noting, however, that for
MUTATE == 0.5, no solution was found in 6 of the 10 cases.Although these results are only ancecdotal, they are enough to convince this non-statistician. The change to
MUTATEwas "really required."A problem with
STOP_CTRSTOP_CTRis the "fitness" score obtained when no pair of queens attack each other. It is the highest "fitness" score you can get. It is the fitness score for a position that solves the n queens puzzle.The fitness score for a given position is found by counting the number of pairs of queens that attack each other, and subtracting from
STOP_CTR. If we denote the number of pairs of mutually attacking queens asn_attacking_pairs, then fitness isSTOP_CTR - n_attacking_pairs.In the OP,
STOP_CTRis set to28, but that value is only correct whennQueensis8.When all the queens attack each other, as occurs when they are all placed in the same row, the number of attacking pairs is "nQueens choose 2". Let's call that
n_attacking_pairs_max. WhennQueensis8,n_attacking_pairs_maxis28. Thus, the lowest fitness score you can get (whennQueens == 8) is0.The desire to make the lowest fitness equal to
0is the reason thatSTOP_CTRwas chosen to be28. That only works, however, fornQueens == 8. For other values ofnQueens,STOP_CTRshould be set ton_attacking_pairs_max, i.e., it should be set to "nQueens choose 2".Otherwise, if you always use
28as the value ofSTOP_CTR, the lowest fitness will not always be0. For values ofnQueensthat are larger than8, the lowest fitness score will be a negative number. Many other fitness scores will also be negative.This plays havoc with the calculation of
BoardPosition::survivalthat is made in functiongetParent. That calculation requires that fitness scores be zero-based.After changing the value of the "global" constant
STOP_CTR, it is also necessary replace the hard-coded value28that appears in two places in the program. Instead of28, functionsfitnessandmainshould useSTOP_CTR.Updates to function
mainThe argument to
srandnow uses astatic_cast. This prevents the compiler from issuing a warning about a narrowing conversion.Other updates improve the messages that are output by function
main. All of thestd::coutstatements are new. As the search for a solution progresses, a growing row of dots marks the progress. For those systems that support ANSI escape codes, functionput_boarddisplays the solution on a chess board where red squares mark the positions of the queens.Updates to function
stopAs there is no guarantee that the genetic algorithm will find a solution to the n queens problem, it is essential to stop after
MAX_ITERiterations. This update reinstates that test in functionstop.Updates to function
GAFunction
GAhas a critical omission: it fails to calculate the fitness of children as they are added to thenewpopulation. Here is the fix:Updates to function
reproduce_crossoverAs coded in the OP, function
reproduce_crossoverhas big problems. The loop that attempts to copyparent2into thechild, while skipping over the elements that were copied fromparent1, fails badly.In addition, there is a subtle bias in the way
cross1andcross2are selected. One consequence is that the final element ofparent1will only participate in a crossover when bothcross1andcross2are chosen to benQueens - 1.The increment of
cross2introduces another bias that causes crossover ranges containing a single element to be preferred over other ranges. Rather than incrementingcross2, it should be redrawn.Here is the rewrite:
Note that only one child is produced from each set of parents. This is unusual, because the genetic algorithm for the n queens problem normally produces two, with second getting the "genes" from the two parents that were discarded by the first
child.As the OP's program runs without making this change, I did not added a second child.
Updates to function
getParentAs coded in the OP, function
getParent, the selection function, has several critical errors.Variable
offsetneeds to be reset to0.0prior to each of the loops that scan forparent1andparent2.The loop for
parent2needs to accumulate offsets in the same way as the loop forparent1.The two
offsettests, which currently use<=, should use>. Otherwise, you end up choosing the first element of thepopulationalmost every time. This error results in a "nearly" infinite loop, because bothparent1andparent2are almost always chosen to bepopulation[0].It is possible that the accumulation of round-off errors in the calculation of
offsetwill produce a value that never exceeds the randomly chosen threshold (parent1_randomorparent2_random), in which case, no parent will be selected.There are enough changes to warrant the creation of an new function,
random_parent, which returns the subscript of a randomly selected parent. In the rewrite below, two calls to functionrandom_parentreplace the two loops in the OP.After two parents are selected, they are tested make sure they are distinct elements from vector
population(with different subscripts). Unlike the OP, however, no test is made to require that they hold differentsequences.Note that the loops that calculate
survivalshould not be repeated every time you get two new parents. Those loops belong in functionGA. Putting them here, slows things down unnecesarily. As it does not cause the program to fail, however, I have not made any correction.Sample output
That's it! With those changes the program successfully solves the n queens problem.
I tested with values of
nQueensas high as12. Using a variation where parents are selected from only the fittest half of the population, I was able get solutions withnQueens == 100.Here are a couple of runs with
nQueensset to8.Run 2:
The complete program
Function
put_boardFunction
put_boarduses ANSI escape codes to display a chess board showing the solution. However, not every system supports ANSI. In order to make it easy to remove functionput_boardfrom the program, its source code has been placed into a separate translation unit.Function
mainis the only place where functionput_boardis referenced. As noted in functionmain, the call to functionput_boardcan be removed by deleting the three lines that are marked with comments.Improved random number functions
If you are feeling ambitious, you might try integrating the following random number functions into your program. They are designed to share a single instantiation of
std::mt19937.Random parent
The selection step of the genetic algorithm chooses "parents" for the "children" of the current "population." Roulette wheel selection picks parents randomly, weighting in favor of the "fittest" parents.
std::discrete_distributionis perfect for this task. Given a vector of weights withnelements, it returns an integer between0andn - 1.The values returned by function
random_parent(below) are subscripts that can be used to key thepopulationvector. Note that functionrandom_parentcalls functionrandom_engineto get a reference to the random engine.The vector of weights is formed by copying the "fitness" scores from the
populationvector.Note that this technique of choosing a parent obviates the need for member
survivalin structBoardPosition.Random bool
The decision to mutate a "child" in the next "generation" is made randomly, with probability
MUTATE.Function
random_boolconverts that probability into a discrete "yes/no" choice. It uses a static distribution object that is instantiated only once, during the first call to functionrandom_bool. Once again, note that functionrandom_boolcalls functionrandom_engineto get a reference to the shared engine.Random position and random crossover offset
A random position can be either a row or column number. Positions range between
0andnQueens - 1.Function
random_positionhas a static distribution object that is instantiated only once, during the first call to functionrandom_position. As before, it calls functionrandom_engineto get the engine.Function
random_crossover_offsetis similar to functionrandom_position. The only difference is that the range extends out tonQueens.The values generated by function
random_crossover_offsetare intended to be used in calls tostd::copy, as part of the specification of the range to be copied.When
cross2 == nQueens, the iterator expressionparent1.begin() + cross2has the same value asparent1.end(), i.e., it is an iterator to one position beyond the end of vectorparent1.