def two_opt_double(route1: Route, route2: Route):
    arc_list1 = route1.required_arc_list
    arc_list2 = route2.required_arc_list
    idx1 = (len(arc_list1) - 1) // 2
    idx2 = (len(arc_list2) - 1) // 2
    half11 = arc_list1[:idx1]
    half12 = arc_list1[idx1:]
    half21 = arc_list2[:idx2]
    half22 = arc_list2[idx2:]
    load11 = 0
    for arc in half11:
        load11 += arc[0][IDX_DEMAND]
    load12 = route1.load - load11
    load21 = 0
    for arc in half21:
        load21 += arc[0][IDX_DEMAND]
    load22 = route2.load - load21
    l1 = load11 + load22
    l2 = load12 + load21
    l3 = load11 + load21
    l4 = load12 + load22
    new_route1 = new_route2 = new_route3 = new_route4 = None
    if l1 < CAPACITY and l2 < CAPACITY:
        new_route1 = Route()
        new_route1.required_arc_list = half11 + half22
        new_route1.load = l1
        new_route1.cost = get_route_cost(new_route1.required_arc_list)
        new_route2 = Route()
        new_route2.required_arc_list = half21 + half12
        new_route2.load = l2
        new_route2.cost = get_route_cost(new_route2.required_arc_list)
    if l3 < CAPACITY and l4 < CAPACITY:
        new_route3 = Route()
        reverse21 = deepcopy(half21)
        for arc in reverse21:
            arc[1] = not arc[1]
        reverse21.reverse()
        new_route3.required_arc_list = half11 + reverse21
        new_route3.load = l3
        new_route3.cost = get_route_cost(new_route3.required_arc_list)
        new_route4 = Route()
        reverse12 = deepcopy(half12)
        for arc in reverse12:
            arc[1] = not arc[1]
        reverse12.reverse()
        new_route4.required_arc_list = reverse12 + half22
        new_route4.load = l4
        new_route4.cost = get_route_cost(new_route4.required_arc_list)
    if new_route1 is None and new_route3 is None:
        return None, None
    elif new_route1 is None:
        return new_route3, new_route4
    elif new_route3 is None:
        return new_route1, new_route2
    else:
        if new_route1.cost + new_route2.cost < new_route3.cost + new_route4.cost:
            return new_route1, new_route2
        else:
            return new_route3, new_route4
def two_opt_single(route: Route):
    arc_list = route.required_arc_list
    # extract sub-route indices
    idx = sample(range(len(arc_list)), 2)
    if idx[0] < idx[1]:
        start, end = idx
    else:
        end, start = idx
    # create a new route
    new_route = Route()
    new_sub_list = deepcopy(arc_list[start: end + 1])
    for arc in new_sub_list:
        arc[1] = not arc[1]
    new_sub_list.reverse()
    new_arc_list = arc_list[: start] + new_sub_list + arc_list[end + 1:]
    new_route.required_arc_list = new_arc_list
    new_route.cost = get_route_cost(new_arc_list)
    new_route.load = route.load
    return new_route
