代碼 | 自適應大鄰域搜索系列之(2) - ALNS算法主邏輯結構解析
00 前言
在上一篇推文中,教大家利用了ALNS的lib庫求解了一個TSP問題作為實例。不知道你萌把代碼跑起來了沒有。那麽,今天咱們再接再厲。跑完代碼以後,小編再給大家深入講解具體的代碼內容。大家快去搬個小板凳一起過來圍觀學習吧~
01 總體概述
前排高能預警,在下面的講解中,會涉及很多C++語言的知識,特別是類與派生這一塊的內容,如果C++基礎比較薄弱的同學則需要回去(洗洗睡)再好好補一補啦,在這裏小編就不再過多科普基礎知識了。默認大家都是C++大佬,能一口說出虛函數表是什麽的內種……
描述整一個ALNS算法邏輯過程的是一個叫ALNS的C++類。下面對其成員變量和成員函數講解一下。
1.1 成員變量
//! 當前解。 ISolution* currentSolution; //! 判斷接受準則。 IAcceptanceModule* acceptanceCriterion; //! ALNS算法運行的相關參數。 ALNS_Parameters* param; //! destroy和repair方法的管理者。 AOperatorManager* opManager; //! 最優解的管理者。 IBestSolutionManager* bestSolManager; //! 局部搜索的管理者。 ILocalSearchManager* lsManager; //! 自上次重新計算重新的權重以來的叠代次數。 size_t nbIterationsWC; //! 當前叠代次數。 size_t nbIterations; //! The current number of iterations without improvement. size_t nbIterationsWithoutImprovement; //! The number of iterations without improvement of the current solution. size_t nbIterationsWithoutImprovementCurrent; //! The number of iterations without acceptation of a transition. size_t nbIterationsWithoutTransition; //! The number of iterations since the last call to a local search //! operator. size_t nbIterationsWithoutLocalSearch; //! 求解的總時間。 clock_t startingTime; //! 最優解的下界。 double lowerBound; //! A set containing the hash keys of the encountred solutions. std::set<long long> knownKeys; //! 用於計算求解過程的一些狀態量。 Statistics stats; //! 最近一次叠代的狀態。 ALNS_Iteration_Status status; //! 每次叠代完成後需要更新的對象。 std::vector<IUpdatable*> updatableStructures; //! ALNS實例的名字。 std::string name;
上面的成員變量類型用的都是抽象類的指針,因為在實際寫代碼的過程中,coder們肯定還要對solution、localsearch等類進行繼承和派生,接口重寫等。用抽象類的指針好處就是在於當它指向子類對象時也能正確調用。再說明一點,上面的ISolution啊IAcceptanceModule等都是一些抽象類的類型,以後會進行介紹和講解的,在這裏大家知道它代表什麽就行了。
1.2 成員函數
//! Constructor. //! \param name the name of the instance. //! \param initialSolution the starting solution that is going to be optimized. //! \param acceptanceCrit the module that determine whether or not a new solution //! is accepted as the current solution. //! \param parameters the set of parameters to be use by the ALNS. //! \param opMan an operator manager. ALNS(std::string instanceName, ISolution& initialSolution, IAcceptanceModule& acceptanceCrit, ALNS_Parameters& parameters, AOperatorManager& opMan, IBestSolutionManager& solMan, ILocalSearchManager& lsMan); //! Destructor. virtual ~ALNS(); //! This method launch the solving process. //! \return true if a feasible solution is found, //! false otherwise. bool solve(); //! This method seeks if a solution is already known, //! if not it is added to the set of known solutions. //! \param sol the solution to be checked. //! \return true if the solution was unknown, false otherwise. bool checkAgainstKnownSolution(ISolution& sol); //! This method perform one iteration of the ALNS solving //! process. void performOneIteration(); //! This method check whether or not the stopping criteria is met. bool isStoppingCriterionMet(); //! Determine whether or not the new solution is better than the //! best known solution. bool isNewBest(ISolution* newSol); //! \return the number of known solutions. size_t getNumberKnownSolutions(){return knownKeys.size();}; //! Determine whether or not the new solution should be accepted //! as the current solution. bool transitionCurrentSolution(ISolution* newSol); //! Return a pointer to the best known solution. IBestSolutionManager* getBestSolutionManager(){return bestSolManager;}; //! Add an object to the list of object to be updated at the end of each //! iteration of the ALNS. //! \param up the updatable object to be added. void addUpdatable(IUpdatable& up){updatableStructures.push_back(&up);}; //! Destroy the manager that have been provided at the construction of //! the instance. void end();
ALNS類的成員函數以及參數說明、函數說明等都在註釋裏面了。這麽簡單的英文相信大家都能看懂,小編就不作翻譯了。
02 具體實現
在看完ALNS類的總體概述以後,我們現在來研究一下各個成員函數的具體實現代碼和過程。
2.1 ALNS::構造和析構函數
構造函數主要做的是一些初始化工作,用傳進來的參數對成員變量進行初始化,或者直接初始化相關的成員變量等。而析構函數主要做的是清理工作,釋放動態申請的堆內存。
//構造
ALNS::ALNS(string instanceName,
ISolution& initialSolution,
IAcceptanceModule& acceptanceCrit,
ALNS_Parameters& parameters,
AOperatorManager& opMan,
IBestSolutionManager& bestSolMan,
ILocalSearchManager& lsMan)
{
name = instanceName;
currentSolution = initialSolution.getCopy();
acceptanceCriterion = &acceptanceCrit;
param = ¶meters;
lowerBound = -DBL_MAX;
nbIterationsWC = 0;
nbIterations = 0;
nbIterationsWithoutImprovement = 0;
opManager = &opMan;
bestSolManager = &bestSolMan;
lsManager = &lsMan;
opManager->setStatistics(&stats);
// We add the initial solution in the best solution manager.
bestSolManager->isNewBestSolution(initialSolution);
nbIterationsWithoutImprovementCurrent = 0;
nbIterationsWithoutTransition = 0;
nbIterationsWithoutLocalSearch = 0;
}
//析構
ALNS::~ALNS()
{
delete currentSolution;
}
2.2 ALNS::performOneIteration()
該方法執行一次叠代操作。也是整個流程比較核心的部分。大概過程是先進行destroy操作和進行repair操作,然後判斷新解質量。而後看情況要不要使用LocalSearch進行搜索,再用判斷接受準則看是否要接受新解。最後更新destroy操作和repair操作的成績。再做一些狀態量的處理等。具體註釋我已經標註在代碼裏了,理解起來不難。
void ALNS::performOneIteration()
{
//重新初始化一些狀態量。
status.partialReinit();
//選擇Repair和Destroy方法
ARepairOperator& repair = opManager->selectRepairOperator();
ADestroyOperator& destroy = opManager->selectDestroyOperator();
ISolution* newSolution = currentSolution->getCopy();
//輸出叠代次數等信息。 param->getLogFrequency()獲取的是logFrequency變量的值,logFrequency變量表示的意思是每隔多少次輸出一下相關信息。
if(nbIterations % param->getLogFrequency() == 0)
{
cout << "[ALNS] it. " << nbIterations << " best sol: " << (*(bestSolManager->begin()))->getObjectiveValue() << " nb known solutions: " << knownKeys.size() << endl;
}
//destroy操作
destroy.destroySolution(*newSolution);
//更新狀態
status.setAlreadyDestroyed(ALNS_Iteration_Status::TRUE);
status.setAlreadyRepaired(ALNS_Iteration_Status::FALSE);//未進行repair操作
//表示newSolution還是status的信息對解進行更新。這裏只提供接口,後面應用的時候要具體重寫。
for(vector<IUpdatable*>::iterator it = updatableStructures.begin(); it != updatableStructures.end(); it++)
{
(*it)->update(*newSolution,status);
}
//進行repair操作
repair.repairSolution(*newSolution);
status.setAlreadyRepaired(ALNS_Iteration_Status::TRUE);
//更新叠代次數
nbIterations++;
status.setIterationId(nbIterations);
nbIterationsWC++;
double newCost = newSolution->getObjectiveValue();
//判斷新生產的解是不是新的最優解
isNewBest(newSolution);
//判斷新生成的解之前有沒有出現過
checkAgainstKnownSolution(*newSolution);
//判斷新生成的解和當前解誰更優
bool betterThanCurrent = (*newSolution)<(*currentSolution);
//如果新生成的解更優
if(betterThanCurrent)
{
nbIterationsWithoutImprovementCurrent = 0;//清0
status.setImproveCurrentSolution(ALNS_Iteration_Status::TRUE);
}
//否則
else
{
nbIterationsWithoutImprovementCurrent++;
status.setImproveCurrentSolution(ALNS_Iteration_Status::FALSE);//解沒有得到提高
}
//更新狀態
status.setNbIterationWithoutImprovementCurrent(nbIterationsWithoutImprovementCurrent);
//param->getPerformLocalSearch()指出要不要用LocalSearch,然後再用LocalSearch對新生成的解進行搜索。lsManager->useLocalSearch(*newSolution,status)將返回true如果經過LocalSearch之後的解有改進的話。
if(param->getPerformLocalSearch() && lsManager->useLocalSearch(*newSolution,status))
{
//判斷LocalSearch之後的新解是不是最優解。
bestSolManager->isNewBestSolution(*newSolution);
}
//判斷是否接受當前的解。
bool transitionAccepted = transitionCurrentSolution(newSolution);
//如果接受
if(transitionAccepted)
{
status.setAcceptedAsCurrentSolution(ALNS_Iteration_Status::TRUE);
nbIterationsWithoutTransition = 0;
}
//否則
else
{
status.setAcceptedAsCurrentSolution(ALNS_Iteration_Status::FALSE);
nbIterationsWithoutTransition++;
}
//更新信息
status.setNbIterationWithoutTransition(nbIterationsWithoutTransition);
//再一次進行LocalSearch操作,以取得更好的效果。
if(param->getPerformLocalSearch() && lsManager->useLocalSearch(*newSolution,status))
{
bestSolManager->isNewBestSolution(*newSolution);
if(status.getAcceptedAsCurrentSolution() == ALNS_Iteration_Status::TRUE)
{
transitionCurrentSolution(newSolution);
}
}
//對destroy,repair方法進行成績更新。
opManager->updateScores(destroy,repair,status);
//記錄本次叠代過程的一些信息。
stats.addEntry(static_cast<double>(clock()-startingTime)/CLOCKS_PER_SEC,nbIterations,destroy.getName(),repair.getName(),newCost,currentSolution->getObjectiveValue(),(*(bestSolManager->begin()))->getObjectiveValue(),knownKeys.size());
//更新destroy,repair方法的權重。是在進行了一定叠代次數以後才更新的,具體次數由param->getTimeSegmentsIt()獲得。
if(nbIterationsWC % param->getTimeSegmentsIt() == 0)
{
opManager->recomputeWeights();
nbIterationsWC = 0;
}
//接口,對解的某些部分再次更新。
for(vector<IUpdatable*>::iterator it = updatableStructures.begin(); it != updatableStructures.end(); it++)
{
(*it)->update(*newSolution,status);
}
//如果有需要,將當前解轉變成最優解再進行下一次叠代操作。
currentSolution = bestSolManager->reloadBestSolution(currentSolution,status);
delete newSolution;
}
2.3 ALNS::checkAgainstKnownSolution(ISolution& sol)
檢查該解是否是之前出現過的解。主要原理是利用一個解的哈希表,所有第一次出現的解都會生成一個唯一的哈希值存於哈希表中。將傳入解的哈希值在哈希表中進行匹配,如果存在,那麽這個解之前已經出現過了,否則就是獨一無二的新解。
bool ALNS::checkAgainstKnownSolution(ISolution& sol)
{
bool notKnownSolution = false;
long long keySol = sol.getHash();
//哈希值匹配
if(knownKeys.find(keySol) == knownKeys.end())
{
notKnownSolution = true;
knownKeys.insert(keySol);
}
//之前已經出現過的解。
if(!notKnownSolution)
{
status.setAlreadyKnownSolution(ALNS_Iteration_Status::TRUE);
}
//全新的解,之前沒有出現過。
else
{
status.setAlreadyKnownSolution(ALNS_Iteration_Status::FALSE);
}
return notKnownSolution;
}
2.4 ALNS::isNewBest(ISolution* newSol)
用來判斷解是否為新的最優解,並做出相應的設置。
bool ALNS::isNewBest(ISolution* newSol)
{
//如果是新的最優解
if(bestSolManager->isNewBestSolution(*newSol))
{
status.setNewBestSolution(ALNS_Iteration_Status::TRUE);
nbIterationsWithoutImprovement = 0;
status.setNbIterationWithoutImprovement(nbIterationsWithoutImprovement);
status.setNbIterationWithoutImprovementSinceLastReload(0);
return true;
}
//如果不是。
else
{
status.setNewBestSolution(ALNS_Iteration_Status::FALSE);
nbIterationsWithoutImprovement++;
status.setNbIterationWithoutImprovement(nbIterationsWithoutImprovement);
status.setNbIterationWithoutImprovementSinceLastReload(status.getNbIterationWithoutImprovementSinceLastReload()+1);
return false;
}
}
2.5 ALNS::transitionCurrentSolution(ISolution* newSol)
利用判斷準則判斷是否要接受當前的解作為新的解。
bool ALNS::transitionCurrentSolution(ISolution* newSol)
{
//如果接受。
if(acceptanceCriterion->transitionAccepted(*bestSolManager,*currentSolution,*newSol,status))
{
delete currentSolution;
currentSolution = newSol->getCopy();
return true;
}
//不接受,原來解不變,什麽也不做。
else
{
return false;
}
}
2.6 ALNS::isStoppingCriterionMet()
判斷算法叠代是否遇到終止條件。各種條件說明已經註釋在代碼:
bool ALNS::isStoppingCriterionMet()
{
//是否找到最優可行解。
if((*(bestSolManager->begin()))->isFeasible() && (*(bestSolManager->begin()))->getObjectiveValue() == lowerBound)
{
return true;
}
else
{
switch(param->getStopCrit())
{
//是否達到最大叠代次數。
case ALNS_Parameters::MAX_IT: {
return nbIterations >= param->getMaxNbIterations();
}
//是否達到最大限制運行時間。
case ALNS_Parameters::MAX_RT: {
clock_t currentTime = clock();
double elapsed = (static_cast<double>(currentTime - startingTime)) / CLOCKS_PER_SEC;
return elapsed >= param->getMaxRunningTime();
}
//the maximum number of iterations without improvement.
case ALNS_Parameters::MAX_IT_NO_IMP: {
return nbIterationsWithoutImprovement >= param->getMaxNbIterationsNoImp();
}
//a mix of the MAX_IT, MAX_RT and MAX_IT_NO_IMP.
case ALNS_Parameters::ALL: {
if(nbIterations >= param->getMaxNbIterations())
{
return true;
}
if(nbIterationsWithoutImprovement >= param->getMaxNbIterationsNoImp())
{
return true;
}
clock_t currentTime = clock();
double elapsed = (static_cast<double>(currentTime - startingTime)) / CLOCKS_PER_SEC;
if(elapsed >= param->getMaxRunningTime())
{
return true;
}
return false;
}
default: {
assert(false);
return false;
}
}
}
}
2.7 ALNS::solve()
開始ALNS算法的叠代過程。這是將上面的模塊組裝起來,然後跑算法求解的過程了。
bool ALNS::solve()
{
startingTime = clock();
param->setLock();
acceptanceCriterion->startSignal();
opManager->startSignal();
stats.setStart();
//如果沒有遇到終止條件,那麽將一次次叠代下去。
while(!isStoppingCriterionMet())
{
performOneIteration();
}
//獲取運行結果,返回解是否可行。
string pathGlob = param->getStatsGlobPath();
pathGlob += name;
pathGlob += ".txt";
string pathOp = param->getStatsOpPath();
pathOp += name;
pathOp += ".txt";
stats.generateStatsFile(pathGlob,pathOp);
return (*(bestSolManager->begin()))->isFeasible();
}
03 小結
至此,ALNS主邏輯的代碼已經講完了,大家還是以整體為重點,整體把握為主。
細枝末節我們後期還會講的。並且……後面還有一大波代碼有得大家酸爽。
不過還是先把碗裏的吃完吧~咱們下期代碼再見!
代碼 | 自適應大鄰域搜索系列之(2) - ALNS算法主邏輯結構解析