def relocate_customer(solution: Solution) -> Solution: """ Performs the best customer relocation move, based on routing costs. Of all such moves, the best is performed and the updated solution is returned. O(n^2), where n is the number of customers. Similar to reinsertion in Hornstra et al. (2020). References ---------- - Savelsbergh, Martin W. P. 1992. "The Vehicle Routing Problem with Time Windows: Minimizing Route Duration." *ORSA Journal on Computing* 4 (2): 146-154. """ improvements = Heap() costs = routing_costs(solution) for idx_route, curr_route in enumerate(solution.routes): for customer in curr_route: for route in solution.routes[idx_route:]: for idx in range(len(route) + 1): gain = _gain(costs, route, idx, customer) if gain >= 0 or not route.can_insert(customer, idx): # This is either infeasible, or not an improving move. continue # The following performs the proposed move on a copy of the # two routes involved. If the move is an improvement, it is # added to the pool of improving moves. old_route = deepcopy(curr_route) new_route = deepcopy(route) old_route.remove_customer(customer) new_route.insert_customer(customer, idx) current = route.cost() + curr_route.cost() proposed = old_route.cost() + new_route.cost() if proposed < current: improvements.push(proposed, (customer, idx, route)) if len(improvements) != 0: _, (customer, insert_idx, next_route) = improvements.pop() solution = copy(solution) route = solution.find_route(customer) if route is next_route and route.customers.index( customer) < insert_idx: # We re-insert into the same route, and the insert location will # shift once we remove the customer. This accounts for that. insert_idx -= 1 route.remove_customer(customer) next_route.insert_customer(customer, insert_idx) return solution
def _remove(destroyed: Solution, customer: int, removed: SetList, customers: Set): route = destroyed.find_route(customer) idx = route.customers.index(customer) # Selects the customer and the direct neighbours before and after the # customer, should those exist. selected = route.customers[max(idx - 1, 0):min(idx + 2, len(route))] for candidate in selected: removed.append(candidate) route.remove_customer(candidate) customers.remove(candidate)
def all_pickups_are_satisfied(solution: Solution) -> Tuple[bool, str]: """ Verifies all pickups are satisfied, that is, the pickup items are loaded according to a feasible loading plan for each customer. """ problem = Problem() for customer in range(problem.num_customers): route = solution.find_route(customer) pickup = problem.pickups[customer] for stacks in route.plan[route.customers.index(customer) + 1:]: try: # Quickly finds the stack this item is stored in, or raises # if no such stack exists. Just the existence is sufficient. stacks.find_stack(pickup) except LookupError: return False, f"{pickup} is not in the solution for all " \ f"appropriate legs of the route." return True, "All pick-ups are satisfied."