def swap(solution):
    route_list = solution.route_list
    route_list_length = len(route_list)
    route_idx_1 = randrange(0, route_list_length)
    route_1 = route_list[route_idx_1]
    route_idx_2 = randrange(0, route_list_length - 1)
    if route_idx_2 >= route_idx_1:
        route_idx_2 += 1
    route_2 = route_list[route_idx_2]
    arc_list_1 = route_1.required_arc_list
    arc_list_2 = route_2.required_arc_list
    arc_idx_1 = randrange(0, len(arc_list_1))
    load_1 = route_1.load
    load_2 = route_2.load
    arc_1 = arc_list_1[arc_idx_1]
    demand_1 = arc_1[0][IDX_DEMAND]
    flag = False
    arc_2 = arc_idx_2 = new_load_1 = new_load_2 = None
    for arc_idx_2 in range(0, len(arc_list_2)):
        arc_2 = arc_list_2[arc_idx_2]
        demand_2 = arc_2[0][IDX_DEMAND]
        new_load_1 = load_1 + demand_2 - demand_1
        new_load_2 = load_2 + demand_1 - demand_2
        if new_load_1 <= CAPACITY and new_load_2 <= CAPACITY:
            flag = True
            break
    # swapping two routes is better than swapping one route
    if flag:
        new_arc_list_11 = arc_list_1[:]
        new_arc_list_12 = arc_list_1[:]
        new_arc_list_21 = arc_list_2[:]
        new_arc_list_22 = arc_list_2[:]
        new_arc_list_11[arc_idx_1] = arc_2
        new_arc_list_12[arc_idx_1] = [arc_2[0], not arc_2[1]]
        new_arc_list_21[arc_idx_2] = arc_1
        new_arc_list_22[arc_idx_2] = [arc_1[0], not arc_1[1]]
        cost11 = get_route_cost(new_arc_list_11)
        cost12 = get_route_cost(new_arc_list_12)
        cost21 = get_route_cost(new_arc_list_21)
        cost22 = get_route_cost(new_arc_list_22)
        new_route_1 = Route()
        new_route_2 = Route()
        new_route_1.load = new_load_1
        new_route_2.load = new_load_2
        if cost11 < cost12:
            new_route_1.required_arc_list = new_arc_list_11
            new_route_1.cost = cost11
        else:
            new_route_1.required_arc_list = new_arc_list_12
            new_route_1.cost = cost12
        if cost21 < cost22:
            new_route_2.required_arc_list = new_arc_list_21
            new_route_2.cost = cost21
        else:
            new_route_2.required_arc_list = new_arc_list_22
            new_route_2.cost = cost22
        new_route_list = route_list[:]
        new_route_list[route_idx_1] = new_route_1
        new_route_list[route_idx_2] = new_route_2
        new_solution = Solution()
        new_solution.route_list = new_route_list
        new_solution.quality = solution.quality + new_route_1.cost - route_1.cost + new_route_2.cost - route_2.cost
        return new_solution
    else:
        new_route = Route()
        new_route.load = route_1.load
        arc_list_1_length = len(arc_list_1)
        if arc_list_1_length < 2:
            return solution
        arc_idx_2 = randrange(0, arc_list_1_length - 1)
        if arc_idx_2 >= arc_idx_1:
            arc_idx_2 += 1
        arc_2 = arc_list_1[arc_idx_2]
        new_arc_list_1 = arc_list_1[:]
        new_arc_list_2 = arc_list_1[:]
        new_arc_list_3 = arc_list_1[:]
        new_arc_list_4 = arc_list_1[:]
        new_arc_list_1[arc_idx_1] = arc_2
        new_arc_list_1[arc_idx_2] = arc_1
        new_arc_list_2[arc_idx_1] = [arc_2[0], not arc_2[1]]
        new_arc_list_2[arc_idx_2] = arc_1
        new_arc_list_3[arc_idx_1] = arc_2
        new_arc_list_3[arc_idx_2] = [arc_1[0], not arc_1[1]]
        new_arc_list_4[arc_idx_1] = [arc_2[0], not arc_2[1]]
        new_arc_list_4[arc_idx_2] = [arc_1[0], not arc_1[1]]

        cost1 = get_route_cost(new_arc_list_1)
        cost2 = get_route_cost(new_arc_list_2)
        cost3 = get_route_cost(new_arc_list_3)
        cost4 = get_route_cost(new_arc_list_4)

        min_cost = min(cost1, cost2, cost3, cost4)
        new_route.cost = min_cost
        if cost1 == min_cost:
            new_route.required_arc_list = new_arc_list_1
        elif cost2 == min_cost:
            new_route.required_arc_list = new_arc_list_2
        elif cost3 == min_cost:
            new_route.required_arc_list = new_arc_list_3
        else:
            new_route.required_arc_list = new_arc_list_4
        new_solution = Solution()
        new_route_list = route_list[:]
        new_route_list[route_idx_1] = new_route
        new_solution.route_list = new_route_list
        new_solution.quality = solution.quality + new_route.cost - route_1.cost
        return new_solution
