論文閱讀-2017-Vidal-NEARP

文章概況

  • 關鍵技術: 記憶結構, 雙向動態規劃, 下界估計, 鄰域空間結構的設計
  • 解決思路: 模式選擇的最優化在鄰域評價過程通過dp完成
  • 突出貢獻: 對於帶有最優方向選擇的一個服務序列(例如ARP的情形)上進行的經典鄰域操作, 提出O(1)時間複雜度的評價方法; 將最優方向的選擇一般化爲一個service的mode的最優選擇, 進而將成果應用到Ejection chains和其他兩種元啓發式搜索中.
  • 計算實驗: 算法優化適用於CARP, MCGRP, periodic CARP, multi-depot CARP, min-max k-vehicles windy rural postman problem.

概念要點:

VRP vs ARP

  • node routing中, 最短路決策隱含在距離矩陣計算中, arc routing中, 路徑決策會收到服務順序的影響, 同時也會受到mode選擇(如服務方向, 轉向限制, 交叉口延誤)等的影響
  • ARP的計算量大致是VRP的四倍

對ARP的問題求解的建模:

  • service: 例如在ARP中, 一個service是節點i和節點j之間的需求(與arc的方向無關)
  • mode: 具體的arc方向, 是從i->j還是從j->i, 因此在ARP中, mode的數量爲2
  • 將ARP求解分爲四個步驟, ASSIGNMENT, SEQUENCING, MODE CHOICES, PATH bewteen services
    • Asignment: 決定一個service由哪個route去服務;
    • Sequence: 決定在一個route中的service的順序;
    • Mode: 決定一個service所採用的mode;
    • Path: 在service k和l的模式確定的前提下, 確定兩個service之間的最短路徑

提高LS性能的關鍵:

  • 一個鄰域操作的評價複雜度壓縮到O(1)
  • 減少需要搜索的決策數, 縮小搜索空間
  • 高效剪枝不可能的鄰域變換(用LB代替精確評價), 縮小搜索空間

文章中對應的方案:

  • 對當前解的sequence進行預處理計算子序列的成本, 用子序列的concat表示鄰域操作, 進而有限次數的concat就可以計算新解成本O(1); 聯繫Vidal2011a
  • 將求解分層後, ASIGNMENT, SEQUENCE前兩層用搜索解決, 這時解決的就是VRP問題(只不過計算最短路或成本時, vrp是去查距離矩陣, 這裏是用dp計算), 下層用dp做, 因此縮小了決策維度
  • 提出了concat時估計下界的方法, 代替精確計算新解成本 [找高效的LB的方法和思想同樣適用VRP]

具體細節:

1. 對ARP問題的建模和表示

在這裏插入圖片描述

  • R1: 一個解中顯式的包含了ASSIGNMENT, SEQUENCING, MODE CHOICES, PATH bewteen services四個層次的決策子集; 不常用, 需要搜索所有決策的組合
  • R2A: R2A的表示方法確定確定了ASSIGNMENT, SEQUENCING, MODE CHOICES, Path通過兩個service的具體mode下的endpoint之間的最短路徑得到. 需要計算最短路徑. 常用, 但是sequence和mode緊耦合, 有些鄰域操作需要同時修改這兩個決策子集
  • R2B: R2B的表示裏, 所有service在一個big trip中沒有分多個route, 這種表示常用於羣算法的crossover操作. 需要O(n^2)的split算法. 不適用於LS階段. 這類元啓發式通常在LS階段也採用R2A.
  • R3: R3A和R3B的區別也是是否需要後續的split操作. R3的表示只包括了ASSIGNMENT, SEQUENCING兩部分, 它們將mode的決策用dp子問題解決, 因此保證了這些決策的最優性

2. 帶最優mode決策的鄰域搜索

符號表

