【SPPRC(ESPPRC)】带资源约束的(基本)最短路问题

参考资料:《运筹优化常用模型、算法及案例实战》、微信公众号“数据魔法师”,“程序猿声”

给定起点和终点,希望在图上找到他们之间的最短路径。

SPPRC V.S. EPPRC

前者的全称是 Shortest Path Problem with Resource Constraint, 后者的全称是Elementary Shorest Path problem with Resource Constraint。
前者存在伪多项式时间的精确算法,而后者是NP-hard问题;
前者的约束相对比较宽松,允许多次经过一个节点;而后者比较严,要求每个节点最多经过一次。在无环图上,二者相差不大。
二者的求解算法——标签法。标签法分为标签设定法、标签校正法。标签设定法中那些选择要扩展的标签(在路径扩展步骤中)一直保留到标记过程结束,在后续的优超算法调用中,它们不会将其识别为可删除的或者可丢弃的。而在 标签校正法中,被扩展的标签有可能被丢弃。(摘自参考文献中的那本书,我还在理解这句话……)
典型的标签设定法:Dijkstra算法;
典型的标签校正算法:Bellman-Ford算法。

应用场景举例

使用列生成算法解VRPTW时,子问题是ESPPRC。考虑到ESPPRC不易求解,将其松弛为SPPRC。这么做得到的下界会比较松,从而可能导致分支树过大。
下面的代码(来自公众号“程序猿声”, 源码出自某国外大佬)是针对分支定界法解VRPTW中调用列生成算法过程中生成的子问题SPPRC,使用标签法求解:
这里的标签很像一个状态,它记录了“目前在哪里”(city), “从哪里来”(indexPrevLabel), “到达这个状态消耗了多少资源”(cost, tTime, demand),“这个状态是否被占优了”(dominated), “路上经过了哪些节点”(vertexVisited).
标签算法中的优超准则用于删除无用路径(也就是那些被占优的标签,或者说那些本身不能产生帕累托最优解、也不能产生可行扩展使得扩展后的路径产生帕累托最优解),以加速算法。
注:代码中有原作者的注释,也有我的拙作注释。

代码的结构:
SPPRC类中有
数据成员 userParam, labels;
类label;
类label的比较器;
函数shortestPath, 用来将带有负检验数的路径加入到 参数 routes中。
具体地,

package BranchAndPrice;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.TreeSet;

// shortest path with resource constraints
// inspired by Irnish and Desaulniers, "SHORTEST PATH PROBLEMS WITH RESOURCE CONSTRAINTS"
// for educational demonstration only - (nearly) no code optimization
//
// four main lists will be used:
// labels: array (ArrayList) => one dimensional unbounded vector
//		 list of all labels created along the feasible paths (i.e. paths satisfying the resource constraints)
//		
// U: sorted list (TreeSet) => one dimensional unbounded vector
//		sorted list containing the indices of the unprocessed labels (paths that can be extended to obtain a longer feasible path)
//
// P: sorted list (TreeSet) => one dimensional unbounded vector
//		sorted list containing the indices of the processed labels ending at the depot with a negative cost
//
// city2labels: matrix (array of ArrayList) => nbClients x unbounded
//		for each city, the list of (indices of the) labels attached to this city/vertex
//		before processing a label at vertex i, we compare pairwise all labels at the same vertex to remove the dominated ones

public class SPPRC {
	paramsVRP userParam;// #vehicles, capacity, ready time, due time,...
	ArrayList<label> labels; // list of labels

	class label { //路径和资源消耗情况,组成标签
		// we use a labelling algorithm. 标签算法
		// labels are attached to each vertex to specify the state of the resources 标签标识了各种资源的使用状态
		// when we follow a corresponding feasible path ending at this vertex 沿可行路径到达当前顶点
		public int city;                // current vertex
		public int indexPrevLabel;    // previous label in the same path (i.e. previous vertex in the same path with the state of the resources)
		// cost,tTime,demand表示这个标签的资源消耗情况
		public double cost;                // first resource: cost (e.g. distance or strict travel time)
		public float tTime;                // second resource: travel time along the path (including wait time and service time)
		public double demand;                // third resource: demand,i.e. total quantity delivered to the clients encountered on this path
		public boolean dominated;            // is this label dominated by another one? i.e. if dominated, forget this path.
		public boolean[] vertexVisited;