def single_insertion(solution: Solution):
    route_list = solution.route_list
    remove_route_idx = randrange(0, len(route_list))
    remove_route = route_list[remove_route_idx]
    remove_arc_list = remove_route.required_arc_list
    remove_arc_idx = randrange(0, len(remove_arc_list))
    remove_arc = remove_arc_list[remove_arc_idx]
    demand = remove_arc[0][IDX_DEMAND]
    insert_route_idx = None
    for i in range(len(route_list)):
        if i != remove_route_idx and route_list[i].load + demand < CAPACITY:
            insert_route_idx = i
            break
    if insert_route_idx is not None:
        insert_route = route_list[insert_route_idx]
        insert_arc_list = insert_route.required_arc_list
        insert_arc_idx = randrange(0, len(insert_arc_list) + 1)
        insert_arc = remove_arc
        new_insert_route = Route()
        new_insert_arc_list_1 = insert_arc_list[:]
        new_insert_arc_list_2 = insert_arc_list[:]
        new_insert_arc_list_1.insert(insert_arc_idx, insert_arc)
        new_insert_arc_list_2.insert(insert_arc_idx, [insert_arc[0], not insert_arc[1]])

        cost1 = get_route_cost(new_insert_arc_list_1)
        cost2 = get_route_cost(new_insert_arc_list_2)
        if cost1 < cost2:
            new_insert_route.required_arc_list = new_insert_arc_list_1
            new_insert_route.cost = cost1
        else:
            new_insert_route.required_arc_list = new_insert_arc_list_2
            new_insert_route.cost = cost2
        new_insert_route.load = insert_route.load + demand

        new_route_list = route_list[:]
        new_remove_arc_list = remove_arc_list[:]
        new_remove_route = None
        del new_remove_arc_list[remove_arc_idx]
        # update insert route first
        new_route_list[insert_route_idx] = new_insert_route
        if not new_remove_arc_list:
            del new_route_list[remove_route_idx]
        else:
            new_remove_route = Route()
            new_remove_route.required_arc_list = new_remove_arc_list
            new_remove_route.load = remove_route.load - demand
            new_remove_route.cost = get_route_cost(new_remove_arc_list)
            new_route_list[remove_route_idx] = new_remove_route
        diff1 = (0 if new_remove_route is None else new_remove_route.cost) - remove_route.cost
        diff2 = new_insert_route.cost - insert_route.cost
        # print('cost1', diff1 + diff2)
        new_solution = Solution()
        new_solution.route_list = new_route_list
        new_solution.quality = solution.quality + diff1 + diff2
        # print(new_solution.quality, get_quality(new_route_list))
        return new_solution
    else:
        length = len(remove_arc_list)
        if length < 2:
            return solution
        insert_arc_idx = randrange(0, length - 1)
        if insert_arc_idx >= remove_arc_idx:
            insert_arc_idx += 1
        new_route = Route()
        new_route.load = remove_route.load
        arc_list_1 = remove_arc_list[:]
        arc_list_2 = remove_arc_list[:]
        arc_list_1.insert(insert_arc_idx, arc_list_1.pop(remove_arc_idx))
        del arc_list_2[remove_arc_idx]
        arc_list_2.insert(insert_arc_idx, [remove_arc[0], not remove_arc[1]])
        cost1 = get_route_cost(arc_list_1)
        cost2 = get_route_cost(arc_list_2)
        if cost1 < cost2:
            new_route.required_arc_list = arc_list_1
            new_route.cost = cost1
        else:
            new_route.required_arc_list = arc_list_2
            new_route.cost = cost2
        # new_route.required_arc_list = arc_list
        new_route_list = route_list[:]
        new_route_list[remove_route_idx] = new_route
        new_solution = Solution()
        new_solution.route_list = new_route_list
        diff = new_route.cost - remove_route.cost
        # print('cost2', diff)
        new_solution.quality = solution.quality + diff
        # print(new_solution.quality, get_quality(new_route_list))
        return new_solution