symbol remark
xXx \in X solution, a set of Routes R(x)
σR(x)\sigma \in R(x) route, σ=(σ(1),σ(2),,σ(σ)\sigma=(\sigma(1),\sigma(2),\dots,\sigma(\|\sigma\|), σ(1)=σ(σ)=0,depot\sigma(1) = \sigma(\|\sigma\|) = 0, depot
siks_i^k cost of service i in mode kMik \in M_i, s01=0s_0^1=0 for depot
cijklc_{ij}^{kl} cost of travel between service i in k mode and service j in mode l
Hσ=(Vσ,Aσ)H_{\sigma}=(V_{\sigma}, A_{\sigma}) an acyclic auxiliary graph illustrated in follow picture
C(σˉ)[k,l]C(\bar{\sigma})[k,l] the cost of the partial shortest path in HσˉH_{\bar{\sigma}} between the first and last service in the sequence, for any combination of modes k and l

上述的輔助無環圖: 每個service是在一列, service的每個mode是一個節點, 之間的連線的cost是cijklc_{ij}^{kl}

在這裏插入圖片描述

基於concat的鄰域操作評價

首先通過O(n^2)複雜度的預處理, 計算當前解的子序列的C(σˉ)[k,l]C(\bar{\sigma})[k,l], 之後在進行鄰域操作時, 只需要按照下式計算兩個子序列σ1\sigma_1σ2\sigma_2連接得到的新解的cost.

在這裏插入圖片描述

這個計算相當於在上述輔助無環圖的簡化圖(concat時可以看做三個節點, 如下圖)上計算Floyd最短路.

在這裏插入圖片描述

cost下界的計算

要使鄰域操作得到的新解有提升, 則要求Δπ=C(σ1)+C(σ2)C(σ1)C(σ2)<0\Delta_\pi=C(\sigma_1')+C(\sigma_2')-C(\sigma_1)-C(\sigma_2) < 0, 則必有ΔπLB=CLB(σ1)+CLB(σ2)C(σ1)C(σ2)<0\Delta_\pi^{LB}=C_{LB}(\sigma_1')+C_{LB}(\sigma_2')-C(\sigma_1)-C(\sigma_2) < 0滿足, 如果ΔπLB\Delta_\pi^{LB}不滿足的話, 則不需要進一步精確計算cost.

一個路徑σ=σ1σK\sigma=\sigma_1\oplus\dots\oplus\sigma_K的下界可以寫成

在這裏插入圖片描述

這樣的LB能夠平均去除90%的鄰域操作, 使得精確評價的計算量不再是計算的瓶頸.

具體的LS模塊設計還需要包括以下幾部分: 鄰域操作->concat操作, 新解的接受策略(first, 50-first, total), 鄰域空間的約束, 是否允許非可行解等.

預處理相關的問題

預處理是O(n^2)的, 對於少車輛長路徑的情況並不友好, 文章限制了預處理的範圍, 僅對從depot出發和到達depot的子序列進行預處理, 並且序列的長度小於10, 這樣的限制使得大多數的concat能夠滿足O(1)的時間複雜度, 同時預處理的複雜度不至於過高, 少量的O(n)的concat操作是可以被接受的.

3. 一般化

一般化的基本模式是將原問題的模型轉化爲有限數量模式的service的序列的模型

ejection chain

在這裏插入圖片描述

  • 選取一個當前解的route的排列π\pi
  • 定義成ejection graph Hπ=(Vπ,Aπ)H_\pi=(V_\pi,A_\pi)
    • VπV_\pi將每個service作爲一個節點, 同時每個路徑還有一個null節點(白色點), R(i)表示i節點所在路徑
    • 對於每對節點, i和j, 只有R(i)在R(j)之前時,i->j之間有一個arc.其中,i,j都可以是null節點
    • arc(i,j)的成本hijh_{ij}與R(j)將j這個服務移除並在該位置插入i服務的成本有關, null節點表示不插入或不移除任何服務.hijh_{ij}可以爲負.當不移除僅插入時, 插入到最優位置的成本作爲hijh_{ij}
  • 找到該有向無環圖中的最短路,如果最短路具有負的cost, 則將這個路徑的操作應用到當前解.

Service on nodes, edges and arcs

將每個service轉換爲一個節點, 不同類型的service有不同數量的mode, NODE, ARC具有一種mode, EDGE具有兩種mode(正反方向). 其他操作與以往一致

Turn restrictions and delays at complex intersections

將雙向道路, 轉向點, 單向道路分別抽象成Edge, Node, Arc, 然後可以套用上面的方法. 對於Node有p中mode(轉向方式數)

在VRP中一般化的情況

In some cases, the mode choices may involve other resources such as time, capacity, and service levels, as objectives or constraints. The methods of this paper can be further generalized to these applications, but leading to resource constrained shortest path subproblems (RCSPP) in cases where several resources are involved.

結論

通過本文的方法, 可以將帶模式選擇的鄰域搜索的複雜度降到和不帶模式選擇的一致的水平, 同時因爲方法將模式選擇降至dp中進行, 保證結果的最優性同時能夠讓啓發式搜索集中在更有限的決策集中.

文章的工作在VRP和ARP之間建立的了聯繫, 給啓發式的研究提供新的方向(考慮更換鄰域結構提高解質量)

計算實驗

代碼分析,以CARP爲例

總體流程

//Genetic.cpp::evolveHGA 
while (nbIterNonProd < maxIterNonProd && (clock() - debut <= ticks) && (!params->isSearchingFeasible || population->getIndividuBestValide() == NULL))
	{
		// CROSSOVER
		parent1 = population->getIndividuBinT(); // Pick two individuals per binary tournament
		parent2 = population->getIndividuBinT(); // Pick two individuals per binary tournament
		rejeton->recopieIndividu(rejeton,parent1); // Put them in adequate data structures
		rejeton2->recopieIndividu(rejeton2,parent2); // Put them in adequate data structures

		if (!params->periodique && !params->multiDepot) 
			resultCross = crossOX(); // Pick OX crossover if its a single-period problem
		else 
			resultCross = crossPIX() ; // Otherwise PIX (see Vidal et al 2012 -- OR)

		// SPLIT
		rejeton->generalSplit();

		// LOCAL SEARCH
		rejeton->updateLS(); //預處理
		rejeton->localSearch->runSearchTotal(); // 實際LS
		rejeton->updateIndiv(); // 對於LS中改動的route單獨更新
		population->updateNbValides(rejeton);
		place = population->addIndividu(rejeton) ;

		// POSSIBLE REPAIR
		// SOME TRACES
		// DIVERSIFICATION
		// PENALTY MANAGEMENT
		// MORE TRACES
		if (traces && nbIter % 500 == 0)
		//...
		nbIter ++ ;
	}

對應的Node中的預處理部分的數據結構

//Noeud.h
// pointer towards the preprocessed SeqData data structures
// "i" is considered to be the current customer
vector <SeqData *> seqi_j ; // data for (i,j) with j > i
vector <SeqData *> seqj_i ; // data for (j,i) (for the same subsequence as i_j, but reversed)
SeqData * seq0_i ; // data for (0,i)
SeqData * seqi_n ; // data for (i,n), n is the end of the route
SeqData * seqi_0 ; // data for (i,0) (for the reversed route)
SeqData * seqn_i ; // data for (n,i) (for the reversed route)
// the same pointers as (i,j) for some values, but simpler to call
SeqData * seq1 ; // data for (i) 
SeqData * seq12 ; // data for (i,i+1)
SeqData * seq21 ; // data for (i+1,i)
SeqData * seq123 ; // data for (i,i+1,i+2)
SeqData * seq321 ; // data for (i+2,i+1,i)

從前往後, 從後往前預處理路徑

//Route.cpp::updateRouteData

	// Computing the auxiliary data on any subsequence (0..i), using forward recursion
	noeud->seq0_i->initialisation(noeud->cour,params,individu,day,isForPrinting);
	noeud->seqi_0->initialisation(noeud->cour,params,individu,day,false);

	firstIt = true ;
	while (!noeud->estUnDepot || firstIt) //不是depot或是第一次迭代
	{
		firstIt = false ;
		noeud = noeud->suiv ; // next depot or client in the route
		Xvalue += params->cli[noeud->cour].coord.x ;
		Yvalue += params->cli[noeud->cour].coord.y ;
		nbNodes ++ ;
		place ++ ;
		noeud->place = place ;
    //...
			noeud->seq0_i->concatOneAfter(noeud->pred->seq0_i,noeud->cour,individu,day); //計算起點0到當前點的cost
		noeud->seqi_0->concatOneBefore(noeud->pred->seqi_0,noeud->cour,individu,day); // reverse
	}

	// Computing the auxiliary data on any subsequence (i..n), using backward recursion
  //...
	firstIt = true ;
	while ( !noeud->estUnDepot || firstIt )
	{
		firstIt = false ;
		noeud = noeud->pred ;
		noeud->seqi_n->concatOneBefore(noeud->suiv->seqi_n,noeud->cour,individu,day); // 計算當前點到終點n的cost
		noeud->seqn_i->concatOneAfter(noeud->suiv->seqn_i,noeud->cour,individu,day); // reverse
	}

	// Computing the auxiliary data on any subsequence (i..j), using forward recursion
	// To gain a bit of time, we limit this preprocessing to subsequences such that i..j does not contain more than "sizeSD" elements
	// (More intelligent strategies could be used (e.g., the hierarchical approach of Irnich))
	Noeud * noeudi ;
	Noeud * noeudj ;
	noeudi = depot ;
	for (int i=0 ; i <= depot->pred->place ; i++)
	{
		noeudi->seqi_j[0]->initialisation(noeudi->cour,params,individu,day,false);
		noeudj = noeudi->suiv ;
		for (int j=1 ; j <= depot->pred->place - i && j < params->sizeSD ; j++)
		{
			noeudi->seqi_j[j]->concatOneAfter(noeudi->seqi_j[j-1],noeudj->cour,individu,day);
			noeudj = noeudj->suiv ;
		}
		noeudi = noeudi->suiv ;
	}

	noeudi = depot->pred ;
	for (int i=0 ; i <= depot->pred->place ; i++)
	{
		noeudi->seqj_i[0]->initialisation(noeudi->cour,params,individu,day,false);
		noeudj = noeudi->pred ;
		for (int j=1 ; j <= depot->pred->place - i && j < params->sizeSD ; j++)
		{
			noeudj->seqj_i[j]->concatOneAfter(noeudj->suiv->seqj_i[j-1],noeudj->cour,individu,day);
			noeudj = noeudj->pred ;
		}
		noeudi = noeudi->pred ;
	}

	// Checking the route feasibility
	double dist ;
	double violLoad ;
	double violDuration ;
	currentRouteCost = depot->pred->seq0_i->evaluation(depot->pred->seq0_i,depot->seq1,this->vehicle,dist,violDuration,violLoad);
	
	if (violDuration < 0.001 && violLoad < 0.001) 
		isFeasible = true ;
	else 
		isFeasible = false ;
}

LS的主體部分

// LocalSearch.cpp::mutationSameDay
int LocalSearch::mutationSameDay (int day)
{
	// Local Search for one given day
	// Based on the classic moves (Relocate, Swap, CROSS, 2-Opt and 2-Opt*)
	//...

	// We search and apply moves until a local minimum is attained
	while (!rechercheTerminee)
	{
		rechercheTerminee = true ;
		moveEffectue = 0 ;

		// For every node U in random order
		for ( int posU = 0 ; posU < (int)ordreParcours[day].size() ; posU ++ )
		{
			//...

			// In the CARP, some service removal do not reduce the cost of the route
			// In this case, very few moves can improve the solution (only 2-opt variants), and thus SWAP and RELOCATE variants do not need to be tested
      // 去除不必要的LS步驟以縮小LS的搜索空間
			costBeforeRemoval = noeudU->seq0_i->evaluation(routeU->depot->pred->seq0_i,routeU->vehicle);
			costAfterRemoval = noeudU->seq0_i->evaluation(noeudU->pred->seq0_i,noeudU->suiv->seqi_n,routeU->vehicle);
			gainWhenRemoving = (costAfterRemoval < costBeforeRemoval - 0.1) ;

			// For every node V in random order
			size2 = (int)noeudU->moves.size();
			for ( int posV = 0 ; posV < size2 && moveEffectue == 0 ; posV ++ ) // U,V兩個node的操作 O(N^2)
			{
				noeudV = &clients[day][noeudU->moves[posV]] ;
				routeV = noeudV->route ;
				vehicleV = routeV->vehicle ;

				// If we have not yet tested the moves involving the node U and the route of node V
				// (This flag is reset to false as soon as there is a modification in the route)
				if (!noeudV->route->nodeAndRouteTested[noeudU->cour])
				{
					y = noeudV->suiv ;
					if (routeV->cour != routeU->cour)
					{
						if (moveEffectue != 1)
						{
							tempNoeud = noeudV ;
							noeudV = noeudV->suiv ;
							y = noeudV->suiv ;
							// Testing Relocate, Swap, CROSS and I-CROSS (limited to 2 customers) of nodeU and nodeV 
							// Case where they are in different routes
							if (gainWhenRemoving) moveEffectue = interRouteGeneralInsert(); 
							noeudV = tempNoeud ;
							y = noeudV->suiv ;
						}

						// 2-Opt*
						if (moveEffectue != 1) 
							moveEffectue = interRoute2Opt ();

						// 2-Opt* (second type, where the routes can be reversed)
						if (moveEffectue != 1) 
							moveEffectue = interRoute2OptInv ();
					}
					else
					{
						tempNoeud = noeudV ;
						noeudV = noeudV->suiv ;
						y = noeudV->suiv ;

						// Testing Relocate, Swap, CROSS and I-CROSS (limited to 2 customers) of nodeU and nodeV 
						// Case where they are in the same route
						if (moveEffectue != 1 && gainWhenRemoving) 
							moveEffectue = intraRouteGeneralInsertDroite();
						noeudV = tempNoeud ;
						y = noeudV->suiv ;
					}
				}
			}

			noeudV = noeudU->suiv ;
			routeV = noeudV->route ;
			vehicleV = routeV->vehicle ;
			y = noeudV->suiv ;
			if (!noeudV->route->nodeAndRouteTested[noeudU->cour])
			{
				while (moveEffectue != 1 && !noeudV->estUnDepot)
				{ 
					// Testing 2-Opt between U and V (if the restriction of the granular search allows) 
					if (params->isCorrelated[noeudU->pred->cour][noeudV->cour] || params->isCorrelated[noeudU->cour][noeudV->suiv->cour]) 
						moveEffectue = intraRoute2Opt ();
					noeudV = noeudV->suiv ;
					y = noeudV->suiv ;
				}
			}

			// Special cases : testing the insertions behind the depot, and the empty routes
			//...

			// Say that we have tested the node U with all routes
			if (moveEffectue == 0)
				nodeTestedForEachRoute(noeudU->cour,day);
		}
	}
	// Calling the ejection chains at the end of the LS
	nbMoves += ejectionChains(day);
	return nbMoves ;
}

同一個類型的評價方法的評價和LB評價的對比

double SeqData::evaluation(SeqData * seq1, SeqData * seq2, Vehicle * vehicle) 
{
	Client * cli1 = &params->cli[seq1->lastNode] ;
	Client * cli2 = &params->cli[seq2->firstNode] ;

	double totDistance = min(
		min(seq1->bestCost00 + params->ar_distanceNodes[cli1->ar_nodesExtr0][cli2->ar_nodesExtr0] + seq2->bestCost00,
		seq1->bestCost00 + params->ar_distanceNodes[cli1->ar_nodesExtr0][cli2->ar_nodesExtr1] + seq2->bestCost10),
		min(seq1->bestCost01 + params->ar_distanceNodes[cli1->ar_nodesExtr1][cli2->ar_nodesExtr0] + seq2->bestCost00,
		seq1->bestCost01 + params->ar_distanceNodes[cli1->ar_nodesExtr1][cli2->ar_nodesExtr1] + seq2->bestCost10));

	return totDistance 
		+ max(seq1->load + seq2->load - vehicle->vehicleCapacity,0.0)*params->penalityCapa 
		+ max(totDistance - vehicle->maxRouteTime,0.0)*params->penalityLength ;
}
double SeqData::evaluationLB(SeqData * seq1, SeqData * seq2, Vehicle * vehicle) 
{
	double totDistance = seq1->distance + seq2->distance + params->timeCost[seq1->lastNode][seq2->firstNode] ;

	return totDistance
		+ max(seq1->load + seq2->load - vehicle->vehicleCapacity,0.0)*params->penalityCapa 
		+ max(totDistance - vehicle->maxRouteTime,0.0)*params->penalityLength ;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章