Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
routing_breaks.cc
Go to the documentation of this file.
1// Copyright 2010-2025 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <algorithm>
17#include <cstdint>
18#include <limits>
19#include <utility>
20#include <vector>
21
22#include "absl/log/check.h"
23#include "absl/types/span.h"
27
28namespace operations_research {
29
30// break_duration_on_transition_ is initialized with an upper bound on the
31// number of transitions.
33 : break_duration_on_transition_(num_nodes, 0) {}
34
36 int path, DimensionValues& dimension_values,
37 const PrePostVisitValues& visits) {
38 using VehicleBreak = DimensionValues::VehicleBreak;
39 const absl::Span<VehicleBreak> vehicle_breaks =
40 absl::MakeSpan(dimension_values.MutableVehicleBreaks(path));
41 if (vehicle_breaks.empty()) return kUnchanged;
42 const absl::Span<Interval> cumuls = dimension_values.MutableCumuls(path);
43 const int num_cumuls = cumuls.size();
44 const absl::Span<const int64_t> travels = dimension_values.Travels(path);
45 const absl::Span<const int64_t> travel_sums =
46 dimension_values.TravelSums(path);
47 const absl::Span<const int64_t> pre_visits = visits.PreVisits(path);
48 const absl::Span<const int64_t> post_visits = visits.PostVisits(path);
49 const auto visit_start_max = [cumuls, pre_visits](int c) -> int64_t {
50 return CapSub(cumuls[c].max, pre_visits[c]);
51 };
52 const auto visit_end_min = [cumuls, post_visits](int c) -> int64_t {
53 return CapAdd(cumuls[c].min, post_visits[c]);
54 };
55 const auto travel_slack = [cumuls, travels](int c) -> int64_t {
56 const int64_t slack =
57 CapSub(CapSub(cumuls[c + 1].max, cumuls[c].min), travels[c]);
58 DCHECK_GE(slack, 0);
59 return slack;
60 };
62 // Propagations on cumuls are delayed to not break BinarySearch.
63 delayed_propagations_.clear();
64 // When breaks must be performed inside the route, accumulate location as a
65 // min/max window and their total duration.
66 int breaks_window_min = num_cumuls;
67 int breaks_window_max = -1;
68 int64_t breaks_total_duration = 0;
69 break_duration_on_transition_.Revert();
70 const int num_breaks = vehicle_breaks.size();
71 for (VehicleBreak& br : vehicle_breaks) {
72 if (br.is_performed.min == 0) continue;
73 if (!IncreaseMin(CapSub(br.end.min, br.start.max), &br.duration, &result)) {
74 return result;
75 }
76 // Find largest c_min such that time windows prevent break br to end
77 // before cumul c_min. In that case, all visits up to and including c_min
78 // must be performed before the break.
79 int c_min = BinarySearch<int, false>(
80 -1, num_cumuls, [&visit_start_max, break_end_min = br.end.min](int c) {
81 return visit_start_max(c) < break_end_min;
82 });
83 if (c_min >= 0) {
84 while (c_min < num_cumuls - 1 && travel_slack(c_min) < br.duration.min) {
85 ++c_min;
86 }
87 if (!IncreaseMin(visit_end_min(c_min), &br.start, &result) ||
88 !IncreaseMin(CapAdd(br.start.min, br.duration.min), &br.end,
89 &result)) {
90 return kInfeasible;
91 }
92 // The min break duration should fit, but in some cases (interbreaks) we
93 // may have br.start.min + br.duration.min < br.end.min.
94 // If br.end.min - br.start.min > travel_slack:
95 // - either the break is during the travel c_min -> c_min+1,
96 // in this case its duration is at most travel_slack,
97 // so that br.end.min - br.start <= travel_slack.
98 // Some of the travel c_min -> c_min+1 may be performed after the break.
99 // - or the break is after c_min+1, and all of the travel c_min -> c_min+1
100 // is performed before the break: br.start >= cumul[c_min] + travel.
101 // We have to take the weaker of the two alternatives, the first one.
102 if (c_min < num_cumuls - 1 &&
103 !IncreaseMin(CapSub(br.end.min, travel_slack(c_min)), &br.start,
104 &result)) {
105 return kInfeasible;
106 }
107 // Visit c_min must be before the break.
108 const int64_t cumul_ub = CapSub(br.start.max, post_visits[c_min]);
109 if (cumuls[c_min].min > cumul_ub) return kInfeasible;
110 delayed_propagations_.push_back(
111 {.value = cumul_ub, .index = c_min, .is_min = false});
112 }
113 // Find smallest c_max such that time windows prevent break br to start
114 // after cumul c_max. In that case, all visits including and after c_max
115 // must be performed after the break.
116 int c_max = BinarySearch<int, false>(
117 num_cumuls, -1,
118 [&visit_end_min, break_start_max = br.start.max](int c) {
119 return break_start_max < visit_end_min(c);
120 });
121 if (c_max < num_cumuls) {
122 while (c_max > 0 && travel_slack(c_max - 1) < br.duration.min) --c_max;
123 if (!DecreaseMax(visit_start_max(c_max), &br.end, &result) ||
124 !DecreaseMax(CapSub(br.end.max, br.duration.min), &br.start,
125 &result)) {
126 return kInfeasible;
127 }
128 // See the comment on the symmetric situation above.
129 if (c_max > 0 &&
130 !DecreaseMax(CapAdd(br.start.max, travel_slack(c_max - 1)), &br.end,
131 &result)) {
132 return kInfeasible;
133 }
134 // Visit c_max must be after the break, delay to not break BinarySearch.
135 const int64_t cumul_lb = CapAdd(br.end.min, pre_visits[c_max]);
136 if (cumuls[c_max].max < cumul_lb) return kInfeasible;
137 delayed_propagations_.push_back(
138 {.value = cumul_lb, .index = c_max, .is_min = true});
139 }
140 // If the break must be inside the route, it must be inside some cumul
141 // window, here [c_min, c_max].
142 // Overload checking: if transit + break duration do not fit, the break is
143 // infeasible.
144 // Edge finding: if it fits, push cumuls c_min/c_max to leave enough room.
145 if (0 <= c_min && c_max < num_cumuls) {
146 const int64_t transit = CapAdd(
147 br.duration.min, CapSub(travel_sums[c_max], travel_sums[c_min]));
148 if (CapAdd(cumuls[c_min].min, transit) > cumuls[c_max].max) {
149 return kInfeasible;
150 }
151 delayed_propagations_.push_back(
152 {.value = CapAdd(cumuls[c_min].min, transit),
153 .index = c_max,
154 .is_min = true});
155 delayed_propagations_.push_back(
156 {.value = CapSub(cumuls[c_max].max, transit),
157 .index = c_min,
158 .is_min = false});
159 breaks_window_min = std::min(breaks_window_min, c_min);
160 breaks_window_max = std::max(breaks_window_max, c_max);
161 CapAddTo(br.duration.min, &breaks_total_duration);
162 // If this break is forced on the transition c_min -> c_min + 1,
163 // accumulate its duration to this transition.
164 if (num_breaks > 1 && c_min + 1 == c_max) {
165 const int64_t total_duration = break_duration_on_transition_.Get(c_min);
166 break_duration_on_transition_.Set(
167 c_min, CapAdd(total_duration, br.duration.min));
168 }
169 }
170 }
171 // After the previous loop, there are no BinarySearch() calls, so there is
172 // no need to delay propagations.
173
174 // Per-transition reasoning: total break duration + travel must fit.
175 for (const int t : break_duration_on_transition_.ChangedIndices()) {
176 const int64_t total =
177 CapAdd(travels[t], break_duration_on_transition_.Get(t));
178 if (!IncreaseMin(CapAdd(cumuls[t].min, total), &cumuls[t + 1], &result) ||
179 !DecreaseMax(CapSub(cumuls[t + 1].max, total), &cumuls[t], &result)) {
180 return kInfeasible;
181 }
182 }
183 // Overload checker reasoning on overall break window.
184 if (breaks_total_duration > 0) {
185 const int64_t window_transit = CapAdd(
186 breaks_total_duration,
187 CapSub(travel_sums[breaks_window_max], travel_sums[breaks_window_min]));
188 if (!IncreaseMin(CapAdd(cumuls[breaks_window_min].min, window_transit),
189 &cumuls[breaks_window_max], &result) ||
190 !DecreaseMax(CapSub(cumuls[breaks_window_max].max, window_transit),
191 &cumuls[breaks_window_min], &result)) {
192 return kInfeasible;
193 }
194 }
195 for (const auto& [value, index, is_min] : delayed_propagations_) {
196 if (is_min && !IncreaseMin(value, &cumuls[index], &result)) {
197 return kInfeasible;
198 }
199 if (!is_min && !DecreaseMax(value, &cumuls[index], &result)) {
200 return kInfeasible;
201 }
202 }
203 return result;
204}
205
206// Add interbreak reasoning.
208 int path, DimensionValues& dimension,
209 absl::Span<const std::pair<int64_t, int64_t>> interbreaks) {
211 absl::Span<Interval> cumuls = dimension.MutableCumuls(path);
212 std::vector<VehicleBreak>& vehicle_breaks =
213 dimension.MutableVehicleBreaks(path);
214 // We use fake breaks for start/end of path:
215 // - start break: [kint64min, cumul[0])
216 // - end break: [cumul[n-1], kint64max).
217 const int64_t kint64min = std::numeric_limits<int64_t>::min();
218 const int64_t kint64max = std::numeric_limits<int64_t>::max();
219 vehicle_breaks.push_back({.start = {kint64min, kint64min},
220 .end = cumuls.front(),
221 .duration = {0, kint64max},
222 .is_performed = {1, 1}});
223 vehicle_breaks.push_back({.start = cumuls.back(),
224 .end = {kint64max, kint64max},
225 .duration = {0, kint64max},
226 .is_performed = {1, 1}});
227 const int num_breaks = vehicle_breaks.size();
228 for (const auto [limit, min_break_duration] : interbreaks) {
229 // Generate and sort events by increasing time. Events have to be
230 // regenerated for each interbreak, because end events depend on the limit.
231 usage_events_.clear();
232 for (int i = 0; i < num_breaks; ++i) {
233 const auto& br = vehicle_breaks[i];
234 if (br.is_performed.max == 0 || br.duration.max < min_break_duration) {
235 continue;
236 }
237 usage_events_.push_back(
238 {.time = br.start.min, .index = i, .is_start = true});
239 usage_events_.push_back(
240 {.time = CapAdd(br.end.max, limit), .index = i, .is_start = false});
241 }
242 std::sort(usage_events_.begin(), usage_events_.end());
243 // Main loop: sweep over events, maintain max profile height.
244 // When sweeping over time, we cross some time intervals of duration > 0:
245 // - if profile height is 0, no break can cover the interval. Infeasible.
246 // - if profile height is 1, the only active break must cover the interval.
247 // When num_active_breaks == 1, the xor of all active breaks is the only
248 // active break.
249 int num_active_breaks = 0;
250 int xor_active_breaks = 0;
251 int64_t previous_time = kint64min;
252 for (const UsageEvent& event : usage_events_) {
253 if (event.time != previous_time) {
254 DCHECK_GT(event.time, previous_time);
255 // Time changed: check covering condition.
256 if (num_active_breaks == 0) return kInfeasible;
257 if (num_active_breaks == 1) {
258 VehicleBreak& br = vehicle_breaks[xor_active_breaks];
259 const int64_t new_start_max =
260 std::min(previous_time, CapSub(br.end.max, min_break_duration));
261 const int64_t new_end_min =
262 std::max(CapSub(event.time, limit),
263 CapAdd(br.start.min, min_break_duration));
264 if (!DecreaseMax(new_start_max, &br.start, &result) ||
265 !IncreaseMin(new_end_min, &br.end, &result)) {
266 return kInfeasible;
267 }
268 if (xor_active_breaks < num_breaks - 2) {
269 const int64_t new_duration_min = std::max(
270 min_break_duration, CapSub(new_end_min, new_start_max));
271 if (!IncreaseMin(1, &br.is_performed, &result) ||
272 !IncreaseMin(new_duration_min, &br.duration, &result)) {
273 return kInfeasible;
274 }
275 }
276 }
277 }
278 // Update the set of active intervals.
279 num_active_breaks += event.is_start ? 1 : -1;
280 xor_active_breaks ^= event.index;
281 previous_time = event.time;
282 }
283 // Propagate fake start/end information to actual start/end.
284 const Interval& new_start = vehicle_breaks[num_breaks - 2].end;
285 const Interval& new_end = vehicle_breaks[num_breaks - 1].start;
286 if (!IntersectWith(new_start, &cumuls.front(), &result) ||
287 !IntersectWith(new_end, &cumuls.back(), &result)) {
288 vehicle_breaks.resize(num_breaks - 2);
289 return kInfeasible;
290 }
291 }
292 // Remove fake path start/end breaks.
293 vehicle_breaks.resize(num_breaks - 2);
294 return result;
295}
296
297} // namespace operations_research
PropagationResult PropagateInterbreak(int path, DimensionValues &dimension, absl::Span< const std::pair< int64_t, int64_t > > interbreaks)
static constexpr PropagationResult kInfeasible
PropagationResult FastPropagations(int path, DimensionValues &dimension_values, const PrePostVisitValues &visits)
static constexpr PropagationResult kUnchanged
absl::Span< Interval > MutableCumuls(int path)
absl::Span< const int64_t > Travels(int path) const
std::vector< VehicleBreak > & MutableVehicleBreaks(int path)
absl::Span< const int64_t > TravelSums(int path) const
absl::Span< const int64_t > PostVisits(int path) const
absl::Span< const int64_t > PreVisits(int path) const
OR-Tools root namespace.
int64_t CapAdd(int64_t x, int64_t y)
void CapAddTo(int64_t x, int64_t *y)
int64_t CapSub(int64_t x, int64_t y)
Point BinarySearch(Point x_true, Point x_false, std::function< bool(Point)> f)
static const int64_t kint64max
Definition types.h:32
static const int64_t kint64min
Definition types.h:31