参考资料:《运筹优化常用模型、算法及案例实战》、微信公众号“数据魔法师”,“程序猿声”
给定起点和终点,希望在图上找到他们之间的最短路径。
前者的全称是 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
}
}