		label(int a1, int a2, double a3, float a4, double a5, boolean a6, boolean[] a7) {
			city = a1;
			indexPrevLabel = a2;
			cost = a3;
			tTime = a4;
			demand = a5;
			dominated = a6;
			vertexVisited = a7;
		}
	} // end class label

	class MyLabelComparator implements Comparator<Integer> {
		// the U treeSet is an ordered list
		// to maintain the order, we need to define a comparator: cost is the main criterium
		public int compare(Integer a, Integer b) {
			label A = labels.get(a);
			label B = labels.get(b);

			// Be careful!  When the comparator returns 0, it means that the two labels are considered EXACTLY the same ones!
			// This comparator is not only used to sort the lists!  When adding to the list, a value of 0 => not added!!!!!
			// 因为这里的cost等都是double型,在计算机中存在精度的问题,所以需要自己定义
			// 先比较cost, --> city when cities are the same, 继续比较 time,demand, 访问节点顺序
			if (A.cost - B.cost < -1e-7)
				return -1;
			else if (A.cost - B.cost > 1e-7)
				return 1;
			else {
				if (A.city == B.city) {
					if (A.tTime - B.tTime < -1e-7)
						return -1;
					else if (A.tTime - B.tTime > 1e-7)
						return 1;
					else if (A.demand - B.demand < -1e-7)
						return -1;
					else if (A.demand - B.demand > 1e-7)
						return 1;
					else {
						int i = 0;
						while (i < userParam.nbclients + 2) {
							if (A.vertexVisited[i] != B.vertexVisited[i]) {
								if (A.vertexVisited[i])// 相比于B, A提前访问了i,所以返回A
									return -1;
								else
									return 1;
							}
							i++;
						}
						return 0;
					}
				} else if (A.city > B.city)
					return 1;
				else
					return -1;
			}
		}
	}


