/******************************************************************************* * Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com) * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser Public License v3 * which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt ******************************************************************************/ package com.opendoorlogistics.components.cluster.capacitated.solver; import gnu.trove.list.array.TIntArrayList; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.PriorityQueue; import java.util.Random; import com.opendoorlogistics.components.cluster.capacitated.solver.ContinueCallback.ContinueOption; import com.opendoorlogistics.core.utils.IntUtils; /** * This algorithm is a modified version of Mulvey and Beck (1984)'s first algorithm (MB1). * * The regret for an unassigned customer is the difference in cost between the best and next best cluster it can be assigned to. * * In the first instance p cluster centres are randomly chosen and customers are assigned to them in order of decreasing regret. Assignment never * breaks the capacity constraints. * * When and if all customers are assigned, the centre of each cluster is reassigned to the candidate customer contained within it that has least * travel cost to all other customers in the cluster. * * This may generate a new set of centres; the assignment and re-centering is repeated continually until no centres change. When the centres remain * stable a local search is then performed which swaps and moves customers between clusters. * * When this local search terminates the whole algorithm is then repeated either from a new set of random centres (multi-start) or with a random * subset of centres set from the current best solution and the rest randomly chosen, so we then perform an iterated local search. This is the major * difference between this implementation and the MB1 algorithm, which is completely multistart. * * The interpretation of MB1 is actually based on its description in 'A bionomic approach to the capacitated p-median problem' Maniezzo, Mingozzi and * Baldacci (didn't have access to the original Mulvey and Beck paper). * * @author Phil * */ public class Solver { private final Problem problem; private final int interchangeNNearest = 5; // private boolean logToConsole = false; private final ContinueCallback cont; private EvaluatedSolution best = null; private boolean useSwapMoves = false; private boolean useInsertionMoves = true; private int step = 0; public void setUseInsertionMoves(boolean useInsertionMoves) { this.useInsertionMoves = useInsertionMoves; } public enum HeuristicType { LOCAL_SEARCH, REGRET_REASSIGN, INITIAL_ASSIGN } public Solver(Problem problem, ContinueCallback cont) { this.problem = problem; this.cont = cont; } // public boolean isLogToConsole() { // return logToConsole; // } // public void setLogToConsole(boolean logToConsole) { // this.logToConsole = logToConsole; // } private int[] generateRandomPMedians(Random random) { int p = problem.getNbClusters(); int[] ret = new int[p]; for (int i = 0; i < p; i++) { ret[i] = problem.getFixedLocation(i); } TIntArrayList available = new TIntArrayList(); for (int i = 0; i < problem.getNbLocations(); i++) { if (IntUtils.contains(ret, i) == false) { available.add(i); } } generateRandomPMedians(random, available, ret); return ret; } private void generateRandomPMedians(Random random, TIntArrayList available, int[] chosen) { if (chosen.length != problem.getNbClusters()) { throw new RuntimeException(); } TIntArrayList unallocatedSlots = new TIntArrayList(problem.getNbClusters()); while (true) { // find unallocated unallocatedSlots.clear(); for (int i = 0; i < chosen.length; i++) { if (chosen[i] == -1) { unallocatedSlots.add(i); } } if (unallocatedSlots.size() == 0) { return; } // choose random unallocated slot int slotIndx = unallocatedSlots.get(random.nextInt(unallocatedSlots.size())); // choose random customer from the available customers if (available.size() == 0) { return; } int randIndx = random.nextInt(available.size()); chosen[slotIndx] = available.get(randIndx); available.removeAt(randIndx); } } private int[] mutatePMedians(Random random, int[] original) { if (original.length != problem.getNbClusters()) { throw new RuntimeException(); } // get the subset of clusters which can be unallocated int p = problem.getNbClusters(); TIntArrayList unallocatables = new TIntArrayList(); for (int i = 0; i < p; i++) { if (original[i] != -1 && problem.getFixedLocation(i) == -1) { unallocatables.add(i); } } // decide how many to unallocate int[] mutated = Arrays.copyOf(original, original.length); if(unallocatables.size()>0){ int nbToUnallocate = 1 + random.nextInt(unallocatables.size()); // unallocate them unallocatables.shuffle(random); for (int i = 0; i < nbToUnallocate; i++) { if (i < unallocatables.size()) { int clusterIndex = unallocatables.get(i); mutated[clusterIndex] = -1; } } } // get the available customers - these are any locations not used in the mutated cluster set TIntArrayList available = new TIntArrayList(); for (int i = 0; i < problem.getNbLocations(); i++) { if (IntUtils.contains(mutated, i) == false) { available.add(i); } } // generate others randomly generateRandomPMedians(random, available, mutated); return mutated; } /** * Do a regret-based assignment of all customers using the fixed centres and then reassign the centres afterwards. * * @param centres * @return */ private EvaluatedSolution regretBasedAssignment(int[] centres, HeuristicType currentHeuristicType) { // create an empty solution with the fixed centres (this assigns the customers which are centres) EvaluatedSolution evaluated = new EvaluatedSolution(problem, centres); evaluated.setAllCentresImmutable(true); // initialise all costs for unassigned customers (cluster centres are already assigned) int p = problem.getNbClusters(); int nc = problem.getNbLocations(); Cost[][] allCosts = new Cost[nc][]; for (int customerIndx = 0; customerIndx < nc; customerIndx++) { if (evaluated.getClusterIndex(customerIndx) == -1) { allCosts[customerIndx] = new Cost[p]; for (int clusterIndx = 0; clusterIndx < p; clusterIndx++) { Cost cost = new Cost(); allCosts[customerIndx][clusterIndx] = cost; if (centres[clusterIndx] != -1) { evaluated.evaluateSet(customerIndx, clusterIndx, cost); } else { // cluster is unavailable cost.setMax(); } } } } // initialise regret Regret[] regrets = new Regret[nc]; for (int customerIndx = 0; customerIndx < nc; customerIndx++) { if (evaluated.getClusterIndex(customerIndx) == -1) { Regret regret = new Regret(customerIndx); regret.update(allCosts[regret.getCustomerIndx()]); regrets[customerIndx] = regret; } } // keep a binary heap of regrets PriorityQueue<Regret> queue = new PriorityQueue<>(regrets.length, new Comparator<Regret>() { @Override public int compare(Regret o1, Regret o2) { // sort highest first int diff = -o1.compareTo(o2); // also sort by id so everything is unique if (diff == 0) { diff = Integer.compare(o1.getCustomerIndx(), o2.getCustomerIndx()); } return diff; } }); for (Regret regret : regrets) { if (regret != null) { queue.add(regret); } } // keep on popping the queue while (queue.size() > 0) { // check for quitting (only break loop if user actually quit) if (getContinue(currentHeuristicType) == ContinueOption.USER_CANCELLED) { return null; } Regret top = queue.poll(); int customerIndx = top.getCustomerIndx(); if (evaluated.getClusterIndex(customerIndx) != -1) { throw new RuntimeException(); } int clusterIndx = Cost.getLowestCostIndx(allCosts[customerIndx]); if (clusterIndx == -1 || allCosts[customerIndx][clusterIndx].isMax()) { // can't assign continue; } // do assignment and blank its cost records evaluated.setCustomerToCluster(customerIndx, clusterIndx); regrets[customerIndx] = null; allCosts[customerIndx] = null; // update all costs for this cluster over all unassigned customers for (int i = 0; i < nc; i++) { Cost[] costs = allCosts[i]; // check customer is still unassigned if (costs != null) { if (evaluated.getClusterIndex(i) != -1) { throw new RuntimeException(); } // get new cost for this cluster index evaluated.evaluateSet(i, clusterIndx, costs[clusterIndx]); // Update regret if the changed cluster corresponds to the first or second // best cost or the changed cluster has a better cost than the second best. Regret regret = regrets[i]; if (regret.getBestIndx() == clusterIndx || regret.getNextBestIndx() == clusterIndx || costs[clusterIndx].compareTo(costs[regret.getNextBestIndx()]) < 0) { // remove, update and re-add queue.remove(regret); regret.update(costs); queue.add(regret); } } } } // // test costs OK // Cost testCost = new Cost(evaluated.getCost()); // evaluated.update(); // if(Cost.isApproxEqual(evaluated.getCost(), testCost)==false){ // throw new RuntimeException(); // } // update to reassign centres and refresh (help prevent rounding error) evaluated.setAllCentresImmutable(false); evaluated.update(); return evaluated; } private boolean localSearchSingleStep(Random random, EvaluatedSolution solution) { // calculate a cluster to cluster distance matrix using the minimum distance to // a point in another cluster int p = problem.getNbClusters(); final double[][] matrix = new double[p][p]; for (int i = 0; i < p; i++) { matrix[i] = new double[p]; Arrays.fill(matrix[i], Double.MAX_VALUE); } int nc = problem.getNbLocations(); for (int i = 0; i < nc; i++) { int ci = solution.getClusterIndex(i); if (ci != -1) { for (int j = 0; j < nc; j++) { int cj = solution.getClusterIndex(j); if (cj != -1) { double distance = problem.getTravel(i, j); matrix[ci][cj] = Math.min(matrix[ci][cj], distance); } } } } // get a sorted list of nearest clusters for each cluster ArrayList<ArrayList<Integer>> nearestLists = new ArrayList<>(); for (int i = 0; i < p; i++) { ArrayList<Integer> nearest = new ArrayList<>(); nearestLists.add(nearest); for (int j = 0; j < p; j++) { if (i != j) { nearest.add(j); } } final int from = i; Collections.sort(nearest, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { double d1 = matrix[from][o1]; double d2 = matrix[from][o2]; return Double.compare(d1, d2); } }); while (nearest.size() > interchangeNNearest) { nearest.remove(nearest.size() - 1); } } // get a randomly ordered list of cluster indices TIntArrayList list = new TIntArrayList(p); for (int i = 0; i < p; i++) { list.add(i); } list.shuffle(random); // record starting cost solution.update(); Cost initial = new Cost(); initial.set(solution.getCost()); // loop over each cluster taking first improving moves for (int i = 0; i < p; i++) { int cli = list.get(i); // shuffle its nearest clusters List<Integer> nearest = nearestLists.get(cli); Collections.shuffle(nearest, random); for (int j = 0; j < nearest.size(); j++) { int clj = nearest.get(j); if (cli == clj) { throw new RuntimeException(); } if (random.nextBoolean()) { if (useInsertionMoves) { interclusterMoves(random,cli, clj, solution); } if (useSwapMoves) { interclusterSwaps(random,cli, clj, solution); } } else { if (useSwapMoves) { interclusterSwaps(random,cli, clj, solution); } if (useInsertionMoves) { interclusterMoves(random,cli, clj, solution); } } } // refresh solution after processing each cluster to help prevent round-off solution.update(); // check for quitting updateGlobalBest(solution); if (getContinue(HeuristicType.LOCAL_SEARCH) != ContinueOption.KEEP_GOING) { break; } } // see if we've improved by more than the round-off limit Cost newCost = solution.getCost(); if (!Cost.isApproxEqual(initial, newCost)) { return newCost.compareTo(initial) < 0; } return false; } /** * Try moving each location in cluster i to cluster j; take any improving moves * * @param clusteri * @param clusterj * @param solution */ private void interclusterMoves(Random random,int clusteri, int clusterj, EvaluatedSolution solution) { Cost cost = new Cost(); TIntArrayList customersi = new TIntArrayList(); solution.getCustomers(clusteri, customersi); int n = customersi.size(); customersi.shuffle(random); int fixedCentre = problem.getFixedLocation(clusteri); // loop over each customer in cluster i for (int i = 0; i < n; i++) { // check we're not moving the fixed centre int customeri = customersi.get(i); if (customeri != fixedCentre) { solution.evaluateSet(customeri, clusterj, cost); if (cost.getCapacityViolation() <= 0 && cost.getTravel() <= 0) { solution.setCustomerToCluster(customeri, clusterj); } } } } // private void mutateLocationAssignment(Random random,int clusteri, int clusterj,double mutatefraction, EvaluatedSolution solution) { // // TIntArrayList customersi = new TIntArrayList(); // solution.getCustomers(clusteri, customersi); // int n = customersi.size(); // customersi.shuffle(random); // int fixedCentre = problem.getFixedLocation(clusteri); // // // loop over each customer in cluster i // for (int i = 0; i < n; i++) { // // check we're not moving the fixed centre // int customeri = customersi.get(i); // if (customeri != fixedCentre) { // boolean mutate = random.nextDouble() < mutatefraction; // if(mutate){ // solution.setCustomerToCluster(customeri, clusterj); // } // } // } // } private void interclusterSwaps(Random random,int clusteri, int clusterj, EvaluatedSolution solution) { TIntArrayList customersi = new TIntArrayList(); solution.getCustomers(clusteri, customersi); customersi.shuffle(random); TIntArrayList customersj = new TIntArrayList(); solution.getCustomers(clusterj, customersj); customersj.shuffle(random); Cost cost = new Cost(); Cost bestSwap = new Cost(); int fixedCentrei = problem.getFixedLocation(clusteri); int fixedCentrej = problem.getFixedLocation(clusterj); int ni = customersi.size(); int nj = customersj.size(); for (int i = 0; i < ni; i++) { int customeri = customersi.get(i); if (customeri != fixedCentrei) { bestSwap.setMax(); int bestSwapCustomerIndx = -1; for (int j = 0; j < nj; j++) { // check still assigned to j before evaluating swap int customerj = customersj.get(j); if (customerj != fixedCentrej) { if (solution.getClusterIndex(customerj) == clusterj) { solution.evaluateSwap(customeri, customerj, cost); if (cost.compareTo(bestSwap) < 0) { bestSwap.set(cost); bestSwapCustomerIndx = customerj; } } } } // do swap if profitable if (bestSwap.getCapacityViolation() <= 0 && bestSwap.getTravel() <= 0) { solution.setCustomerToCluster(customeri, clusterj); solution.setCustomerToCluster(bestSwapCustomerIndx, clusteri); } } } } private ContinueOption getContinue(HeuristicType currentHeuristic) { // always call the callback even on step 0 as it also reports cost ContinueOption ret = cont.continueOptimisation(step, currentHeuristic, best); // always ensure we have a solution if the user hasn't cancelled if (best == null && ret == ContinueOption.FINISH_NOW) { return ContinueOption.KEEP_GOING; } return ret; } public synchronized EvaluatedSolution run() { Random random = new Random(123); // reset best = null; step = 0; while (true) { // get new centres from either mutation or random restart int[] centres; if (random.nextInt(3) == 0 || best == null) { centres = generateRandomPMedians(random); } else { centres = mutatePMedians(random, Utils.getCentres(best)); } // create an initial assigned solution using regret EvaluatedSolution assigned = regretBasedAssignment(centres, HeuristicType.INITIAL_ASSIGN); if(assigned!=null){ updateGlobalBest(assigned); } if (getContinue(HeuristicType.INITIAL_ASSIGN) != ContinueOption.KEEP_GOING) { break; } // then loop through cycles of choose centre, do regret based assignment EvaluatedSolution localBest = regretReassignLoop(assigned); if (getContinue(HeuristicType.REGRET_REASSIGN) != ContinueOption.KEEP_GOING) { break; } // now optimise using swaps and moves until the local search stagnates if (useInsertionMoves || useSwapMoves) { while (localSearchSingleStep(random, localBest)) { updateGlobalBest(localBest); if (getContinue(HeuristicType.LOCAL_SEARCH) != ContinueOption.KEEP_GOING) { break; } } } // check if we've beaten the best updateGlobalBest(localBest); step++; } return best; } private boolean updateGlobalBest(EvaluatedSolution sol) { if (best == null || (Cost.isApproxEqual(sol.getCost(), best.getCost()) == false && sol.getCost().compareTo(best.getCost()) <= 0)) { // deep copy best = new EvaluatedSolution(sol); if(Cost.isApproxEqual(best.getCost(), sol.getCost())==false){ throw new RuntimeException(); } return true; } return false; } private EvaluatedSolution regretReassignLoop(EvaluatedSolution initial) { // continue looping until no improvement EvaluatedSolution localBest = new EvaluatedSolution(initial); // int nbRegretLoops=0; while (true) { // get the current centres and do a regret-based assignment using them int[] centres = Utils.getCentres(localBest); EvaluatedSolution assigned = regretBasedAssignment(centres, HeuristicType.REGRET_REASSIGN); // if(logToConsole){ // System.out.println("After regret: " + assigned.getCost() ); // } // update local best (assigned can be null if user cancelled) if (assigned!=null && Cost.isApproxEqual(assigned.getCost(), localBest.getCost()) == false && assigned.getCost().compareTo(localBest.getCost()) < 0) { localBest = assigned; } else { break; } // update global best and check for quitting updateGlobalBest(localBest); if (getContinue(HeuristicType.REGRET_REASSIGN) != ContinueOption.KEEP_GOING) { break; } // nbRegretLoops++; } return localBest; } public void setUseSwapMoves(boolean doSwaps) { this.useSwapMoves = doSwaps; } }