#How whe Simulated Annealing algorithm works
The description of the “More Pizza” algorithm have been provided in a previous post.
#The main() function
- Reading of the input parameters (input file name, specific procedure parameters, etc. ).
- Reading of input data and storage of the information in a proper data structure.
- The solving procedure.
- Validation of the result (optional, as it is done by Google itself)
- Output of the solution to a file (if not already done in the solving procedure).
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <algorithm>
#include <chrono>
#include <map>
#include <set>
#include <random>
#include <memory>
using namespace std;
// Default input and output filenames
string iFileName{ "..\a_example.in" };
string oFileName{ "..\a_example.out" };
// Max number of slices (capacity)
int32_t M{ 0 };
// Number available of pizzas
int32_t N{ 0 };
using TSol = vector<int32_t>; // it is used for both pizzas and solution
TSol pizzas{}; // storing number of slices of each pizza
// Random numbers generator
std::random_device rd;
std::mt19937 g(rd());
std::uniform_int_distribution<int32_t> rnProbValue(0, 100);
std::uniform_real_distribution<double> rnProb(0, 1);
// Number of neighbors to check
int32_t numMaxItemsToCheck{ 5u };
double T0 = 10000.0; // Default initial temperature
double ae = 0.000001; // Default reduction factor
int main(int argc, char* argv[])
{
// Reading input parameters
for (int i = 0; i < argc; i++)
{
cout << "argv[" << i << "] = " << argv[i] << endl;
}
if (argc > 1)
{
iFileName = argv[1];
oFileName = iFileName;
oFileName.append(".out");
if (argc >= 3)
{
T0 = std::atof(argv[2]);
if (argc >= 4)
{
numMaxItemsToCheck = std::atoi(argv[3]);
if (argc >= 5)
{
ae = std::atof(argv[4]);
}
}
}
}
std::cout << "input file: " << iFileName << endl;
std::cout << "output file: " << oFileName << endl;
std::cout << "initial temperature: " << T0 << endl;
std::cout << "alpha exp: " << ae << endl;
std::cout << "num neighbours: " << numMaxItemsToCheck << endl;
// Read data from input file
readInput();
auto start = chrono::steady_clock::now();
orderPizzas();
auto end = chrono::steady_clock::now();
cout << "Elapsed time in milliseconds : "
<< chrono::duration_cast<chrono::milliseconds>(end - start).count()
<< " ms" << endl;
return 0;
}
#Reading data from a file
// Read input data from a file
void readInput()
{
ifstream iFile;
iFile.open(iFileName);
iFile >> M; // Maximum number of slices (i.e., max score)
iFile >> N; // Number of pizzas
std::cout << "Max. num. slices: " << M << endl;
std::cout << "N. pizzas: " << N << endl;
//init pizzas vector
pizzas.reserve(N);
for (int32_t i = 0; i < N; i++)
{
int32_t tmpPizza{};
iFile >> tmpPizza;
pizzas.emplace_back(tmpPizza);
}
iFile.close();
pizzas.shrink_to_fit();
std::cout << "DONE!" << endl;
}
#The solution class
- store the actual solution.
- store the score of the solution.
- save the “state” of a solution (e.g., which pizzas can be added to the current solution).
- implement utility functions to: -- create an empty solution (i.e., having zero score). -- create a random solution. -- change the current solution in a random way (please, check mutate() function). -- calculate the score of the solution (please, check score() function). -- save the solution into a file with the required format (please, check the function saveSolutionToFile() ). -- print the current solution to screen
// Container used to store the solution
// including its "state". The state of
// a solution is required to quickly change it
// and generate a new "neighbour"
struct TSolCnt
{
// This constructor is used to create an empty solution
TSolCnt() : sol{}, score{0}, availablePizzas(N,0)
{
// Initially, all pizzas are available
for (int i = 0; i < N; i++)
{
availablePizzas[i]=i;
}
}
// Constructor used to make an initial random solution
TSolCnt( std::uniform_int_distribution<>& rnPizzaID ) :
sol(), // empty solution
score(0), // initial score
availablePizzas(N,0)
{
// Initially, all pizzas are available
for (int i = 0; i < N; i++)
{
availablePizzas[i] = i;
}
// Pizzas that cannot be used will be stored at the end of this vector
std::sort(availablePizzas.begin(), availablePizzas.end(),[&](int a, int b)
{
return (M - (score + pizzas[a])) > (M - (score + pizzas[b]));
});
while ( score < M &&
( !availablePizzas.empty() && ( M - ( score + pizzas[availablePizzas.front()]))>0 ))
{
auto it_end = std::find_if(availablePizzas.cbegin(),
availablePizzas.cend(),
[&](int idx) {
return (M - (score + pizzas[idx])) < 0;
});
const auto numElToCheck = std::distance(availablePizzas.cbegin(), it_end);
const int32_t avIdx = rnPizzaID(g) % numElToCheck;
const auto searchIt = std::next(availablePizzas.cbegin(), avIdx);
const int32_t searchIdx = *searchIt;
if ( (pizzas[searchIdx] + score) <= M)
{
sol.emplace_back(searchIdx);
score += pizzas[searchIdx];
availablePizzas.erase(searchIt);
cout << "searchIdx: " << searchIdx << ", score: " << score << "tr";
// Sorting indexes by our criteria in descending order
std::sort(availablePizzas.begin(), availablePizzas.end(), [&](int a, int b)
{
return (M - (score + pizzas[a])) > (M - (score + pizzas[b]));
});
// Sorting indexes
std::sort(sol.begin(), sol.end());
}
}
}
TSolCnt& operator= (const TSolCnt& rhs)
{
score = rhs.score;
sol.assign(rhs.sol.begin(), rhs.sol.end());
availablePizzas = rhs.availablePizzas;
return *this;
};
void mutate(
std::uniform_int_distribution<int32_t>& rnPizzaID,
const int32_t maxNumRemovals)
{
const int32_t oldSize = static_cast<int32_t>(sol.size());
int32_t numRemovals = 0;
do
{
// find a pizza to remove
const int32_t searchIdx = sol[rnPizzaID(g) % sol.size()];
// update set of available pizzas
availablePizzas.emplace_back(searchIdx);
// update solution and its score
score -= pizzas[searchIdx];
auto it = std::find(sol.cbegin(), sol.cend(), searchIdx);
sol.erase(it);
// increase counter
++numRemovals;
} while (!sol.empty()
&& (sol.size() == oldSize || numRemovals < maxNumRemovals));
// Sorting indexes by our criteria in descending order
std::sort(availablePizzas.begin(), availablePizzas.end(), [&](int a, int b)
{
return (M - (score + pizzas[a])) >(M - (score + pizzas[b]));
});
// adding slices to fill the order
while (score < M &&
(!availablePizzas.empty() && ( M - ( score + pizzas[availablePizzas.front() ] ) ) >=0 ) )
{
const int32_t avIdx = rnPizzaID(g) % availablePizzas.size();
const auto searchIt = std::next(availablePizzas.cbegin(), avIdx);
const int32_t searchIdx = *searchIt;
if ((pizzas[searchIdx] + score) <= M)
{
sol.emplace_back(searchIdx);
score += pizzas[searchIdx];
availablePizzas.erase(searchIt);
// Sorting indexes by our criteria in descending order
std::sort(availablePizzas.begin(), availablePizzas.end(), [&](int a, int b)
{
return (M - (score + pizzas[a])) > (M - (score + pizzas[b]));
});
// Sorting indexes
std::sort(sol.begin(), sol.end());
}
}
}
// Helper function to print the solution
void print()
{
std::cout << "Ordering " << sol.size() << " pizzas:" << std::endl;
int32_t score = 0;
for (auto item : sol)
{
std::cout << item << " ";
score += pizzas[item];
}
cout << endl;
cout << "Score: " << score << endl;
}
// Save solution to a file.
// The name of the file is SCENARIO + "_" + SCORE + ".txt"
void saveSolutionToFile()
{
// print to a file
ofstream outFile;
outFile.open(iFileName + "_" + to_string(score) + ".out");
outFile << sol.size() << endl;
int x = 0;
for (auto& item : sol)
{
outFile << item;
if (x < static_cast<int32_t>(sol.size() - 1))
{
outFile << " ";
}
++x;
}
outFile << endl;
outFile.close();
}
TSol sol; // vector of indexes of the pizzas
int32_t score; // score of the current solution
// Set storing the indexes of the available pizzas
// This vector will be always sorted in a descending order
// by the difference <M - (score + pizzas[index])>, where index
// refers to each value stored inside this vector.
TSol availablePizzas;
};
#How to quickly calculate new solutions?
For this reason, in the mutate() function, after a certain number of pizzas have been randomly removed, then only the pizzas that can effectively contribute to the solution are considered in the selection process.
#The orderPizzas() function
As described in this post, the used algorithm is a “Simulated Annealing” procedure. Please, find the implementation of that algorithm below:
// Using smart pointers, it is straightforward and faster
// to replace local solution with a better solution.
using TSolPtr = std::shared_ptr<TSolCnt>;
void orderPizzas()
{
const int32_t numEl{ static_cast<int32_t>( pizzas.size() ) };
// This is used to choose randomly a pizza to order or remove
std::uniform_int_distribution<int32_t> rnPizzaID(0, numEl - 1);
// Counters
int32_t numItBetweenImprovement = 0;
int32_t cntIters{ 0 };
cout << "...ok! Ready to start..." << endl;
// make local initial solution
TSolPtr localSol = std::make_shared<TSolCnt>(rnPizzaID);
localSol->saveSolutionToFile();
// this is the best solution until now found... so let's save it!
TSolPtr savedSol = std::make_shared<TSolCnt>(*localSol);
// Print some data...
cout << "Initial score: " << savedSol->score << "tsize: " << savedSol->sol.size() << endl;
#ifdef _DEBUG
savedSol->print();
#endif // _DEBUG
double temperature = T0; // initialize temperature
while (savedSol->score<M && numItBetweenImprovement < 500'000)
{
// Calculating new temperature
++cntIters;
temperature = T0 * exp(-ae * cntIters);
// Calculating the number of pizzas to be replaced
const auto maxNumRemovals = 1 + numItBetweenImprovement / 100;
// creating new solutions from current one!
TSolPtr bestNb = std::make_shared<TSolCnt>(); // with zero score
for (auto i = 0; i < numMaxItemsToCheck; i++)
{
// Creating a copy of the local solution
TSolPtr nb = std::make_shared<TSolCnt>(*localSol);
if (nb)
{
// Mutate the new solution
nb->mutate(rnPizzaID, maxNumRemovals);
// Checking if the new solution is better
if (nb->score > bestNb->score )
{
bestNb.swap(nb);
}
}
}
const int32_t deltaEnergy = bestNb->score - savedSol->score;
// If a better solution is found
if (bestNb->score>0 && deltaEnergy > 0)
{
savedSol.swap(bestNb);
localSol = std::make_shared<TSolCnt>(*savedSol);
savedSol->saveSolutionToFile();
cout << "New max score: " << savedSol->score << "tsize: " << savedSol->sol.size();
cout << "tTemperature: " << temperature;
cout << "tdelta: " << deltaEnergy;
cout << "tnumItBetweenImprovements: " << numItBetweenImprovement << endl;
numItBetweenImprovement = 0;
#ifdef _DEBUG
savedSol->print();
#endif // _DEBUG
}
else
{
// check if it can be accepted
const auto probAccept = 1.0 / (1 + exp(-deltaEnergy / temperature));
if (rnProb(g) < probAccept) // accepted
{
localSol.swap(bestNb);
cout << "Accepted lower score: " << localSol->score << "tsize: " << localSol->sol.size();
}
else
{
// Reset local solution to be equal to the best one found until now
localSol = std::make_shared<TSolCnt>(*savedSol);
cout << "Unaccepted lower score: " << bestNb->score << "tsize: " << bestNb->sol.size();
}
cout << "tTemperature: " << temperature;
cout << "tProbAccept: " << probAccept;
cout << "tnumIts: " << numItBetweenImprovement << "r";
++numItBetweenImprovement;
}
}
#ifdef _DEBUG
savedSol->print();
#endif // _DEBUG
}
#The console output
The algorithm has been executed for all the unsolved test scenarios, as shown in the following images.
Small
Medium
Quite big
Also Big
#Results for each test scenario
| Scenario | Average execution time |
|---|---|
| Small | < 1s |
| Medium | < 1s |
| Quite Big | 1-2 minutes |
| Also Big | < 1s |
#The complete code
// Author: Pietro L. Carotenuto
// Website: https://www.pietrolc.com
#include "stdafx.h"
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <algorithm>
#include <chrono>
#include <map>
#include <set>
#include <random>
#include <memory>
using namespace std;
// Default input and output filenames
string iFileName{ "..\a_example.in" };
string oFileName{ "..\a_example.out" };
// Max number of slices (capacity)
int32_t M{ 0 };
// Number available of pizzas
int32_t N{ 0 };
using TSol = vector<int32_t>; // it is used for both pizzas and solution
TSol pizzas{}; // storing number of slices of each pizza
// Random numbers generator
std::random_device rd;
std::mt19937 g(rd());
std::uniform_int_distribution<int32_t> rnProbValue(0, 100);
std::uniform_real_distribution<double> rnProb(0, 1);
// Number of neighbours to check
int32_t numMaxItemsToCheck{ 5u };
double T0 = 10000.0; // Default initial temperature
double ae = 0.000001; // Default reduction factor
// Read input data from a file
void readInput()
{
ifstream iFile;
iFile.open(iFileName);
iFile >> M;
iFile >> N;
std::cout << "Max num slices: " << M << endl;
std::cout << "N. pizzas: " << N << endl;
//init pizzas vector
pizzas.reserve(N);
for (int32_t i = 0; i < N; i++)
{
int32_t tmpPizza{};
iFile >> tmpPizza;
pizzas.emplace_back(tmpPizza);
}
iFile.close();
pizzas.shrink_to_fit();
std::cout << "DONE!" << endl;
}
// Container used to store the solution
// including its "state". The state of
// a solution is required to quickly change it
// and generate a new "neighbour"
struct TSolCnt
{
TSolCnt() : sol{}, score{0}, availablePizzas(N,0)
{
for (int i = 0; i < N; i++)
{
availablePizzas[i]=i;
}
}
// Constructor used to make an initial solution
TSolCnt( std::uniform_int_distribution<>& rnPizzaID ) :
sol(), // empty solution
score(0), // initial score
availablePizzas(N,0)
{
for (int i = 0; i < N; i++)
{
availablePizzas[i] = i;
}
// Sorting indexes by our criteria in descending order
std::sort(availablePizzas.begin(), availablePizzas.end(),[&](int a, int b)
{
return (M - (score + pizzas[a])) > (M - (score + pizzas[b]));
});
while ( score < M &&
( !availablePizzas.empty() && ( M - ( score + pizzas[availablePizzas.front()]))>0 ))
{
auto it_end = std::find_if(availablePizzas.cbegin(),
availablePizzas.cend(),
[&](int idx) {
return (M - (score + pizzas[idx])) < 0;
});
const auto numElToCheck = std::distance(availablePizzas.cbegin(), it_end);
const int32_t avIdx = rnPizzaID(g) % numElToCheck;
const auto searchIt = std::next(availablePizzas.cbegin(), avIdx);
const int32_t searchIdx = *searchIt;
if ( (pizzas[searchIdx] + score) <= M)
{
sol.emplace_back(searchIdx);
score += pizzas[searchIdx];
availablePizzas.erase(searchIt);
cout << "searchIdx: " << searchIdx << ", score: " << score << "tr";
// Sorting indexes by our criteria in descending order
std::sort(availablePizzas.begin(), availablePizzas.end(), [&](int a, int b)
{
return (M - (score + pizzas[a])) > (M - (score + pizzas[b]));
});
// Sorting indexes
std::sort(sol.begin(), sol.end());
}
}
}
TSolCnt& operator= (const TSolCnt& rhs)
{
score = rhs.score;
sol.assign(rhs.sol.begin(), rhs.sol.end());
availablePizzas = rhs.availablePizzas;
return *this;
};
void mutate(
std::uniform_int_distribution<int32_t>& rnPizzaID,
const int32_t maxNumRemovals)
{
const int32_t oldSize = static_cast<int32_t>(sol.size());
int32_t numRemovals = 0;
do
{
// find a pizza to remove
const int32_t searchIdx = sol[rnPizzaID(g) % sol.size()];
// update set of available pizzas
availablePizzas.emplace_back(searchIdx);
// update solution and its score
score -= pizzas[searchIdx];
auto it = std::find(sol.cbegin(), sol.cend(), searchIdx);
sol.erase(it);
// increase counter
++numRemovals;
} while (!sol.empty()
&& (sol.size() == oldSize || numRemovals < maxNumRemovals));
// Sorting indexes by our criteria in descending order
std::sort(availablePizzas.begin(), availablePizzas.end(), [&](int a, int b)
{
return (M - (score + pizzas[a])) >(M - (score + pizzas[b]));
});
// adding slices to fill the order
while (score < M &&
(!availablePizzas.empty() && ( M - ( score + pizzas[availablePizzas.front() ] ) ) >=0 ) )
{
const int32_t avIdx = rnPizzaID(g) % availablePizzas.size();
const auto searchIt = std::next(availablePizzas.cbegin(), avIdx);
const int32_t searchIdx = *searchIt;
if ((pizzas[searchIdx] + score) <= M)
{
sol.emplace_back(searchIdx);
score += pizzas[searchIdx];
availablePizzas.erase(searchIt);
// Sorting indexes by our criteria in descending order
std::sort(availablePizzas.begin(), availablePizzas.end(), [&](int a, int b)
{
return (M - (score + pizzas[a])) > (M - (score + pizzas[b]));
});
// Sorting indexes
std::sort(sol.begin(), sol.end());
}
}
}
// Helper function to print the solution
void print()
{
std::cout << "Ordering " << sol.size() << " pizzas:" << std::endl;
int32_t score = 0;
for (auto item : sol)
{
std::cout << item << " ";
score += pizzas[item];
}
cout << endl;
cout << "Score: " << score << endl;
}
// Save solution to a file.
// The name of the file is SCENARIO + "_" + SCORE + ".txt"
void saveSolutionToFile()
{
// print to a file
ofstream outFile;
outFile.open(iFileName + "_" + to_string(score) + ".out");
outFile << sol.size() << endl;
int x = 0;
for (auto& item : sol)
{
outFile << item;
if (x < static_cast<int32_t>(sol.size() - 1))
{
outFile << " ";
}
++x;
}
outFile << endl;
outFile.close();
}
TSol sol; // vector of indexes of the pizzas
int32_t score; // score of the current solution
// Set storing the indexes of the available pizzas
// This vector will be always sorted in a descending order
// by the difference <M - (score + pizzas[index])>, where index
// refers to each value stored inside this vector.
TSol availablePizzas;
};
// Using smart pointers, it is straightforward and faster
// to replace local solution with a better solution.
using TSolPtr = std::shared_ptr<TSolCnt>;
void orderPizzas()
{
const int32_t numEl{ static_cast<int32_t>( pizzas.size() ) };
// This is used to choose randomly a pizza to order or remove
std::uniform_int_distribution<int32_t> rnPizzaID(0, numEl - 1);
// Counters
int32_t numItBetweenImprovement = 0;
int32_t cntIters{ 0 };
cout << "...ok! Ready to start..." << endl;
// make local initial solution
TSolPtr localSol = std::make_shared<TSolCnt>(rnPizzaID);
localSol->saveSolutionToFile();
// this is the best solution until now found... so let's save it!
TSolPtr savedSol = std::make_shared<TSolCnt>(*localSol);
// Print some data...
cout << "Initial score: " << savedSol->score << "tsize: " << savedSol->sol.size() << endl;
#ifdef _DEBUG
savedSol->print();
#endif // _DEBUG
double temperature = T0; // initialize temperature
while (savedSol->score<M && numItBetweenImprovement < 500'000)
{
// Calculating new temperature
++cntIters;
temperature = T0 * exp(-ae * cntIters);
// Calculating the number of pizzas to be replaced
const auto maxNumRemovals = 1 + numItBetweenImprovement / 100;
// creating new solutions from current one!
TSolPtr bestNb = std::make_shared<TSolCnt>(); // with zero score
for (auto i = 0; i < numMaxItemsToCheck; i++)
{
// Creating a copy of the local solution
TSolPtr nb = std::make_shared<TSolCnt>(*localSol);
if (nb)
{
// Mutate the new solution
nb->mutate(rnPizzaID, maxNumRemovals);
// Checking if the new solution is better
if (nb->score > bestNb->score )
{
bestNb.swap(nb);
}
}
}
const int32_t deltaEnergy = bestNb->score - savedSol->score;
// If a better solution is found
if (bestNb->score>0 && deltaEnergy > 0)
{
savedSol.swap(bestNb);
localSol = std::make_shared<TSolCnt>(*savedSol);
savedSol->saveSolutionToFile();
cout << "New max score: " << savedSol->score << "\tsize: " << savedSol->sol.size();
cout << "\tTemperature: " << temperature;
cout << "\tdelta: " << deltaEnergy;
cout << "\tnumItBetweenImprovements: " << numItBetweenImprovement << endl;
numItBetweenImprovement = 0;
#ifdef _DEBUG
savedSol->print();
#endif // _DEBUG
}
else
{
// check if it can be accepted
const auto probAccept = 1.0 / (1 + exp(-deltaEnergy / temperature));
if (rnProb(g) < probAccept) // accepted
{
localSol.swap(bestNb);
cout << "Accepted lower score: " << localSol->score << "\tsize: " << localSol->sol.size();
}
else
{
// Reset local solution to be equal to the best one found until now
localSol = std::make_shared<TSolCnt>(*savedSol);
cout << "Unaccepted lower score: " << bestNb->score << "\tsize: " << bestNb->sol.size();
}
cout << "\tTemperature: " << temperature;
cout << "\tProbAccept: " << probAccept;
cout << "\tnumIts: " << numItBetweenImprovement << "\r";
++numItBetweenImprovement;
}
}
#ifdef _DEBUG
savedSol->print();
#endif // _DEBUG
}
int main(int argc, char* argv[])
{
// Reading algorithm parameters
for (int i = 0; i < argc; i++)
cout << "argv[" << i << "] = " << argv[i] << endl;
if (argc > 1)
{
iFileName = argv[1];
oFileName = iFileName;
oFileName.append(".out");
if (argc >= 3)
{
T0 = std::atof(argv[2]);
if (argc >= 4)
{
numMaxItemsToCheck = std::atoi(argv[3]);
if (argc >= 5)
{
ae = std::atof(argv[4]);
}
}
}
}
std::cout << "input file: " << iFileName << endl;
std::cout << "output file: " << oFileName << endl;
std::cout << "initial temperature: " << T0 << endl;
std::cout << "alpha exp: " << ae << endl;
std::cout << "num neighbours: " << numMaxItemsToCheck << endl;
// Read data from input file
readInput();
auto start = chrono::steady_clock::now();
orderPizzas();
auto end = chrono::steady_clock::now();
cout << "Elapsed time in milliseconds : "
<< chrono::duration_cast<chrono::milliseconds>(end - start).count()
<< " ms" << endl;
return 0;
}
#New code - using OOP design - is on Github
After five years, I've found the time to refactor this original code I used for the Google HashCode challenge. I've decided to split this large piece of code into smaller classes. The complete and updated code can be found here: SimulatedAnnealingCpp repo.