	public void shortestPath(paramsVRP userParamArg, ArrayList<route> routes, int nbRoute) {
		// 标号算法的主体部分,因为有删除标签的行为,所以标签校正算法
		label current;
		int i, j, idx, nbsol, maxSol;
		double d, d2;//cumutive demand of the next customer and the next and next customer
		int[] checkDom;// checkDom[i]=2 means city i has 2 labels checked
		float tt, tt2; // timePoint when we arrive the next customer i and the next and next customer j
		Integer currentidx; // the index of current label

		this.userParam = userParamArg;
		// unprocessed labels list => ordered TreeSet List (?optimal:  need to be sorted like this?)
		// 这里的“未处理”表示“未拓展”的
		TreeSet<Integer> U = new TreeSet<Integer>(new MyLabelComparator());   // unprocessed labels list

		// processed labels list => ordered TreeSet List , “处理过的”表示“已经拓展的”
		TreeSet<Integer> P = new TreeSet<Integer>(new MyLabelComparator());   // processed labels list

		// array of labels // 一个标签,很像一个状态:从哪里来,现在在哪里,路上访问了谁,耗费了多少资源
		labels = new ArrayList<label>(2 * userParam.nbclients); // initial size at least larger than nb clients
		boolean[] cust = new boolean[userParam.nbclients + 2];

		// for depot 0
		cust[0] = true;// vertexVisited
		for (i = 1; i < userParam.nbclients + 2; i++) cust[i] = false;
		labels.add(new label(0, -1, 0.0, 0, 0, false, cust));    // first label: start from depot (client 0)
		U.add(0);

		// for each city, an array with the index of the corresponding labels (for dominance)
		checkDom = new int[userParam.nbclients + 2];// 每个客户节点 被检查过“占优性”的节点有多少个
		ArrayList<Integer>[] city2labels = new ArrayList[userParam.nbclients + 2];
		for (i = 0; i < userParam.nbclients + 2; i++) {
			city2labels[i] = new ArrayList<Integer>();
			checkDom[i] = 0;  // index of the first label in city2labels that needs to be checked for dominance (last labels added)
		}
		city2labels[0].add(0);

		nbsol = 0;
		maxSol = 2 * nbRoute;
		while ((U.size() > 0) && (nbsol < maxSol)) {
			// second term if we want to limit to the first solutions encountered to speed up the SPPRC (perhaps not the BP)
			// remark: we'll keep only nbRoute, but we compute 2 x nbRoute!
			// It makes a huge difference => we'll keep the most negative ones
			// this is something to analyze further!  how many solutions to keep and which ones?
			// process one label => get the index AND remove it from U
			currentidx = U.pollFirst(); // 从队首弹出一个 待拓展的路径的下标
			current = labels.get(currentidx);

			// check for dominance 查看 当前城市的标签们 有没有 被占优的,要删除掉 被占优的标签
			// code not fully optimized:
			int l1, l2;
			boolean pathdom;
			label la1, la2;
			ArrayList<Integer> cleaning = new ArrayList<Integer>();
			// check for dominance between the labels added since the last time
			// we came here with this city and all the other ones
			// ?? checkDom?? here, why not directly use i=0
			for (i = checkDom[current.city]; i < city2labels[current.city].size(); i++) {
				for (j = 0; j < i; j++) {
					l1 = city2labels[current.city].get(i);
					l2 = city2labels[current.city].get(j);
					la1 = labels.get(l1);
					la2 = labels.get(l2);
					// could happen since we clean 'city2labels' thanks
					// to 'cleaning' only after the double loop
					if (!(la1.dominated || la2.dominated)) { // 两个标签暂时都没有被占优
						// Q1: 判断 标签2 是否被占优了
						pathdom = true;
						for (int k = 1; pathdom && (k < userParam.nbclients + 2); k++) {
							//la1没有访问节点k 或者 la2访问了节点k 、、说明  la1对应的路径比la2短
							// if pathdom=true, then it means la1来过的 la2必定也来过
							// 看书! la1 访问过的节点 少于 la2 访问过的节点
							pathdom = (!la1.vertexVisited[k] || la2.vertexVisited[k]);
						}
						if (pathdom && (la1.cost <= la2.cost) && (la1.tTime <= la2.tTime)
								&& (la1.demand <= la2.demand)) {
							labels.get(l2).dominated = true;// l2 被占优了
							U.remove((Integer) l2);
							cleaning.add(l2);
							pathdom = false; // ?? why bother to do this
							//System.out.print(" ###Remove"+l2);
						}

						// Q2: 判断 标签1 是否被占优了
						pathdom = true;
						for (int k = 1; pathdom && (k < userParam.nbclients + 2); k++) {
							//la2没有访问节点k或者la1访问了节点k
							//如果pathdom=true, that means la2 的沿途节点数 比 la1少, 因为la2访问过的,la1必然访问过
							pathdom = (!la2.vertexVisited[k] || la1.vertexVisited[k]);
						}
						if (pathdom && (la2.cost <= la1.cost) && (la2.tTime <= la1.tTime) && (la2.demand <= la1.demand)) {
							labels.get(l1).dominated = true;// l1被占优了
							U.remove(l1);
							cleaning.add(l1);
							//System.out.print(" ###Remove"+l1);
							j = city2labels[current.city].size();// ?? get out of this for loop, so I guess it's kind of speed-up
						}
					}
				}
			}

			for (Integer c : cleaning)
				city2labels[current.city].remove((Integer) c);   // a little bit confusing but ok since c is an Integer and not an int!

			cleaning = null;
			// for current.city, how many non-denominant labels have we checked?
			checkDom[current.city] = city2labels[current.city].size();  // update checkDom: all labels currently in city2labels were checked for dom.

			// expand REF
			if (!current.dominated) {// 当前的这个label没有被占优
				//System.out.println("Label "+current.city+" "+current.indexPrevLabel+" "+current.cost+" "+current.ttime+" "+current.dominated);
				if (current.city == userParam.nbclients + 1) { // ??shortest path candidate to the depot! 此时 不能再扩展路径了
					if (current.cost < -1e-7) {                // SP candidate for the column generation
						P.add(currentidx);// 当前标签没有被占优,将它加到 已处理的集合P 中
						nbsol = 0; // 数 集合P 中 未被占优的标签 的个数
						for (Integer labi : P) { // labi : label index
							label s = labels.get(labi);
							if (!s.dominated)
								nbsol++;
						}
					}
				} else {
					// if not the depot, we can consider extensions of the path
					for (i = 0; i < userParam.nbclients + 2; i++) { // try to reach Customer i
						// don't go back to a vertex already visited or along a forbidden edge
						// expand this label to some customer i
						if ((!current.vertexVisited[i]) &&
								(userParam.dist[current.city][i] < userParam.verybig - 1e-6)) {
							// ttime
							tt = (float) (current.tTime + userParam.ttime[current.city][i]
									+ userParam.s[current.city]);
							if (tt < userParam.a[i])// 提前到了,等到时间窗开放,才能服务
								tt = userParam.a[i];
							// demand
							d = current.demand + userParam.d[i];
							//System.out.println("  -- "+i+" d:"+d+" t:"+tt);

							//the potential next customer is feasible?
							if ((tt <= userParam.b[i]) && (d <= userParam.capacity)) {
								// satisfy the time window constraint and capacity constraint
								// current.city --> i
								idx = labels.size();
								boolean[] newCust = new boolean[userParam.nbclients + 2]; // vertextVisited
								System.arraycopy(current.vertexVisited, 0, newCust, 0, userParam.nbclients + 2);
								newCust[i] = true;
								//speedup: third technique - Feillet 2004 as mentioned in Laporte's paper 、、??
								// current.city --> i --X--> j, so we marke all infeasible points j as 'visited'
								// then we could skip this point
								for (j = 1; j <= userParam.nbclients; j++) {
									if (!newCust[j]) {
										tt2 = (float) (tt + userParam.ttime[i][j] + userParam.s[i]);
										d2 = d + userParam.d[j];
										if ((tt2 > userParam.b[j]) || (d2 > userParam.capacity)) {
											newCust[j] = true;  // useless to visit this client , so marker it as 'visited'
										}
									}
								}
								// expand this label and then we obtain a new label( whose city is i) that is seen as unprocessed, so push it into U
								// label: city, indexPrevLabel, cost,tTime,demand, dominated, vertexVisited
								labels.add(new label(i, currentidx, current.cost + userParam.cost[current.city][i], tt, d, false, newCust));    // first label: start from depot (client 0)
								if (!U.add((Integer) idx)) { // idx: index of this new label
									// only happens if there exists already a label at this vertex with the same cost, time and demand and visiting the same cities before
									// It can happen with some paths where the order of the cities is permuted
									// I guess, e,g, 0-1-2-3-0, 0-3-1-2-0
									labels.get(idx).dominated = true; // => we can forget this label and keep only the other one?? only keep the previous one
									// ?? but how can you do this? why?
								}
								else {
									city2labels[i].add(idx);
								}
							}
						}
					}
				}
			}
		}
		// clean
		checkDom = null;

		// filtering: find the path from depot to the destination
		Integer lab;
		i = 0;
		while ((i < nbRoute) && ((lab = P.pollFirst()) != null)) {
			label s = labels.get(lab);
			if (!s.dominated) {
				if (/*(i < nbroute / 2) ||*/ (s.cost < -1e-4)) {
					// System.out.println(s.cost);
					//        	if(s.cost > 0) {
					//        		System.out.println("warning >>>>>>>>>>>>>>>>>>>>");
					//        	}
					route newRoute = new route();
					newRoute.setcost(s.cost);
					newRoute.addcity(s.city);
					// 按照indexPreLabel回溯,找到最优解
					int path = s.indexPrevLabel;
					while (path >= 0) {// 这里和前面depot0的标签中indexPreLabel=-1相呼应
						newRoute.addcity(labels.get(path).city);
						path = labels.get(path).indexPrevLabel;
					}
					newRoute.switchpath();
					routes.add(newRoute);
					i++;
				}
			}

		}// end while
	}
}

你可能感兴趣的:(运筹优化,算法)